@embedpdf/plugin-i18n 2.15.0 → 3.0.0-next.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/LICENSE +192 -21
  2. package/dist/index.cjs +268 -2
  3. package/dist/index.cjs.map +1 -1
  4. package/dist/index.d.cts +159 -0
  5. package/dist/index.d.ts +159 -1
  6. package/dist/index.js +262 -397
  7. package/dist/index.js.map +1 -1
  8. package/package.json +18 -58
  9. package/dist/lib/actions.d.ts +0 -15
  10. package/dist/lib/i18n-plugin.d.ts +0 -28
  11. package/dist/lib/index.d.ts +0 -9
  12. package/dist/lib/locales/en.d.ts +0 -2
  13. package/dist/lib/locales/es.d.ts +0 -2
  14. package/dist/lib/locales/index.d.ts +0 -2
  15. package/dist/lib/manifest.d.ts +0 -4
  16. package/dist/lib/reducer.d.ts +0 -5
  17. package/dist/lib/types.d.ts +0 -104
  18. package/dist/preact/adapter.d.ts +0 -5
  19. package/dist/preact/core.d.ts +0 -1
  20. package/dist/preact/index.cjs +0 -2
  21. package/dist/preact/index.cjs.map +0 -1
  22. package/dist/preact/index.d.ts +0 -1
  23. package/dist/preact/index.js +0 -70
  24. package/dist/preact/index.js.map +0 -1
  25. package/dist/react/adapter.d.ts +0 -2
  26. package/dist/react/core.d.ts +0 -1
  27. package/dist/react/index.cjs +0 -2
  28. package/dist/react/index.cjs.map +0 -1
  29. package/dist/react/index.d.ts +0 -1
  30. package/dist/react/index.js +0 -69
  31. package/dist/react/index.js.map +0 -1
  32. package/dist/shared/components/index.d.ts +0 -1
  33. package/dist/shared/components/translate.d.ts +0 -35
  34. package/dist/shared/hooks/index.d.ts +0 -1
  35. package/dist/shared/hooks/use-i18n.d.ts +0 -43
  36. package/dist/shared/index.d.ts +0 -3
  37. package/dist/shared-preact/components/index.d.ts +0 -1
  38. package/dist/shared-preact/components/translate.d.ts +0 -35
  39. package/dist/shared-preact/hooks/index.d.ts +0 -1
  40. package/dist/shared-preact/hooks/use-i18n.d.ts +0 -43
  41. package/dist/shared-preact/index.d.ts +0 -3
  42. package/dist/shared-react/components/index.d.ts +0 -1
  43. package/dist/shared-react/components/translate.d.ts +0 -35
  44. package/dist/shared-react/hooks/index.d.ts +0 -1
  45. package/dist/shared-react/hooks/use-i18n.d.ts +0 -43
  46. package/dist/shared-react/index.d.ts +0 -3
  47. package/dist/svelte/components/Translate.svelte.d.ts +0 -9
  48. package/dist/svelte/components/index.d.ts +0 -1
  49. package/dist/svelte/hooks/index.d.ts +0 -1
  50. package/dist/svelte/hooks/use-i18n.svelte.d.ts +0 -41
  51. package/dist/svelte/index.cjs +0 -2
  52. package/dist/svelte/index.cjs.map +0 -1
  53. package/dist/svelte/index.d.ts +0 -3
  54. package/dist/svelte/index.js +0 -90
  55. package/dist/svelte/index.js.map +0 -1
  56. package/dist/vue/components/index.d.ts +0 -1
  57. package/dist/vue/components/translate.vue.d.ts +0 -9
  58. package/dist/vue/hooks/index.d.ts +0 -1
  59. package/dist/vue/hooks/use-i18n.d.ts +0 -30
  60. package/dist/vue/index.cjs +0 -2
  61. package/dist/vue/index.cjs.map +0 -1
  62. package/dist/vue/index.d.ts +0 -3
  63. package/dist/vue/index.js +0 -99
  64. package/dist/vue/index.js.map +0 -1
