Skip to content

Commit 649e28a

Browse files
committed
Add i18n plugin with multi-framework support
Introduces the @embedpdf/plugin-i18n package, providing internationalization capabilities for React, Preact, Vue, and Svelte. Includes core plugin logic, translation hooks, shared components, locale definitions (English and Spanish), and integration into the example app. Updates dependencies and lockfile to register the new plugin.
1 parent ab7253d commit 649e28a

40 files changed

Lines changed: 923 additions & 0 deletions

examples/react-tailwind/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@
3333
"@embedpdf/plugin-history": "workspace:*",
3434
"@embedpdf/plugin-annotation": "workspace:*",
3535
"@embedpdf/plugin-view-manager": "workspace:*",
36+
"@embedpdf/plugin-commands": "workspace:*",
37+
"@embedpdf/plugin-i18n": "workspace:*",
3638
"@embedpdf/utils": "workspace:*",
3739
"@embedpdf/models": "workspace:*",
3840
"@embedpdf/pdfium": "workspace:*",

packages/plugin-i18n/package.json

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
{
2+
"name": "@embedpdf/plugin-i18n",
3+
"version": "1.0.0",
4+
"type": "module",
5+
"license": "MIT",
6+
"main": "./dist/index.cjs",
7+
"module": "./dist/index.js",
8+
"types": "./dist/index.d.ts",
9+
"exports": {
10+
".": {
11+
"types": "./dist/index.d.ts",
12+
"import": "./dist/index.js",
13+
"require": "./dist/index.cjs"
14+
},
15+
"./preact": {
16+
"types": "./dist/preact/index.d.ts",
17+
"import": "./dist/preact/index.js",
18+
"require": "./dist/preact/index.cjs"
19+
},
20+
"./react": {
21+
"types": "./dist/react/index.d.ts",
22+
"import": "./dist/react/index.js",
23+
"require": "./dist/react/index.cjs"
24+
},
25+
"./vue": {
26+
"types": "./dist/vue/index.d.ts",
27+
"import": "./dist/vue/index.js",
28+
"require": "./dist/vue/index.cjs"
29+
},
30+
"./svelte": {
31+
"types": "./dist/svelte/index.d.ts",
32+
"svelte": "./dist/svelte/index.js",
33+
"import": "./dist/svelte/index.js",
34+
"require": "./dist/svelte/index.cjs"
35+
}
36+
},
37+
"scripts": {
38+
"build:base": "vite build --mode base",
39+
"build:react": "vite build --mode react",
40+
"build:preact": "vite build --mode preact",
41+
"build:vue": "vite build --mode vue",
42+
"build:svelte": "vite build --mode svelte",
43+
"build": "pnpm run clean && concurrently -c auto -n base,react,preact,vue,svelte \"vite build --mode base\" \"vite build --mode react\" \"vite build --mode preact\" \"vite build --mode vue\" \"vite build --mode svelte\"",
44+
"clean": "rimraf dist",
45+
"lint": "eslint src --color",
46+
"lint:fix": "eslint src --color --fix"
47+
},
48+
"dependencies": {
49+
"@embedpdf/models": "workspace:*"
50+
},
51+
"devDependencies": {
52+
"@embedpdf/build": "workspace:*",
53+
"@embedpdf/core": "workspace:*",
54+
"@types/react": "^18.2.0",
55+
"typescript": "^5.0.0"
56+
},
57+
"peerDependencies": {
58+
"@embedpdf/core": "workspace:*",
59+
"react": ">=16.8.0",
60+
"react-dom": ">=16.8.0",
61+
"preact": "^10.26.4",
62+
"vue": ">=3.2.0",
63+
"svelte": ">=5 <6"
64+
},
65+
"files": [
66+
"dist",
67+
"README.md"
68+
],
69+
"repository": {
70+
"type": "git",
71+
"url": "https://github.com/embedpdf/embed-pdf-viewer",
72+
"directory": "packages/plugin-i18n"
73+
},
74+
"homepage": "https://www.embedpdf.com/docs",
75+
"bugs": {
76+
"url": "https://github.com/embedpdf/embed-pdf-viewer/issues"
77+
},
78+
"publishConfig": {
79+
"access": "public"
80+
}
81+
}

