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.