package/dist/index.js CHANGED
@@ -1,398 +1,263 @@
1
- import { BasePlugin, createEmitter, createScopedEmitter, arePropsEqual } from "@embedpdf/core";
2
- const enUS = {
3
- code: "en",
4
- name: "English",
5
- translations: {
6
- commands: {
7
- zoom: {
8
- in: "Zoom In",
9
- out: "Zoom Out",
10
- fitWidth: "Fit to Width",
11
- fitPage: "Fit to Page",
12
- automatic: "Automatic",
13
- level: "Zoom Level ({level}%)",
14
- inArea: "Zoom In Area"
15
- },
16
- fullscreen: {
17
- enter: "Enter Full Screen",
18
- exit: "Exit Full Screen"
19
- },
20
- rotate: {
21
- clockwise: "Rotate Clockwise",
22
- counterclockwise: "Rotate Counter-Clockwise"
23
- },
24
- menu: "Menu",
25
- sidebar: "Sidebar",
26
- search: "Search",
27
- comment: "Comment",
28
- download: "Download",
29
- print: "Print",
30
- openFile: "Open PDF",
31
- save: "Save",
32
- settings: "Settings",
33
- view: "View",
34
- annotate: "Annotate",
35
- shapes: "Shapes",
36
- redact: "Redact",
37
- fillAndSign: "Fill and Sign",
38
- form: "Form",
39
- pan: "Pan",
40
- pointer: "Pointer",
41
- undo: "Undo",
42
- redo: "Redo",
43
- copy: "Copy",
44
- screenshot: "Screenshot",
45
- nextPage: "Next Page",
46
- previousPage: "Previous Page"
47
- }
48
- }
49
- };
50
- const esES = {
51
- code: "es",
52
- name: "Español",
53
- translations: {
54
- commands: {
55
- zoom: {
56
- in: "Acercar",
57
- out: "Alejar",
58
- fitWidth: "Ajustar al ancho",
59
- fitPage: "Ajustar a la página",
60
- automatic: "Automático",
61
- level: "Nivel de zoom ({level}%)",
62
- inArea: "Acercar área"
63
- },
64
- fullscreen: {
65
- enter: "Pantalla completa",
66
- exit: "Salir de pantalla completa"
67
- },
68
- rotate: {
69
- clockwise: "Girar a la derecha",
70
- counterclockwise: "Girar a la izquierda"
71
- },
72
- menu: "Menú",
73
- sidebar: "Barra lateral",
74
- search: "Buscar",
75
- comment: "Comentario",
76
- download: "Descargar",
77
- print: "Imprimir",
78
- openFile: "Abrir PDF",
79
- save: "Guardar",
80
- settings: "Configuración",
81
- view: "Ver",
82
- annotate: "Anotar",
83
- shapes: "Formas",
84
- redact: "Redactar",
85
- fillAndSign: "Rellenar y firmar",
86
- form: "Formulario",
87
- pan: "Desplazar",
88
- pointer: "Puntero",
89
- undo: "Deshacer",
90
- redo: "Rehacer",
91
- copy: "Copiar",
92
- screenshot: "Captura de pantalla",
93
- nextPage: "Página siguiente",
94
- previousPage: "Página anterior"
95
- }
96
- }
97
- };
98
- const I18N_PLUGIN_ID = "i18n";
99
- const manifest = {
100
- id: I18N_PLUGIN_ID,
101
- name: "I18n Plugin",
102
- version: "1.0.0",
103
- provides: ["i18n"],
104
- requires: [],
105
- optional: [],
106
- defaultConfig: {
107
- defaultLocale: "en",
108
- locales: [enUS, esES]
109
- }
110
- };
111
- const SET_LOCALE = "I18N/SET_LOCALE";
112
- const REGISTER_LOCALE = "I18N/REGISTER_LOCALE";
113
- const setLocale = (locale) => ({
114
- type: SET_LOCALE,
115
- payload: locale
1
+ import { createCapabilityToken, definePlugin } from "@embedpdf/core";
2
+ //#region src/translate.ts
3
+ /**
4
+ * The pure lookup core — state in, string out. No platform access anywhere
5
+ * (`Intl.PluralRules` is core ECMAScript, not DOM); this is the part that
6
+ * ports to Rust verbatim.
7
+ */
8
+ /** Walk a dotted path ('commands.zoom.in') through a dictionary tree. */
9
+ function lookup(dict, key) {
10
+ if (!dict) return void 0;
11
+ let node = dict;
12
+ for (const part of key.split(".")) {
13
+ if (node === void 0 || typeof node === "string") return void 0;
14
+ node = node[part];
15
+ }
16
+ return node;
17
+ }
18
+ /** CLDR plural category for a count, with a safe fallback when the runtime
19
+ * lacks ICU data for the locale. */
20
+ function pluralCategory(locale, count) {
21
+ try {
22
+ return new Intl.PluralRules(locale).select(count);
23
+ } catch {
24
+ return count === 1 ? "one" : "other";
25
+ }
26
+ }
27
+ /** A leaf hit is a string; a branch object needs a plural pick via `count`. */
28
+ function resolveNode(node, locale, params) {
29
+ if (typeof node === "string") return node;
30
+ if (node !== void 0 && typeof params?.count === "number") {
31
+ const branch = node[pluralCategory(locale, params.count)] ?? node["other"];
32
+ if (typeof branch === "string") return branch;
33
+ }
34
+ }
35
+ /** Replace `{slot}` markers; unknown slots stay verbatim (visible in QA). */
36
+ function interpolate(text, params) {
37
+ if (!params) return text;
38
+ return text.replace(/\{(\w+)\}/g, (match, slot) => params[slot] !== void 0 ? String(params[slot]) : match);
39
+ }
40
+ /**
41
+ * Resolve a key against the state: current locale → fallback locale →
42
+ * `options.fallback` (interpolated too) → the key itself.
43
+ */
44
+ function translate(state, key, options) {
45
+ const { locale, fallbackLocale, locales } = state;
46
+ const text = resolveNode(lookup(locales[locale]?.translations, key), locale, options?.params) ?? resolveNode(lookup(locales[fallbackLocale]?.translations, key), fallbackLocale, options?.params);
47
+ if (text !== void 0) return {
48
+ text: interpolate(text, options?.params),
49
+ found: true
50
+ };
51
+ if (options?.fallback !== void 0) return {
52
+ text: interpolate(options.fallback, options.params),
53
+ found: false
54
+ };
55
+ return {
56
+ text: key,
57
+ found: false
58
+ };
59
+ }
60
+ //#endregion
61
+ //#region src/capability.ts
62
+ /**
63
+ * The i18n capability — selectors (pure reads over state) + intents (the only
64
+ * writers). Built synchronously in `createKernel()`, so `t()` works before the
65
+ * engine exists — including inside the viewer shell's loading UI.
66
+ */
67
+ function createI18nCapability(ctx, config) {
68
+ const warned = /* @__PURE__ */ new Set();
69
+ const warnOnce = (id, message) => {
70
+ if (warned.has(id)) return;
71
+ warned.add(id);
72
+ console.warn(message);
73
+ };
74
+ return {
75
+ t: (key, options) => {
76
+ const result = translate(ctx.getState(), key, options);
77
+ if (!result.found && options?.fallback === void 0) warnOnce(`key:${key}`, `[i18n] missing translation "${key}" (locale: ${ctx.getState().locale})`);
78
+ return result.text;
79
+ },
80
+ locale: () => ctx.getState().locale,
81
+ dir: () => {
82
+ const { locales, locale } = ctx.getState();
83
+ return locales[locale]?.dir ?? "ltr";
84
+ },
85
+ locales: () => {
86
+ const { locales } = ctx.getState();
87
+ const known = Object.values(locales).map((locale) => ({
88
+ code: locale.code,
89
+ name: locale.name,
90
+ dir: locale.dir ?? "ltr",
91
+ loaded: true
92
+ }));
93
+ for (const code of Object.keys(config.loaders ?? {})) if (!locales[code]) known.push({
94
+ code,
95
+ name: code,
96
+ dir: "ltr",
97
+ loaded: false
98
+ });
99
+ return known;
100
+ },
101
+ loading: () => ctx.getState().loading,
102
+ setLocale: (code) => {
103
+ if (ctx.getState().locales[code]) ctx.dispatch({
104
+ type: "I18N/SET_LOCALE",
105
+ locale: code
106
+ });
107
+ else if (config.loaders?.[code]) ctx.dispatch({
108
+ type: "I18N/LOAD_STARTED",
109
+ locale: code
110
+ });
111
+ else warnOnce(`locale:${code}`, `[i18n] unknown locale "${code}" — not registered and no loader configured`);
112
+ },
113
+ registerLocale: (locale) => ctx.dispatch({
114
+ type: "I18N/REGISTER_LOCALE",
115
+ locale
116
+ })
117
+ };
118
+ }
119
+ //#endregion
120
+ //#region src/effects.ts
121
+ /**
122
+ * The plugin's ONE side-effect: fetching lazy locale packs. State-driven — a
123
+ * load is `state.loading`, set by the capability (a switch to a lazy pack) or
124
+ * by the initial state (a lazy startup locale), so a load requested before
125
+ * effects ran is picked up here and nothing is lost to boot ordering.
126
+ */
127
+ function registerI18nEffects(config) {
128
+ return (ctx) => {
129
+ const load = (code) => {
130
+ const loader = config.loaders?.[code];
131
+ if (!loader) return;
132
+ loader().then((locale) => {
133
+ ctx.dispatch({
134
+ type: "I18N/REGISTER_LOCALE",
135
+ locale
136
+ });
137
+ if (ctx.getState().loading === code) ctx.dispatch({
138
+ type: "I18N/SET_LOCALE",
139
+ locale: locale.code
140
+ });
141
+ }, () => ctx.dispatch({
142
+ type: "I18N/LOAD_FAILED",
143
+ locale: code
144
+ }));
145
+ };
146
+ const pending = ctx.getState().loading;
147
+ if (pending) load(pending);
148
+ ctx.watch(() => ctx.getState().loading, (code) => {
149
+ if (code) load(code);
150
+ });
151
+ };
152
+ }
153
+ //#endregion
154
+ //#region src/reducer.ts
155
+ /**
156
+ * Pure and serializable — locale packs are data, so they live HERE, not in a
157
+ * side-table. One source of truth: `t()` is a pure function of this state and
158
+ * reactivity rides the kernel's one change stream (no emitters).
159
+ */
160
+ function initialI18nState(config) {
161
+ const fallbackLocale = config.fallbackLocale ?? "en";
162
+ const locales = {};
163
+ for (const locale of config.locales ?? []) locales[locale.code] = locale;
164
+ const locale = config.locale ?? fallbackLocale;
165
+ return {
166
+ locale,
167
+ fallbackLocale,
168
+ locales,
169
+ loading: !locales[locale] && config.loaders?.[locale] !== void 0 ? locale : null
170
+ };
171
+ }
172
+ function i18nReducer(state, action) {
173
+ switch (action.type) {
174
+ case "I18N/SET_LOCALE":
175
+ if (!state.locales[action.locale]) return state;
176
+ if (state.locale === action.locale && state.loading === null) return state;
177
+ return {
178
+ ...state,
179
+ locale: action.locale,
180
+ loading: null
181
+ };
182
+ case "I18N/REGISTER_LOCALE": return {
183
+ ...state,
184
+ locales: {
185
+ ...state.locales,
186
+ [action.locale.code]: action.locale
187
+ }
188
+ };
189
+ case "I18N/LOAD_STARTED": return state.loading === action.locale ? state : {
190
+ ...state,
191
+ loading: action.locale
192
+ };
193
+ case "I18N/LOAD_FAILED": return state.loading === action.locale ? {
194
+ ...state,
195
+ loading: null
196
+ } : state;
197
+ default: return state;
198
+ }
199
+ }
200
+ //#endregion
201
+ //#region src/types.ts
202
+ const I18nToken = createCapabilityToken("i18n");
203
+ //#endregion
204
+ //#region src/i18n.plugin.ts
205
+ /**
206
+ * The i18n plugin: workspace-scoped (locale is a workspace concern) with NO
207
+ * dependencies — engine-free, DOM-free. Its capability is built synchronously
208
+ * in `createKernel()`, so the shell translates from the first frame, while
209
+ * the engine is still booting.
210
+ */
211
+ const i18nPlugin = (config = {}) => definePlugin({
212
+ id: "i18n",
213
+ scope: "workspace",
214
+ token: I18nToken,
215
+ initialState: () => initialI18nState(config),
216
+ reduce: i18nReducer,
217
+ capability: (ctx) => createI18nCapability(ctx, config),
218
+ effects: registerI18nEffects(config)
116
219
  });
117
- const registerLocale = (locale) => ({
118
- type: REGISTER_LOCALE,
119
- payload: locale
120
- });
121
- const _I18nPlugin = class _I18nPlugin extends BasePlugin {
122
- constructor(id, registry, config) {
123
- super(id, registry);
124
- this.locales = /* @__PURE__ */ new Map();
125
- this.paramResolvers = /* @__PURE__ */ new Map();
126
- this.paramsCache = /* @__PURE__ */ new Map();
127
- this.localeChange$ = createEmitter();
128
- this.paramsChanged$ = createScopedEmitter((documentId, data) => ({ documentId, ...data }), { cache: false });
129
- this.config = config;
130
- config.locales.forEach((locale) => {
131
- this.locales.set(locale.code, locale);
132
- this.dispatch(registerLocale(locale.code));
133
- });
134
- if (config.paramResolvers) {
135
- Object.entries(config.paramResolvers).forEach(([key, resolver]) => {
136
- this.paramResolvers.set(key, resolver);
137
- });
138
- }
139
- this.dispatch(setLocale(config.defaultLocale));
140
- this.registry.getStore().subscribe((_action, newState) => {
141
- this.detectParamChanges(newState);
142
- });
143
- }
144
- async initialize() {
145
- this.logger.info("I18nPlugin", "Initialize", "I18n plugin initialized");
146
- }
147
- async destroy() {
148
- this.localeChange$.clear();
149
- this.paramsChanged$.clear();
150
- this.paramResolvers.clear();
151
- this.paramsCache.clear();
152
- super.destroy();
153
- }
154
- onDocumentClosed(documentId) {
155
- this.paramsCache.delete(documentId);
156
- this.paramsChanged$.clearScope(documentId);
157
- this.logger.debug(
158
- "I18nPlugin",
159
- "DocumentClosed",
160
- `Cleaned up params cache for document: ${documentId}`
161
- );
162
- }
163
- buildCapability() {
164
- return {
165
- t: (key, options) => this.translate(key, options),
166
- forDocument: (documentId) => this.createI18nScope(documentId),
167
- registerParamResolver: (key, resolver) => this.registerParamResolver(key, resolver),
168
- unregisterParamResolver: (key) => this.unregisterParamResolver(key),
169
- setLocale: (locale) => this.setLocale(locale),
170
- getLocale: () => this.state.currentLocale,
171
- getAvailableLocales: () => [...this.state.availableLocales],
172
- getLocaleInfo: (code) => this.locales.get(code) ?? null,
173
- registerLocale: (locale) => this.registerLocale(locale),
174
- hasLocale: (code) => this.locales.has(code),
175
- onLocaleChange: this.localeChange$.on,
176
- onParamsChanged: this.paramsChanged$.onGlobal
177
- };
178
- }
179
- // ─────────────────────────────────────────────────────────
180
- // Document Scoping
181
- // ─────────────────────────────────────────────────────────
182
- createI18nScope(documentId) {
183
- return {
184
- t: (key, options) => this.translate(key, { documentId, ...options }),
185
- onParamsChanged: this.paramsChanged$.forScope(documentId)
186
- };
187
- }
188
- // ─────────────────────────────────────────────────────────
189
- // Translation Logic
190
- // ─────────────────────────────────────────────────────────
191
- translate(key, options) {
192
- const locale = this.locales.get(this.state.currentLocale);
193
- const fallbackLocale = this.config.fallbackLocale ? this.locales.get(this.config.fallbackLocale) : null;
194
- let value = this.getNestedValue(locale == null ? void 0 : locale.translations, key);
195
- if (!value && fallbackLocale) {
196
- value = this.getNestedValue(fallbackLocale.translations, key);
197
- }
198
- if (!value) {
199
- if (options == null ? void 0 : options.fallback) {
200
- value = options.fallback;
201
- } else {
202
- this.logger.warn(
203
- "I18nPlugin",
204
- "MissingTranslation",
205
- `Translation not found for key: ${key}`
206
- );
207
- return key;
208
- }
209
- }
210
- let params = options == null ? void 0 : options.params;
211
- if (!params && this.paramResolvers.has(key)) {
212
- params = this.resolveParams(key, options == null ? void 0 : options.documentId);
213
- }
214
- return this.interpolate(value, params);
215
- }
216
- resolveParams(key, documentId) {
217
- const resolver = this.paramResolvers.get(key);
218
- if (!resolver) return void 0;
219
- const state = this.registry.getStore().getState();
220
- try {
221
- return resolver({ state, documentId });
222
- } catch (error) {
223
- this.logger.error(
224
- "I18nPlugin",
225
- "ParamResolverError",
226
- `Error resolving params for key "${key}":`,
227
- error
228
- );
229
- return void 0;
230
- }
231
- }
232
- getNestedValue(obj, path) {
233
- if (!obj) return void 0;
234
- const parts = path.split(".");
235
- let current = obj;
236
- for (const part of parts) {
237
- if (current === void 0 || current === null) return void 0;
238
- current = current[part];
239
- }
240
- return typeof current === "string" ? current : void 0;
241
- }
242
- interpolate(str, params) {
243
- if (!params) return str;
244
- return str.replace(/\{(\w+)\}/g, (match, key) => {
245
- const value = params[key];
246
- return value !== void 0 ? String(value) : match;
247
- });
248
- }
249
- // ─────────────────────────────────────────────────────────
250
- // Smart Param Change Detection
251
- // ─────────────────────────────────────────────────────────
252
- detectParamChanges(newState) {
253
- const documentIds = Object.keys(newState.core.documents);
254
- documentIds.forEach((documentId) => {
255
- this.detectDocumentParamChanges(documentId, newState);
256
- });
257
- }
258
- detectDocumentParamChanges(documentId, newState) {
259
- const previousCache = this.paramsCache.get(documentId);
260
- const changedKeys = [];
261
- this.paramResolvers.forEach((resolver, key) => {
262
- try {
263
- const newParams = resolver({ state: newState, documentId });
264
- const prevParams = previousCache == null ? void 0 : previousCache.get(key);
265
- if (!arePropsEqual(prevParams, newParams)) {
266
- changedKeys.push(key);
267
- if (!this.paramsCache.has(documentId)) {
268
- this.paramsCache.set(documentId, /* @__PURE__ */ new Map());
269
- }
270
- this.paramsCache.get(documentId).set(key, newParams);
271
- }
272
- } catch (error) {
273
- this.logger.error(
274
- "I18nPlugin",
275
- "ParamDetectionError",
276
- `Error detecting param changes for key "${key}":`,
277
- error
278
- );
279
- }
280
- });
281
- if (changedKeys.length > 0) {
282
- this.paramsChanged$.emit(documentId, { changedKeys });
283
- this.logger.debug(
284
- "I18nPlugin",
285
- "ParamsChanged",
286
- `Translation params changed for document ${documentId}:`,
287
- changedKeys
288
- );
289
- }
290
- }
291
- // ─────────────────────────────────────────────────────────
292
- // Param Resolver Management
293
- // ─────────────────────────────────────────────────────────
294
- registerParamResolver(key, resolver) {
295
- if (this.paramResolvers.has(key)) {
296
- this.logger.warn(
297
- "I18nPlugin",
298
- "ResolverOverwrite",
299
- `Param resolver for "${key}" already exists and will be overwritten`
300
- );
301
- }
302
- this.paramResolvers.set(key, resolver);
303
- this.paramsCache.forEach((docCache) => {
304
- docCache.delete(key);
305
- });
306
- this.logger.debug("I18nPlugin", "ResolverRegistered", `Param resolver registered for: ${key}`);
307
- }
308
- unregisterParamResolver(key) {
309
- if (this.paramResolvers.delete(key)) {
310
- this.paramsCache.forEach((docCache) => {
311
- docCache.delete(key);
312
- });
313
- this.logger.debug(
314
- "I18nPlugin",
315
- "ResolverUnregistered",
316
- `Param resolver unregistered for: ${key}`
317
- );
318
- }
319
- }
320
- // ─────────────────────────────────────────────────────────
321
- // Locale Management
322
- // ─────────────────────────────────────────────────────────
323
- setLocale(locale) {
324
- if (!this.locales.has(locale)) {
325
- this.logger.warn("I18nPlugin", "LocaleNotFound", `Locale '${locale}' is not registered`);
326
- return;
327
- }
328
- const previousLocale = this.state.currentLocale;
329
- if (previousLocale === locale) return;
330
- this.dispatch(setLocale(locale));
331
- this.localeChange$.emit({
332
- previousLocale,
333
- currentLocale: locale
334
- });
335
- this.logger.info("I18nPlugin", "LocaleChanged", `Locale changed to: ${locale}`);
336
- }
337
- registerLocale(locale) {
338
- if (this.locales.has(locale.code)) {
339
- this.logger.warn(
340
- "I18nPlugin",
341
- "LocaleAlreadyRegistered",
342
- `Locale '${locale.code}' is already registered`
343
- );
344
- return;
345
- }
346
- this.locales.set(locale.code, locale);
347
- this.dispatch(registerLocale(locale.code));
348
- this.logger.info("I18nPlugin", "LocaleRegistered", `Locale registered: ${locale.code}`);
349
- }
350
- };
351
- _I18nPlugin.id = "i18n";
352
- let I18nPlugin = _I18nPlugin;
353
- const initialState = {
354
- currentLocale: "en",
355
- availableLocales: []
356
- };
357
- const i18nReducer = (state = initialState, action) => {
358
- switch (action.type) {
359
- case SET_LOCALE: {
360
- const locale = action.payload;
361
- if (!state.availableLocales.includes(locale)) {
362
- console.warn(`I18nPlugin: Locale '${locale}' not available`);
363
- return state;
364
- }
365
- return {
366
- ...state,
367
- currentLocale: locale
368
- };
369
- }
370
- case REGISTER_LOCALE: {
371
- const locale = action.payload;
372
- if (state.availableLocales.includes(locale)) {
373
- return state;
374
- }
375
- return {
376
- ...state,
377
- availableLocales: [...state.availableLocales, locale]
378
- };
379
- }
380
- default:
381
- return state;
382
- }
383
- };
384
- const I18nPluginPackage = {
385
- manifest,
386
- create: (registry, config) => new I18nPlugin(I18N_PLUGIN_ID, registry, config),
387
- reducer: i18nReducer,
388
- initialState
389
- };
390
- export {
391
- I18N_PLUGIN_ID,
392
- I18nPlugin,
393
- I18nPluginPackage,
394
- enUS,
395
- esES,
396
- manifest
397
- };
398
- //# sourceMappingURL=index.js.map
220
+ //#endregion
221
+ //#region src/negotiate.ts
222
+ /**
223
+ * Pure BCP-47 locale negotiation — strings in, string out. The plugin never
224
+ * reads the platform; the EMBEDDER feeds it whatever request list its
225
+ * environment offers:
226
+ *
227
+ * ```ts
228
+ * // browser shell: negotiateLocale(['en', 'es', 'ar'], navigator.languages)
229
+ * // server render: negotiateLocale(codes, parseAcceptLanguage(header))
230
+ * ```
231
+ *
232
+ * Matching, per requested code in preference order:
233
+ * 1. exact match (case-insensitive): 'es-MX' → 'es-MX'
234
+ * 2. request narrowed to its language: 'en-GB' → 'en'
235
+ * 3. any available dialect of the language: 'zh' → 'zh-Hans'
236
+ *
237
+ * Returns the matched code exactly as it appears in `available`, or null.
238
+ */
239
+ function negotiateLocale(available, requested) {
240
+ const byCanon = /* @__PURE__ */ new Map();
241
+ for (const code of available) {
242
+ const canon = code.toLowerCase();
243
+ if (!byCanon.has(canon)) byCanon.set(canon, code);
244
+ }
245
+ const languageOf = (code) => code.toLowerCase().split("-")[0];
246
+ for (const want of requested) {
247
+ const exact = byCanon.get(want.toLowerCase());
248
+ if (exact !== void 0) return exact;
249
+ }
250
+ for (const want of requested) {
251
+ const narrowed = byCanon.get(languageOf(want));
252
+ if (narrowed !== void 0) return narrowed;
253
+ }
254
+ for (const want of requested) {
255
+ const language = languageOf(want);
256
+ for (const [canon, original] of byCanon) if (languageOf(canon) === language) return original;
257
+ }
258
+ return null;
259
+ }
260
+ //#endregion
261
+ export { I18nToken, i18nPlugin, interpolate, negotiateLocale, translate };
262
+
263
+ //# sourceMappingURL=index.js.map