From 5951a2e1ad0dca18e39c6497632144f43c895eed Mon Sep 17 00:00:00 2001
From: Kheireddine Boukhatem
<105202227+kheireddinebou@users.noreply.github.com>
Date: Fri, 10 Apr 2026 10:01:33 +0100
Subject: [PATCH] fix(i18n): allow localization of early loading states via
useStaticTranslation
---
packages/plugin-i18n/CHANGELOG.md | 2 +
.../plugin-i18n/src/shared/hooks/index.ts | 1 +
.../shared/hooks/use-static-translation.ts | 49 +++++++++++++++++++
viewers/snippet/src/components/app.tsx | 14 ++++--
viewers/snippet/src/config/translations.ts | 32 ++++++++++++
.../code-examples/headless/i18n-example.tsx | 28 ++++++++++-
.../react/headless/plugins/plugin-i18n.mdx | 19 +++++++
.../svelte/headless/plugins/plugin-i18n.mdx | 23 +++++++++
.../docs/vue/headless/plugins/plugin-i18n.mdx | 21 ++++++++
9 files changed, 184 insertions(+), 5 deletions(-)
create mode 100644 packages/plugin-i18n/src/shared/hooks/use-static-translation.ts
diff --git a/packages/plugin-i18n/CHANGELOG.md b/packages/plugin-i18n/CHANGELOG.md
index 843beea11..865dcc47e 100644
--- a/packages/plugin-i18n/CHANGELOG.md
+++ b/packages/plugin-i18n/CHANGELOG.md
@@ -71,6 +71,7 @@
- **useTranslation Hook**:
- Now supports optional `documentId` parameter for document-scoped translations
- `useTranslations(documentId?)` hook for getting document-scoped translation function
+ - `useStaticTranslation(config?)` hook/composable for static translation resolution before plugin initialization
### New Features
- Locale registration and management
@@ -115,6 +116,7 @@
- **useTranslation Hook**:
- Now supports optional `documentId` parameter for document-scoped translations
- `useTranslations(documentId?)` hook for getting document-scoped translation function
+ - `useStaticTranslation(config?)` hook/composable for static translation resolution before plugin initialization
### New Features
- Locale registration and management
diff --git a/packages/plugin-i18n/src/shared/hooks/index.ts b/packages/plugin-i18n/src/shared/hooks/index.ts
index 96096d2db..4d8152ead 100644
--- a/packages/plugin-i18n/src/shared/hooks/index.ts
+++ b/packages/plugin-i18n/src/shared/hooks/index.ts
@@ -1 +1,2 @@
export * from './use-i18n';
+export * from './use-static-translation';
diff --git a/packages/plugin-i18n/src/shared/hooks/use-static-translation.ts b/packages/plugin-i18n/src/shared/hooks/use-static-translation.ts
new file mode 100644
index 000000000..7f6e02e42
--- /dev/null
+++ b/packages/plugin-i18n/src/shared/hooks/use-static-translation.ts
@@ -0,0 +1,49 @@
+import { useCallback } from '@framework';
+import type { Locale, TranslationDictionary } from '../../lib/types';
+
+/**
+ * Resolves a translation key from locale data statically before the i18n plugin is initialized.
+ * Used for early loading states that appear before plugins are ready.
+ */
+export function getStaticTranslation(
+ locales: Locale[],
+ defaultLocale: string,
+ key: string,
+ fallback?: string,
+): string {
+ if (!locales || locales.length === 0) return fallback ?? key;
+
+ const locale = locales.find((l) => l.code === defaultLocale) ?? locales[0];
+ if (!locale) return fallback ?? key;
+
+ const parts = key.split('.');
+ let current: TranslationDictionary | string = locale.translations;
+ for (const part of parts) {
+ if (typeof current === 'string') return fallback ?? key;
+ current = current[part];
+ if (current === undefined) return fallback ?? key;
+ }
+ return typeof current === 'string' ? current : (fallback ?? key);
+}
+
+/**
+ * Hook that returns a static translation function for use before dthe i18n system is ready.
+ *
+ * @param config Locales and default locale to use for resolution
+ * @returns A translation function that takes a key and returns the translated string
+ */
+
+const EMPTY_ARRAY: Locale[] = [];
+
+export function useStaticTranslation(
+ config?: { locales?: Locale[]; defaultLocale?: string }
+) {
+ const locales = config?.locales || EMPTY_ARRAY;
+ const defaultLocale = config?.defaultLocale || 'en';
+
+ return useCallback(
+ (key: string, fallback?: string) => getStaticTranslation(locales, defaultLocale, key, fallback),
+ [locales, defaultLocale]
+ );
+}
+
diff --git a/viewers/snippet/src/components/app.tsx b/viewers/snippet/src/components/app.tsx
index 36e1c5288..931d0d0c0 100644
--- a/viewers/snippet/src/components/app.tsx
+++ b/viewers/snippet/src/components/app.tsx
@@ -38,7 +38,12 @@ import {
useActiveDocument,
} from '@embedpdf/plugin-document-manager/preact';
import { CommandsPluginPackage, CommandsPluginConfig } from '@embedpdf/plugin-commands/preact';
-import { I18nPluginPackage, I18nPluginConfig, useTranslations } from '@embedpdf/plugin-i18n/preact';
+import {
+ I18nPluginPackage,
+ I18nPluginConfig,
+ useTranslations,
+ useStaticTranslation,
+} from '@embedpdf/plugin-i18n/preact';
import {
MarqueeZoom,
ZoomMode,
@@ -544,12 +549,15 @@ export function PDFViewer({ config, onRegistryReady }: PDFViewerProps) {
[],
);
+ const i18nConfig = useMemo(() => ({ ...DEFAULTS.i18n, ...config.i18n }), [config.i18n]);
+ const staticTranslate = useStaticTranslation(i18nConfig);
+
if (!engine || isLoading)
return (
<>
-
+
>
);
@@ -689,7 +697,7 @@ export function PDFViewer({ config, onRegistryReady }: PDFViewerProps) {
>
) : (
-
+
)}
>
diff --git a/viewers/snippet/src/config/translations.ts b/viewers/snippet/src/config/translations.ts
index 297812551..f9179c64e 100644
--- a/viewers/snippet/src/config/translations.ts
+++ b/viewers/snippet/src/config/translations.ts
@@ -6,6 +6,10 @@ export const englishTranslations: Locale = {
code: 'en',
name: 'English',
translations: {
+ viewer: {
+ initializingPlugins: 'Initializing plugins...',
+ initializingEngine: 'Initializing PDF engine...',
+ },
search: {
placeholder: 'Search',
caseSensitive: 'Case sensitive',
@@ -400,6 +404,10 @@ export const germanTranslations: Locale = {
code: 'de',
name: 'Deutsch',
translations: {
+ viewer: {
+ initializingPlugins: 'Plugins werden initialisiert...',
+ initializingEngine: 'PDF-Engine wird initialisiert...',
+ },
search: {
placeholder: 'Suchen',
caseSensitive: 'Groß-/Kleinschreibung',
@@ -795,6 +803,10 @@ export const dutchTranslations: Locale = {
code: 'nl',
name: 'Nederlands',
translations: {
+ viewer: {
+ initializingPlugins: 'Plugins initialiseren...',
+ initializingEngine: 'PDF-engine initialiseren...',
+ },
search: {
placeholder: 'Zoeken',
caseSensitive: 'Hoofdlettergevoelig',
@@ -1190,6 +1202,10 @@ export const frenchTranslations: Locale = {
code: 'fr',
name: 'Français',
translations: {
+ viewer: {
+ initializingPlugins: 'Initialisation des plugins...',
+ initializingEngine: 'Initialisation du moteur PDF...',
+ },
search: {
placeholder: 'Rechercher',
caseSensitive: 'Respecter la casse',
@@ -1585,6 +1601,10 @@ export const spanishTranslations: Locale = {
code: 'es',
name: 'Español',
translations: {
+ viewer: {
+ initializingPlugins: 'Inicializando plugins...',
+ initializingEngine: 'Inicializando motor PDF...',
+ },
search: {
placeholder: 'Buscar',
caseSensitive: 'Distinguir mayúsculas',
@@ -1980,6 +2000,10 @@ export const simplifiedChineseTranslations: Locale = {
code: 'zh-CN',
name: '简体中文',
translations: {
+ viewer: {
+ initializingPlugins: '正在初始化插件...',
+ initializingEngine: '正在初始化PDF引擎...',
+ },
search: {
placeholder: '搜索',
caseSensitive: '大小写敏感',
@@ -2369,6 +2393,10 @@ export const swedishTranslations: Locale = {
code: 'sv',
name: 'Svenska',
translations: {
+ viewer: {
+ initializingPlugins: 'Initialiserar plugins...',
+ initializingEngine: 'Initialiserar PDF-motor...',
+ },
search: {
placeholder: 'Sök',
caseSensitive: 'Skiftlägeskänslig',
@@ -2760,6 +2788,10 @@ export const japaneseTranslations: Locale = {
code: 'ja',
name: '日本語',
translations: {
+ viewer: {
+ initializingPlugins: 'プラグインを初期化中...',
+ initializingEngine: 'PDFエンジンを初期化中...',
+ },
search: {
placeholder: '検索',
caseSensitive: '大文字小文字を区別',
diff --git a/website/src/content/docs/react/code-examples/headless/i18n-example.tsx b/website/src/content/docs/react/code-examples/headless/i18n-example.tsx
index 539553559..378059faa 100644
--- a/website/src/content/docs/react/code-examples/headless/i18n-example.tsx
+++ b/website/src/content/docs/react/code-examples/headless/i18n-example.tsx
@@ -27,6 +27,7 @@ import {
ParamResolvers,
useTranslations,
useI18nCapability,
+ useStaticTranslation,
} from '@embedpdf/plugin-i18n/react'
import { GlobalStoreState } from '@embedpdf/core'
import {
@@ -56,6 +57,10 @@ const englishLocale: Locale = {
toolbar: {
language: 'Language',
},
+ viewer: {
+ initializingPlugins: 'Initializing plugins...',
+ initializingEngine: 'Initializing PDF engine...',
+ },
},
}
@@ -76,6 +81,10 @@ const spanishLocale: Locale = {
toolbar: {
language: 'Idioma',
},
+ viewer: {
+ initializingPlugins: 'Inicializando plugins...',
+ initializingEngine: 'Inicializando motor de PDF...',
+ },
},
}
@@ -96,6 +105,10 @@ const germanLocale: Locale = {
toolbar: {
language: 'Sprache',
},
+ viewer: {
+ initializingPlugins: 'Plugins werden initialisiert...',
+ initializingEngine: 'PDF-Engine wird initialisiert...',
+ },
},
}
@@ -232,13 +245,22 @@ export const PDFViewer = () => {
[],
)
+ const { provides } = useI18nCapability()
+ const currentLocale = provides?.getLocale() ?? 'en'
+ const staticTranslate = useStaticTranslation({
+ locales: [englishLocale, spanishLocale, germanLocale],
+ defaultLocale: currentLocale,
+ })
+
if (isLoading || !engine) {
return (
- Loading PDF Engine...
+
+ {staticTranslate('viewer.initializingEngine')}
+
@@ -307,7 +329,9 @@ export const PDFViewer = () => {
- Initializing plugins...
+
+ {staticTranslate('viewer.initializingPlugins')}
+
diff --git a/website/src/content/docs/react/headless/plugins/plugin-i18n.mdx b/website/src/content/docs/react/headless/plugins/plugin-i18n.mdx
index d4401ca68..330c20d36 100644
--- a/website/src/content/docs/react/headless/plugins/plugin-i18n.mdx
+++ b/website/src/content/docs/react/headless/plugins/plugin-i18n.mdx
@@ -316,6 +316,25 @@ A convenience hook for translating a single key. Useful for derived state or sim
const zoomLabel = useTranslation('zoom.in', undefined, documentId)
```
+### Hook: `useStaticTranslation(config?)`
+
+Returns a static translation function for use before the i18n system is fully ready. Useful for early loading states.
+
+#### Parameters
+
+| Parameter | Type | Description |
+| :--- | :--- | :--- |
+| **`config`** | `{ locales?: Locale[]; defaultLocale?: string }` | **(Optional)** Configuration for static translation resolution. |
+
+#### Returns
+
+A translation function `(key: string, fallback?: string) => string`.
+
+```tsx
+const translate = useStaticTranslation({ locales, defaultLocale: 'en' })
+const loadingText = translate('loading.label', 'Loading...')
+```
+
### Hook: `useLocale()`
Returns the current locale code. Updates automatically when locale changes.
diff --git a/website/src/content/docs/svelte/headless/plugins/plugin-i18n.mdx b/website/src/content/docs/svelte/headless/plugins/plugin-i18n.mdx
index 467f990a5..94384e515 100644
--- a/website/src/content/docs/svelte/headless/plugins/plugin-i18n.mdx
+++ b/website/src/content/docs/svelte/headless/plugins/plugin-i18n.mdx
@@ -243,6 +243,29 @@ The primary store for translating text in components. Automatically updates when
| **`params`** | `Record` | Parameters to interpolate into the translation (e.g., `{ level: 150 }`). |
| **`fallback`** | `string` | Fallback text if translation is not found. |
+### Function: `useStaticTranslation(config?)`
+
+Returns a static translation function for use before the i18n system is fully ready. Useful for early loading states.
+
+#### Parameters
+
+| Parameter | Type | Description |
+| :--- | :--- | :--- |
+| **`config`** | `{ locales?: Locale[]; defaultLocale?: string }` | **(Optional)** Configuration for static translation resolution. |
+
+#### Returns
+
+A translation function `(key: string, fallback?: string) => string`.
+
+```svelte
+
+```
+
### Store: `useI18nCapability()`
Provides access to all i18n operations.
diff --git a/website/src/content/docs/vue/headless/plugins/plugin-i18n.mdx b/website/src/content/docs/vue/headless/plugins/plugin-i18n.mdx
index 4a76e2149..48ad14304 100644
--- a/website/src/content/docs/vue/headless/plugins/plugin-i18n.mdx
+++ b/website/src/content/docs/vue/headless/plugins/plugin-i18n.mdx
@@ -325,6 +325,27 @@ const zoomLabel = useTranslation('zoom.in', undefined, () => props.documentId)
```
+### Composable: `useStaticTranslation(config?)`
+
+Returns a static translation function for use before the i18n system is fully ready. Useful for early loading states.
+
+#### Parameters
+
+| Parameter | Type | Description |
+| :--- | :--- | :--- |
+| **`config`** | `{ locales?: Locale[]; defaultLocale?: string }` | **(Optional)** Configuration for static translation resolution. |
+
+#### Returns
+
+A translation function `(key: string, fallback?: string) => string`.
+
+```vue
+
+```
+
### Composable: `useLocale()`
Returns the current locale code. Updates automatically when locale changes.