packages/plugin-i18n/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './lib';
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { Action } from '@embedpdf/core';
2+
import { Locale, LocaleCode } from './types';
3+
4+
export const SET_LOCALE = 'I18N/SET_LOCALE';
5+
export const REGISTER_LOCALE = 'I18N/REGISTER_LOCALE';
6+
7+
export interface SetLocaleAction extends Action {
8+
type: typeof SET_LOCALE;
9+
payload: LocaleCode;
10+
}
11+
12+
export interface RegisterLocaleAction extends Action {
13+
type: typeof REGISTER_LOCALE;
14+
payload: LocaleCode;
15+
}
16+
17+
export type I18nAction = SetLocaleAction | RegisterLocaleAction;
18+
19+
export const setLocale = (locale: LocaleCode): SetLocaleAction => ({
20+
type: SET_LOCALE,
21+
payload: locale,
22+
});
23+
24+
export const registerLocale = (locale: LocaleCode): RegisterLocaleAction => ({
25+
type: REGISTER_LOCALE,
26+
payload: locale,
27+
});
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
import { BasePlugin, PluginRegistry, createEmitter } from '@embedpdf/core';
2+
import {
3+
I18nCapability,
4+
I18nPluginConfig,
5+
I18nState,
6+
Locale,
7+
LocaleCode,
8+
LocaleChangeEvent,
9+
TranslationKey,
10+
} from './types';
11+
import {
12+
I18nAction,
13+
setLocale as setLocaleAction,
14+
registerLocale as registerLocaleAction,
15+
} from './actions';
16+
17+
export class I18nPlugin extends BasePlugin<
18+
I18nPluginConfig,
19+
I18nCapability,
20+
I18nState,
21+
I18nAction
22+
> {
23+
static readonly id = 'i18n' as const;
24+
25+
private config: I18nPluginConfig;
26+
private locales = new Map<LocaleCode, Locale>();
27+
private readonly localeChange$ = createEmitter<LocaleChangeEvent>();
28+
29+
constructor(id: string, registry: PluginRegistry, config: I18nPluginConfig) {
30+
super(id, registry);
31+
32+
this.config = config;
33+
34+
// Register all provided locales
35+
config.locales.forEach((locale) => {
36+
this.locales.set(locale.code, locale);
37+
this.dispatch(registerLocaleAction(locale.code));
38+
});
39+
40+
// Set initial locale
41+
this.dispatch(setLocaleAction(config.defaultLocale));
42+
}
43+
44+
async initialize(): Promise<void> {
45+
this.logger.info('I18nPlugin', 'Initialize', 'I18n plugin initialized');
46+
}
47+
48+
async destroy(): Promise<void> {
49+
this.localeChange$.clear();
50+
super.destroy();
51+
}
52+
53+
protected buildCapability(): I18nCapability {
54+
return {
55+
t: (key, params) => this.translate(key, params),
56+
setLocale: (locale) => this.setLocale(locale),
57+
getLocale: () => this.state.currentLocale,
58+
getAvailableLocales: () => [...this.state.availableLocales],
59+
getLocaleInfo: (code) => this.locales.get(code) ?? null,
60+
registerLocale: (locale) => this.registerLocale(locale),
61+
hasLocale: (code) => this.locales.has(code),
62+
onLocaleChange: this.localeChange$.on,
63+
};
64+
}
65+
66+
// ─────────────────────────────────────────────────────────
67+
// Translation Logic
68+
// ─────────────────────────────────────────────────────────
69+
70+
private translate(key: TranslationKey, params?: Record<string, string | number>): string {
71+
const locale = this.locales.get(this.state.currentLocale);
72+
const fallbackLocale = this.config.fallbackLocale
73+
? this.locales.get(this.config.fallbackLocale)
74+
: null;
75+
76+
// Try current locale
77+
let value = this.getNestedValue(locale?.translations, key);
78+
79+
// Try fallback locale
80+
if (!value && fallbackLocale) {
81+
value = this.getNestedValue(fallbackLocale.translations, key);
82+
}
83+
84+
// If still not found, return the key itself (with warning)
85+
if (!value) {
86+
this.logger.warn('I18nPlugin', 'MissingTranslation', `Translation not found for key: ${key}`);
87+
return key;
88+
}
89+
90+
// Interpolate parameters
91+
return this.interpolate(value, params);
92+
}
93+
94+
private getNestedValue(obj: any, path: string): string | undefined {
95+
if (!obj) return undefined;
96+
97+
const parts = path.split('.');
98+
let current = obj;
99+
100+
for (const part of parts) {
101+
if (current === undefined || current === null) return undefined;
102+
current = current[part];
103+
}
104+
105+
return typeof current === 'string' ? current : undefined;
106+
}
107+
108+
private interpolate(str: string, params?: Record<string, string | number>): string {
109+
if (!params) return str;
110+
111+
// Replace {key} with params[key]
112+
return str.replace(/\{(\w+)\}/g, (match, key) => {
113+
const value = params[key];
114+
return value !== undefined ? String(value) : match;
115+
});
116+
}
117+
118+
// ─────────────────────────────────────────────────────────
119+
// Locale Management
120+
// ─────────────────────────────────────────────────────────
121+
122+
private setLocale(locale: LocaleCode): void {
123+
if (!this.locales.has(locale)) {
124+
this.logger.warn('I18nPlugin', 'LocaleNotFound', `Locale '${locale}' is not registered`);
125+
return;
126+
}
127+
128+
const previousLocale = this.state.currentLocale;
129+
if (previousLocale === locale) return; // No change
130+
131+
this.dispatch(setLocaleAction(locale));
132+
133+
this.localeChange$.emit({
134+
previousLocale,
135+
currentLocale: locale,
136+
});
137+
138+
this.logger.info('I18nPlugin', 'LocaleChanged', `Locale changed to: ${locale}`);
139+
}
140+
141+
private registerLocale(locale: Locale): void {
142+
if (this.locales.has(locale.code)) {
143+
this.logger.warn(
144+
'I18nPlugin',
145+
'LocaleAlreadyRegistered',
146+
`Locale '${locale.code}' is already registered`,
147+
);
148+
return;
149+
}
150+
151+
this.locales.set(locale.code, locale);
152+
this.dispatch(registerLocaleAction(locale.code));
153+
154+
this.logger.info('I18nPlugin', 'LocaleRegistered', `Locale registered: ${locale.code}`);
155+
}
156+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { PluginPackage } from '@embedpdf/core';
2+
import { manifest, I18N_PLUGIN_ID } from './manifest';
3+
import { I18nPluginConfig, I18nState } from './types';
4+
import { I18nPlugin } from './i18n-plugin';
5+
import { I18nAction } from './actions';
6+
import { i18nReducer, initialState } from './reducer';
7+
8+
export const I18nPluginPackage: PluginPackage<I18nPlugin, I18nPluginConfig, I18nState, I18nAction> =
9+
{
10+
manifest,
11+
create: (registry, config) => new I18nPlugin(I18N_PLUGIN_ID, registry, config),
12+
reducer: i18nReducer,
13+
initialState,
14+
};
15+
16+
export * from './i18n-plugin';
17+
export * from './types';
18+
export * from './manifest';
19+
export * from './locales';
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { Locale } from '../types';
2+
3+
export const enUS: Locale = {
4+
code: 'en',
5+
name: 'English',
6+
translations: {
7+
commands: {
8+
zoom: {
9+
in: 'Zoom In',
10+
out: 'Zoom Out',
11+
fitWidth: 'Fit to Width',
12+
fitPage: 'Fit to Page',
13+
automatic: 'Automatic',
14+
level: 'Zoom Level ({level}%)',
15+
inArea: 'Zoom In Area',
16+
},
17+
fullscreen: {
18+
enter: 'Enter Full Screen',
19+
exit: 'Exit Full Screen',
20+
},
21+
rotate: {
22+
clockwise: 'Rotate Clockwise',
23+
counterclockwise: 'Rotate Counter-Clockwise',
24+
},
25+
menu: 'Menu',
26+
sidebar: 'Sidebar',
27+
search: 'Search',
28+
comment: 'Comment',
29+
download: 'Download',
30+
print: 'Print',
31+
openFile: 'Open PDF',
32+
save: 'Save',
33+
settings: 'Settings',
34+
view: 'View',
35+
annotate: 'Annotate',
36+
shapes: 'Shapes',
37+
redact: 'Redact',
38+
fillAndSign: 'Fill and Sign',
39+
form: 'Form',
40+
pan: 'Pan',
41+
pointer: 'Pointer',
42+
undo: 'Undo',
43+
redo: 'Redo',
44+
copy: 'Copy',
45+
screenshot: 'Screenshot',
46+
nextPage: 'Next Page',
47+
previousPage: 'Previous Page',
48+
},
49+
},
50+
};
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { Locale } from '../types';
2+
3+
export const esES: Locale = {
4+
code: 'es',
5+
name: 'Español',
6+
translations: {
7+
commands: {
8+
zoom: {
9+
in: 'Acercar',
10+
out: 'Alejar',
11+
fitWidth: 'Ajustar al ancho',
12+
fitPage: 'Ajustar a la página',
13+
automatic: 'Automático',
14+
level: 'Nivel de zoom ({level}%)',
15+
inArea: 'Acercar área',
16+
},
17+
fullscreen: {
18+
enter: 'Pantalla completa',
19+
exit: 'Salir de pantalla completa',
20+
},
21+
rotate: {
22+
clockwise: 'Girar a la derecha',
23+
counterclockwise: 'Girar a la izquierda',
24+
},
25+
menu: 'Menú',
26+
sidebar: 'Barra lateral',
27+
search: 'Buscar',
28+
comment: 'Comentario',
29+
download: 'Descargar',
30+
print: 'Imprimir',
31+
openFile: 'Abrir PDF',
32+
save: 'Guardar',
33+
settings: 'Configuración',
34+
view: 'Ver',
35+
annotate: 'Anotar',
36+
shapes: 'Formas',
37+
redact: 'Redactar',
38+
fillAndSign: 'Rellenar y firmar',
39+
form: 'Formulario',
40+
pan: 'Desplazar',
41+
pointer: 'Puntero',
42+
undo: 'Deshacer',
43+
redo: 'Rehacer',
44+
copy: 'Copiar',
45+
screenshot: 'Captura de pantalla',
46+
nextPage: 'Página siguiente',
47+
previousPage: 'Página anterior',
48+
},
49+
},
50+
};

0 commit comments

Comments
 (0)