@embedpdf/plugin-i18n 3.0.0-next.10 → 3.0.0-next.12
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.
- package/dist/contract-BIk4Kv-7.cjs +11 -0
- package/dist/contract-BIk4Kv-7.cjs.map +1 -0
- package/dist/contract-D08iYq9-.js +7 -0
- package/dist/contract-D08iYq9-.js.map +1 -0
- package/dist/contract-DCt9Ikqu.d.cts +116 -0
- package/dist/contract-DCt9Ikqu.d.ts +116 -0
- package/dist/contract.cjs +3 -0
- package/dist/contract.d.cts +2 -0
- package/dist/contract.d.ts +2 -0
- package/dist/contract.js +2 -0
- package/dist/index.cjs +3 -5
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -114
- package/dist/index.d.ts +1 -114
- package/dist/index.js +2 -4
- package/dist/index.js.map +1 -1
- package/package.json +6 -2
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
//#region src/types.ts
|
|
2
|
+
const I18nToken = (0, require("@embedpdf/core").createCapabilityToken)("i18n");
|
|
3
|
+
//#endregion
|
|
4
|
+
Object.defineProperty(exports, "I18nToken", {
|
|
5
|
+
enumerable: true,
|
|
6
|
+
get: function() {
|
|
7
|
+
return I18nToken;
|
|
8
|
+
}
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
//# sourceMappingURL=contract-BIk4Kv-7.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"contract-BIk4Kv-7.cjs","names":[],"sources":["../src/types.ts"],"sourcesContent":["import { createCapabilityToken } from '@embedpdf/core';\n\n/**\n * @embedpdf/plugin-i18n — the contract.\n *\n * Translation is pure data over pure state: locale packs live IN the store\n * (not in a side-table), so `t()` is a pure read, every consumer is reactive\n * through the one kernel change stream, and the whole thing serializes (SSR,\n * snapshots, persist). Workspace-scoped, `requires: []`, never touches the\n * engine or the DOM — the capability is alive from `createKernel()`, before\n * a single WASM byte is fetched. The plugin ships NO strings: mechanism here,\n * content (locale packs) in the product that embeds it.\n */\n\n/** Nested tree of translation strings. Leaves interpolate `{param}` slots. */\nexport interface TranslationDictionary {\n readonly [key: string]: string | TranslationDictionary;\n}\n\nexport interface Locale {\n /** BCP-47 code: 'en', 'es', 'ar', 'zh-Hans'. */\n readonly code: string;\n /** Native display name: 'Español' — what a locale switcher shows. */\n readonly name: string;\n /** Text direction for viewer chrome. Defaults to 'ltr'. */\n readonly dir?: 'ltr' | 'rtl';\n readonly translations: TranslationDictionary;\n}\n\nexport interface I18nState {\n readonly locale: string;\n readonly fallbackLocale: string;\n /** Registered packs by code — the lookup table `t()` reads. */\n readonly locales: Readonly<Record<string, Locale>>;\n /** Code of a lazy pack currently being fetched (drives switcher spinners). */\n readonly loading: string | null;\n}\n\nexport type I18nAction =\n /** Switch to a REGISTERED locale. The reducer ignores unregistered codes —\n * loading a lazy pack goes through LOAD_STARTED (the capability routes). */\n | { type: 'I18N/SET_LOCALE'; locale: string }\n | { type: 'I18N/REGISTER_LOCALE'; locale: Locale }\n | { type: 'I18N/LOAD_STARTED'; locale: string }\n | { type: 'I18N/LOAD_FAILED'; locale: string };\n\nexport interface I18nConfig {\n /**\n * Startup locale. Compute platform inputs OUTSIDE the plugin — it never\n * touches the DOM:\n *\n * ```ts\n * i18nPlugin({ locale: negotiateLocale(codes, navigator.languages) ?? 'en' })\n * ```\n *\n * May name a `loaders` pack: the fallback locale shows until the pack\n * arrives (effects fetch it at startup). Defaults to `fallbackLocale`.\n */\n locale?: string;\n /** The pack tried when a key misses the current locale. Default 'en'. */\n fallbackLocale?: string;\n /** Eagerly available packs. */\n locales?: Locale[];\n /**\n * Lazy packs: code → loader. `setLocale(code)` fetches on demand (in\n * effects), registers the pack, then switches. Until a lazy pack loads, a\n * locale switcher shows its code as the name — register eagerly (packs are\n * small) when you want native names in the switcher up front.\n */\n loaders?: Record<string, () => Promise<Locale>>;\n}\n\nexport interface TranslateOptions {\n /**\n * `{slot}` interpolation values. When `count` is a number and the key\n * resolves to a branch object, the branch is picked by CLDR plural\n * category (`Intl.PluralRules`), falling back to `other`:\n *\n * ```ts\n * // pages: { one: '{count} page', other: '{count} pages' }\n * t('pages', { params: { count: 1 } }) // '1 page'\n * ```\n */\n params?: Record<string, string | number>;\n /** Returned (interpolated) when the key misses every pack — instead of the key. */\n fallback?: string;\n}\n\n/** A locale as a switcher sees it — registered packs plus not-yet-loaded lazy ones. */\nexport interface LocaleInfo {\n readonly code: string;\n /** Native name; the bare code until a lazy pack has loaded. */\n readonly name: string;\n readonly dir: 'ltr' | 'rtl';\n readonly loaded: boolean;\n}\n\nexport interface I18nCapability {\n /** Translate a key: current locale → fallback locale → `options.fallback` → the key. */\n t(key: string, options?: TranslateOptions): string;\n locale(): string;\n /** Text direction of the CURRENT locale — wire to `dir=` on the shell. */\n dir(): 'ltr' | 'rtl';\n /** Every known locale (registered + lazy), in registration order. */\n locales(): LocaleInfo[];\n /** Code of the lazy pack being fetched right now, if any. */\n loading(): string | null;\n /** Switch locale; fetches the pack first when it's a lazy one. */\n setLocale(code: string): void;\n /** Register a pack at runtime (customer-supplied translations). */\n registerLocale(locale: Locale): void;\n}\n\nexport const I18nToken = createCapabilityToken<I18nCapability>('i18n');\n"],"mappings":";AAiHA,MAAa,aAAA,2BAAA,CAAA,CAAA,sBAAA,CAAkD,MAAM"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"contract-D08iYq9-.js","names":[],"sources":["../src/types.ts"],"sourcesContent":["import { createCapabilityToken } from '@embedpdf/core';\n\n/**\n * @embedpdf/plugin-i18n — the contract.\n *\n * Translation is pure data over pure state: locale packs live IN the store\n * (not in a side-table), so `t()` is a pure read, every consumer is reactive\n * through the one kernel change stream, and the whole thing serializes (SSR,\n * snapshots, persist). Workspace-scoped, `requires: []`, never touches the\n * engine or the DOM — the capability is alive from `createKernel()`, before\n * a single WASM byte is fetched. The plugin ships NO strings: mechanism here,\n * content (locale packs) in the product that embeds it.\n */\n\n/** Nested tree of translation strings. Leaves interpolate `{param}` slots. */\nexport interface TranslationDictionary {\n readonly [key: string]: string | TranslationDictionary;\n}\n\nexport interface Locale {\n /** BCP-47 code: 'en', 'es', 'ar', 'zh-Hans'. */\n readonly code: string;\n /** Native display name: 'Español' — what a locale switcher shows. */\n readonly name: string;\n /** Text direction for viewer chrome. Defaults to 'ltr'. */\n readonly dir?: 'ltr' | 'rtl';\n readonly translations: TranslationDictionary;\n}\n\nexport interface I18nState {\n readonly locale: string;\n readonly fallbackLocale: string;\n /** Registered packs by code — the lookup table `t()` reads. */\n readonly locales: Readonly<Record<string, Locale>>;\n /** Code of a lazy pack currently being fetched (drives switcher spinners). */\n readonly loading: string | null;\n}\n\nexport type I18nAction =\n /** Switch to a REGISTERED locale. The reducer ignores unregistered codes —\n * loading a lazy pack goes through LOAD_STARTED (the capability routes). */\n | { type: 'I18N/SET_LOCALE'; locale: string }\n | { type: 'I18N/REGISTER_LOCALE'; locale: Locale }\n | { type: 'I18N/LOAD_STARTED'; locale: string }\n | { type: 'I18N/LOAD_FAILED'; locale: string };\n\nexport interface I18nConfig {\n /**\n * Startup locale. Compute platform inputs OUTSIDE the plugin — it never\n * touches the DOM:\n *\n * ```ts\n * i18nPlugin({ locale: negotiateLocale(codes, navigator.languages) ?? 'en' })\n * ```\n *\n * May name a `loaders` pack: the fallback locale shows until the pack\n * arrives (effects fetch it at startup). Defaults to `fallbackLocale`.\n */\n locale?: string;\n /** The pack tried when a key misses the current locale. Default 'en'. */\n fallbackLocale?: string;\n /** Eagerly available packs. */\n locales?: Locale[];\n /**\n * Lazy packs: code → loader. `setLocale(code)` fetches on demand (in\n * effects), registers the pack, then switches. Until a lazy pack loads, a\n * locale switcher shows its code as the name — register eagerly (packs are\n * small) when you want native names in the switcher up front.\n */\n loaders?: Record<string, () => Promise<Locale>>;\n}\n\nexport interface TranslateOptions {\n /**\n * `{slot}` interpolation values. When `count` is a number and the key\n * resolves to a branch object, the branch is picked by CLDR plural\n * category (`Intl.PluralRules`), falling back to `other`:\n *\n * ```ts\n * // pages: { one: '{count} page', other: '{count} pages' }\n * t('pages', { params: { count: 1 } }) // '1 page'\n * ```\n */\n params?: Record<string, string | number>;\n /** Returned (interpolated) when the key misses every pack — instead of the key. */\n fallback?: string;\n}\n\n/** A locale as a switcher sees it — registered packs plus not-yet-loaded lazy ones. */\nexport interface LocaleInfo {\n readonly code: string;\n /** Native name; the bare code until a lazy pack has loaded. */\n readonly name: string;\n readonly dir: 'ltr' | 'rtl';\n readonly loaded: boolean;\n}\n\nexport interface I18nCapability {\n /** Translate a key: current locale → fallback locale → `options.fallback` → the key. */\n t(key: string, options?: TranslateOptions): string;\n locale(): string;\n /** Text direction of the CURRENT locale — wire to `dir=` on the shell. */\n dir(): 'ltr' | 'rtl';\n /** Every known locale (registered + lazy), in registration order. */\n locales(): LocaleInfo[];\n /** Code of the lazy pack being fetched right now, if any. */\n loading(): string | null;\n /** Switch locale; fetches the pack first when it's a lazy one. */\n setLocale(code: string): void;\n /** Register a pack at runtime (customer-supplied translations). */\n registerLocale(locale: Locale): void;\n}\n\nexport const I18nToken = createCapabilityToken<I18nCapability>('i18n');\n"],"mappings":";;AAiHA,MAAa,YAAY,sBAAsC,MAAM"}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
//#region src/types.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* @embedpdf/plugin-i18n — the contract.
|
|
4
|
+
*
|
|
5
|
+
* Translation is pure data over pure state: locale packs live IN the store
|
|
6
|
+
* (not in a side-table), so `t()` is a pure read, every consumer is reactive
|
|
7
|
+
* through the one kernel change stream, and the whole thing serializes (SSR,
|
|
8
|
+
* snapshots, persist). Workspace-scoped, `requires: []`, never touches the
|
|
9
|
+
* engine or the DOM — the capability is alive from `createKernel()`, before
|
|
10
|
+
* a single WASM byte is fetched. The plugin ships NO strings: mechanism here,
|
|
11
|
+
* content (locale packs) in the product that embeds it.
|
|
12
|
+
*/
|
|
13
|
+
/** Nested tree of translation strings. Leaves interpolate `{param}` slots. */
|
|
14
|
+
interface TranslationDictionary {
|
|
15
|
+
readonly [key: string]: string | TranslationDictionary;
|
|
16
|
+
}
|
|
17
|
+
interface Locale {
|
|
18
|
+
/** BCP-47 code: 'en', 'es', 'ar', 'zh-Hans'. */
|
|
19
|
+
readonly code: string;
|
|
20
|
+
/** Native display name: 'Español' — what a locale switcher shows. */
|
|
21
|
+
readonly name: string;
|
|
22
|
+
/** Text direction for viewer chrome. Defaults to 'ltr'. */
|
|
23
|
+
readonly dir?: 'ltr' | 'rtl';
|
|
24
|
+
readonly translations: TranslationDictionary;
|
|
25
|
+
}
|
|
26
|
+
interface I18nState {
|
|
27
|
+
readonly locale: string;
|
|
28
|
+
readonly fallbackLocale: string;
|
|
29
|
+
/** Registered packs by code — the lookup table `t()` reads. */
|
|
30
|
+
readonly locales: Readonly<Record<string, Locale>>;
|
|
31
|
+
/** Code of a lazy pack currently being fetched (drives switcher spinners). */
|
|
32
|
+
readonly loading: string | null;
|
|
33
|
+
}
|
|
34
|
+
type I18nAction =
|
|
35
|
+
/** Switch to a REGISTERED locale. The reducer ignores unregistered codes —
|
|
36
|
+
* loading a lazy pack goes through LOAD_STARTED (the capability routes). */
|
|
37
|
+
{
|
|
38
|
+
type: 'I18N/SET_LOCALE';
|
|
39
|
+
locale: string;
|
|
40
|
+
} | {
|
|
41
|
+
type: 'I18N/REGISTER_LOCALE';
|
|
42
|
+
locale: Locale;
|
|
43
|
+
} | {
|
|
44
|
+
type: 'I18N/LOAD_STARTED';
|
|
45
|
+
locale: string;
|
|
46
|
+
} | {
|
|
47
|
+
type: 'I18N/LOAD_FAILED';
|
|
48
|
+
locale: string;
|
|
49
|
+
};
|
|
50
|
+
interface I18nConfig {
|
|
51
|
+
/**
|
|
52
|
+
* Startup locale. Compute platform inputs OUTSIDE the plugin — it never
|
|
53
|
+
* touches the DOM:
|
|
54
|
+
*
|
|
55
|
+
* ```ts
|
|
56
|
+
* i18nPlugin({ locale: negotiateLocale(codes, navigator.languages) ?? 'en' })
|
|
57
|
+
* ```
|
|
58
|
+
*
|
|
59
|
+
* May name a `loaders` pack: the fallback locale shows until the pack
|
|
60
|
+
* arrives (effects fetch it at startup). Defaults to `fallbackLocale`.
|
|
61
|
+
*/
|
|
62
|
+
locale?: string;
|
|
63
|
+
/** The pack tried when a key misses the current locale. Default 'en'. */
|
|
64
|
+
fallbackLocale?: string;
|
|
65
|
+
/** Eagerly available packs. */
|
|
66
|
+
locales?: Locale[];
|
|
67
|
+
/**
|
|
68
|
+
* Lazy packs: code → loader. `setLocale(code)` fetches on demand (in
|
|
69
|
+
* effects), registers the pack, then switches. Until a lazy pack loads, a
|
|
70
|
+
* locale switcher shows its code as the name — register eagerly (packs are
|
|
71
|
+
* small) when you want native names in the switcher up front.
|
|
72
|
+
*/
|
|
73
|
+
loaders?: Record<string, () => Promise<Locale>>;
|
|
74
|
+
}
|
|
75
|
+
interface TranslateOptions {
|
|
76
|
+
/**
|
|
77
|
+
* `{slot}` interpolation values. When `count` is a number and the key
|
|
78
|
+
* resolves to a branch object, the branch is picked by CLDR plural
|
|
79
|
+
* category (`Intl.PluralRules`), falling back to `other`:
|
|
80
|
+
*
|
|
81
|
+
* ```ts
|
|
82
|
+
* // pages: { one: '{count} page', other: '{count} pages' }
|
|
83
|
+
* t('pages', { params: { count: 1 } }) // '1 page'
|
|
84
|
+
* ```
|
|
85
|
+
*/
|
|
86
|
+
params?: Record<string, string | number>;
|
|
87
|
+
/** Returned (interpolated) when the key misses every pack — instead of the key. */
|
|
88
|
+
fallback?: string;
|
|
89
|
+
}
|
|
90
|
+
/** A locale as a switcher sees it — registered packs plus not-yet-loaded lazy ones. */
|
|
91
|
+
interface LocaleInfo {
|
|
92
|
+
readonly code: string;
|
|
93
|
+
/** Native name; the bare code until a lazy pack has loaded. */
|
|
94
|
+
readonly name: string;
|
|
95
|
+
readonly dir: 'ltr' | 'rtl';
|
|
96
|
+
readonly loaded: boolean;
|
|
97
|
+
}
|
|
98
|
+
interface I18nCapability {
|
|
99
|
+
/** Translate a key: current locale → fallback locale → `options.fallback` → the key. */
|
|
100
|
+
t(key: string, options?: TranslateOptions): string;
|
|
101
|
+
locale(): string;
|
|
102
|
+
/** Text direction of the CURRENT locale — wire to `dir=` on the shell. */
|
|
103
|
+
dir(): 'ltr' | 'rtl';
|
|
104
|
+
/** Every known locale (registered + lazy), in registration order. */
|
|
105
|
+
locales(): LocaleInfo[];
|
|
106
|
+
/** Code of the lazy pack being fetched right now, if any. */
|
|
107
|
+
loading(): string | null;
|
|
108
|
+
/** Switch locale; fetches the pack first when it's a lazy one. */
|
|
109
|
+
setLocale(code: string): void;
|
|
110
|
+
/** Register a pack at runtime (customer-supplied translations). */
|
|
111
|
+
registerLocale(locale: Locale): void;
|
|
112
|
+
}
|
|
113
|
+
declare const I18nToken: import("@embedpdf/core").CapabilityToken<I18nCapability>;
|
|
114
|
+
//#endregion
|
|
115
|
+
export { I18nToken as a, TranslateOptions as c, I18nState as i, TranslationDictionary as l, I18nCapability as n, Locale as o, I18nConfig as r, LocaleInfo as s, I18nAction as t };
|
|
116
|
+
//# sourceMappingURL=contract-DCt9Ikqu.d.cts.map
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
//#region src/types.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* @embedpdf/plugin-i18n — the contract.
|
|
4
|
+
*
|
|
5
|
+
* Translation is pure data over pure state: locale packs live IN the store
|
|
6
|
+
* (not in a side-table), so `t()` is a pure read, every consumer is reactive
|
|
7
|
+
* through the one kernel change stream, and the whole thing serializes (SSR,
|
|
8
|
+
* snapshots, persist). Workspace-scoped, `requires: []`, never touches the
|
|
9
|
+
* engine or the DOM — the capability is alive from `createKernel()`, before
|
|
10
|
+
* a single WASM byte is fetched. The plugin ships NO strings: mechanism here,
|
|
11
|
+
* content (locale packs) in the product that embeds it.
|
|
12
|
+
*/
|
|
13
|
+
/** Nested tree of translation strings. Leaves interpolate `{param}` slots. */
|
|
14
|
+
interface TranslationDictionary {
|
|
15
|
+
readonly [key: string]: string | TranslationDictionary;
|
|
16
|
+
}
|
|
17
|
+
interface Locale {
|
|
18
|
+
/** BCP-47 code: 'en', 'es', 'ar', 'zh-Hans'. */
|
|
19
|
+
readonly code: string;
|
|
20
|
+
/** Native display name: 'Español' — what a locale switcher shows. */
|
|
21
|
+
readonly name: string;
|
|
22
|
+
/** Text direction for viewer chrome. Defaults to 'ltr'. */
|
|
23
|
+
readonly dir?: 'ltr' | 'rtl';
|
|
24
|
+
readonly translations: TranslationDictionary;
|
|
25
|
+
}
|
|
26
|
+
interface I18nState {
|
|
27
|
+
readonly locale: string;
|
|
28
|
+
readonly fallbackLocale: string;
|
|
29
|
+
/** Registered packs by code — the lookup table `t()` reads. */
|
|
30
|
+
readonly locales: Readonly<Record<string, Locale>>;
|
|
31
|
+
/** Code of a lazy pack currently being fetched (drives switcher spinners). */
|
|
32
|
+
readonly loading: string | null;
|
|
33
|
+
}
|
|
34
|
+
type I18nAction =
|
|
35
|
+
/** Switch to a REGISTERED locale. The reducer ignores unregistered codes —
|
|
36
|
+
* loading a lazy pack goes through LOAD_STARTED (the capability routes). */
|
|
37
|
+
{
|
|
38
|
+
type: 'I18N/SET_LOCALE';
|
|
39
|
+
locale: string;
|
|
40
|
+
} | {
|
|
41
|
+
type: 'I18N/REGISTER_LOCALE';
|
|
42
|
+
locale: Locale;
|
|
43
|
+
} | {
|
|
44
|
+
type: 'I18N/LOAD_STARTED';
|
|
45
|
+
locale: string;
|
|
46
|
+
} | {
|
|
47
|
+
type: 'I18N/LOAD_FAILED';
|
|
48
|
+
locale: string;
|
|
49
|
+
};
|
|
50
|
+
interface I18nConfig {
|
|
51
|
+
/**
|
|
52
|
+
* Startup locale. Compute platform inputs OUTSIDE the plugin — it never
|
|
53
|
+
* touches the DOM:
|
|
54
|
+
*
|
|
55
|
+
* ```ts
|
|
56
|
+
* i18nPlugin({ locale: negotiateLocale(codes, navigator.languages) ?? 'en' })
|
|
57
|
+
* ```
|
|
58
|
+
*
|
|
59
|
+
* May name a `loaders` pack: the fallback locale shows until the pack
|
|
60
|
+
* arrives (effects fetch it at startup). Defaults to `fallbackLocale`.
|
|
61
|
+
*/
|
|
62
|
+
locale?: string;
|
|
63
|
+
/** The pack tried when a key misses the current locale. Default 'en'. */
|
|
64
|
+
fallbackLocale?: string;
|
|
65
|
+
/** Eagerly available packs. */
|
|
66
|
+
locales?: Locale[];
|
|
67
|
+
/**
|
|
68
|
+
* Lazy packs: code → loader. `setLocale(code)` fetches on demand (in
|
|
69
|
+
* effects), registers the pack, then switches. Until a lazy pack loads, a
|
|
70
|
+
* locale switcher shows its code as the name — register eagerly (packs are
|
|
71
|
+
* small) when you want native names in the switcher up front.
|
|
72
|
+
*/
|
|
73
|
+
loaders?: Record<string, () => Promise<Locale>>;
|
|
74
|
+
}
|
|
75
|
+
interface TranslateOptions {
|
|
76
|
+
/**
|
|
77
|
+
* `{slot}` interpolation values. When `count` is a number and the key
|
|
78
|
+
* resolves to a branch object, the branch is picked by CLDR plural
|
|
79
|
+
* category (`Intl.PluralRules`), falling back to `other`:
|
|
80
|
+
*
|
|
81
|
+
* ```ts
|
|
82
|
+
* // pages: { one: '{count} page', other: '{count} pages' }
|
|
83
|
+
* t('pages', { params: { count: 1 } }) // '1 page'
|
|
84
|
+
* ```
|
|
85
|
+
*/
|
|
86
|
+
params?: Record<string, string | number>;
|
|
87
|
+
/** Returned (interpolated) when the key misses every pack — instead of the key. */
|
|
88
|
+
fallback?: string;
|
|
89
|
+
}
|
|
90
|
+
/** A locale as a switcher sees it — registered packs plus not-yet-loaded lazy ones. */
|
|
91
|
+
interface LocaleInfo {
|
|
92
|
+
readonly code: string;
|
|
93
|
+
/** Native name; the bare code until a lazy pack has loaded. */
|
|
94
|
+
readonly name: string;
|
|
95
|
+
readonly dir: 'ltr' | 'rtl';
|
|
96
|
+
readonly loaded: boolean;
|
|
97
|
+
}
|
|
98
|
+
interface I18nCapability {
|
|
99
|
+
/** Translate a key: current locale → fallback locale → `options.fallback` → the key. */
|
|
100
|
+
t(key: string, options?: TranslateOptions): string;
|
|
101
|
+
locale(): string;
|
|
102
|
+
/** Text direction of the CURRENT locale — wire to `dir=` on the shell. */
|
|
103
|
+
dir(): 'ltr' | 'rtl';
|
|
104
|
+
/** Every known locale (registered + lazy), in registration order. */
|
|
105
|
+
locales(): LocaleInfo[];
|
|
106
|
+
/** Code of the lazy pack being fetched right now, if any. */
|
|
107
|
+
loading(): string | null;
|
|
108
|
+
/** Switch locale; fetches the pack first when it's a lazy one. */
|
|
109
|
+
setLocale(code: string): void;
|
|
110
|
+
/** Register a pack at runtime (customer-supplied translations). */
|
|
111
|
+
registerLocale(locale: Locale): void;
|
|
112
|
+
}
|
|
113
|
+
declare const I18nToken: import("@embedpdf/core").CapabilityToken<I18nCapability>;
|
|
114
|
+
//#endregion
|
|
115
|
+
export { I18nToken as a, TranslateOptions as c, I18nState as i, TranslationDictionary as l, I18nCapability as n, Locale as o, I18nConfig as r, LocaleInfo as s, I18nAction as t };
|
|
116
|
+
//# sourceMappingURL=contract-DCt9Ikqu.d.ts.map
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import { a as I18nToken, c as TranslateOptions, i as I18nState, l as TranslationDictionary, n as I18nCapability, o as Locale, r as I18nConfig, s as LocaleInfo } from "./contract-DCt9Ikqu.cjs";
|
|
2
|
+
export { type I18nCapability, type I18nConfig, type I18nState, I18nToken, type Locale, type LocaleInfo, type TranslateOptions, type TranslationDictionary };
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import { a as I18nToken, c as TranslateOptions, i as I18nState, l as TranslationDictionary, n as I18nCapability, o as Locale, r as I18nConfig, s as LocaleInfo } from "./contract-DCt9Ikqu.js";
|
|
2
|
+
export { type I18nCapability, type I18nConfig, type I18nState, I18nToken, type Locale, type LocaleInfo, type TranslateOptions, type TranslationDictionary };
|
package/dist/contract.js
ADDED
package/dist/index.cjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
const require_contract = require("./contract-BIk4Kv-7.cjs");
|
|
2
3
|
let _embedpdf_core = require("@embedpdf/core");
|
|
3
4
|
//#region src/translate.ts
|
|
4
5
|
/**
|
|
@@ -199,9 +200,6 @@ function i18nReducer(state, action) {
|
|
|
199
200
|
}
|
|
200
201
|
}
|
|
201
202
|
//#endregion
|
|
202
|
-
//#region src/types.ts
|
|
203
|
-
const I18nToken = (0, _embedpdf_core.createCapabilityToken)("i18n");
|
|
204
|
-
//#endregion
|
|
205
203
|
//#region src/i18n.plugin.ts
|
|
206
204
|
/**
|
|
207
205
|
* The i18n plugin: workspace-scoped (locale is a workspace concern) with NO
|
|
@@ -212,7 +210,7 @@ const I18nToken = (0, _embedpdf_core.createCapabilityToken)("i18n");
|
|
|
212
210
|
const i18nPlugin = (config = {}) => (0, _embedpdf_core.definePlugin)({
|
|
213
211
|
id: "i18n",
|
|
214
212
|
scope: "workspace",
|
|
215
|
-
token: I18nToken,
|
|
213
|
+
token: require_contract.I18nToken,
|
|
216
214
|
initialState: () => initialI18nState(config),
|
|
217
215
|
reduce: i18nReducer,
|
|
218
216
|
capability: (ctx) => createI18nCapability(ctx, config),
|
|
@@ -259,7 +257,7 @@ function negotiateLocale(available, requested) {
|
|
|
259
257
|
return null;
|
|
260
258
|
}
|
|
261
259
|
//#endregion
|
|
262
|
-
exports.I18nToken = I18nToken;
|
|
260
|
+
exports.I18nToken = require_contract.I18nToken;
|
|
263
261
|
exports.i18nPlugin = i18nPlugin;
|
|
264
262
|
exports.interpolate = interpolate;
|
|
265
263
|
exports.negotiateLocale = negotiateLocale;
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs","names":[],"sources":["../src/translate.ts","../src/capability.ts","../src/effects.ts","../src/reducer.ts","../src/types.ts","../src/i18n.plugin.ts","../src/negotiate.ts"],"sourcesContent":["import type { I18nState, TranslateOptions, TranslationDictionary } from './types';\n\n/**\n * The pure lookup core — state in, string out. No platform access anywhere\n * (`Intl.PluralRules` is core ECMAScript, not DOM); this is the part that\n * ports to Rust verbatim.\n */\n\n/** Walk a dotted path ('commands.zoom.in') through a dictionary tree. */\nfunction lookup(\n dict: TranslationDictionary | undefined,\n key: string,\n): string | TranslationDictionary | undefined {\n if (!dict) return undefined;\n let node: string | TranslationDictionary | undefined = dict;\n for (const part of key.split('.')) {\n if (node === undefined || typeof node === 'string') return undefined;\n node = node[part];\n }\n return node;\n}\n\n/** CLDR plural category for a count, with a safe fallback when the runtime\n * lacks ICU data for the locale. */\nfunction pluralCategory(locale: string, count: number): string {\n try {\n return new Intl.PluralRules(locale).select(count);\n } catch {\n return count === 1 ? 'one' : 'other';\n }\n}\n\n/** A leaf hit is a string; a branch object needs a plural pick via `count`. */\nfunction resolveNode(\n node: string | TranslationDictionary | undefined,\n locale: string,\n params?: Record<string, string | number>,\n): string | undefined {\n if (typeof node === 'string') return node;\n if (node !== undefined && typeof params?.count === 'number') {\n const branch = node[pluralCategory(locale, params.count)] ?? node['other'];\n if (typeof branch === 'string') return branch;\n }\n return undefined;\n}\n\n/** Replace `{slot}` markers; unknown slots stay verbatim (visible in QA). */\nexport function interpolate(text: string, params?: Record<string, string | number>): string {\n if (!params) return text;\n return text.replace(/\\{(\\w+)\\}/g, (match, slot) =>\n params[slot] !== undefined ? String(params[slot]) : match,\n );\n}\n\nexport interface TranslateResult {\n readonly text: string;\n /** False when the key missed every pack (text is then the fallback or the key). */\n readonly found: boolean;\n}\n\n/**\n * Resolve a key against the state: current locale → fallback locale →\n * `options.fallback` (interpolated too) → the key itself.\n */\nexport function translate(\n state: I18nState,\n key: string,\n options?: TranslateOptions,\n): TranslateResult {\n const { locale, fallbackLocale, locales } = state;\n const text =\n resolveNode(lookup(locales[locale]?.translations, key), locale, options?.params) ??\n resolveNode(\n lookup(locales[fallbackLocale]?.translations, key),\n fallbackLocale,\n options?.params,\n );\n if (text !== undefined) return { text: interpolate(text, options?.params), found: true };\n if (options?.fallback !== undefined)\n return { text: interpolate(options.fallback, options.params), found: false };\n return { text: key, found: false };\n}\n","import type { PluginContext } from '@embedpdf/core';\nimport { translate } from './translate';\nimport type { I18nAction, I18nCapability, I18nConfig, I18nState, LocaleInfo } from './types';\n\n/**\n * The i18n capability — selectors (pure reads over state) + intents (the only\n * writers). Built synchronously in `createKernel()`, so `t()` works before the\n * engine exists — including inside the viewer shell's loading UI.\n */\nexport function createI18nCapability(\n ctx: PluginContext<I18nState, I18nAction>,\n config: I18nConfig,\n): I18nCapability {\n // Dev signal, once per offender — a missing key otherwise fails silently\n // (by design: `t()` always returns a usable string).\n const warned = new Set<string>();\n const warnOnce = (id: string, message: string) => {\n if (warned.has(id)) return;\n warned.add(id);\n console.warn(message);\n };\n\n return {\n t: (key, options) => {\n const result = translate(ctx.getState(), key, options);\n if (!result.found && options?.fallback === undefined) {\n warnOnce(\n `key:${key}`,\n `[i18n] missing translation \"${key}\" (locale: ${ctx.getState().locale})`,\n );\n }\n return result.text;\n },\n locale: () => ctx.getState().locale,\n dir: () => {\n const { locales, locale } = ctx.getState();\n return locales[locale]?.dir ?? 'ltr';\n },\n locales: () => {\n const { locales } = ctx.getState();\n const known: LocaleInfo[] = Object.values(locales).map((locale) => ({\n code: locale.code,\n name: locale.name,\n dir: locale.dir ?? 'ltr',\n loaded: true,\n }));\n for (const code of Object.keys(config.loaders ?? {})) {\n if (!locales[code]) known.push({ code, name: code, dir: 'ltr', loaded: false });\n }\n return known;\n },\n loading: () => ctx.getState().loading,\n setLocale: (code) => {\n const state = ctx.getState();\n if (state.locales[code]) {\n ctx.dispatch({ type: 'I18N/SET_LOCALE', locale: code });\n } else if (config.loaders?.[code]) {\n ctx.dispatch({ type: 'I18N/LOAD_STARTED', locale: code });\n } else {\n warnOnce(\n `locale:${code}`,\n `[i18n] unknown locale \"${code}\" — not registered and no loader configured`,\n );\n }\n },\n registerLocale: (locale) => ctx.dispatch({ type: 'I18N/REGISTER_LOCALE', locale }),\n };\n}\n","import type { EffectContext } from '@embedpdf/core';\nimport type { I18nAction, I18nConfig, I18nState } from './types';\n\n/**\n * The plugin's ONE side-effect: fetching lazy locale packs. State-driven — a\n * load is `state.loading`, set by the capability (a switch to a lazy pack) or\n * by the initial state (a lazy startup locale), so a load requested before\n * effects ran is picked up here and nothing is lost to boot ordering.\n */\nexport function registerI18nEffects(config: I18nConfig) {\n return (ctx: EffectContext<I18nState, I18nAction>): void => {\n const load = (code: string) => {\n const loader = config.loaders?.[code];\n if (!loader) return; // unreachable via the capability; guards bad dispatches\n loader().then(\n (locale) => {\n ctx.dispatch({ type: 'I18N/REGISTER_LOCALE', locale });\n // Complete the switch only if this load is still the wanted one —\n // the user may have switched again while the pack was in flight.\n if (ctx.getState().loading === code) {\n ctx.dispatch({ type: 'I18N/SET_LOCALE', locale: locale.code });\n }\n },\n () => ctx.dispatch({ type: 'I18N/LOAD_FAILED', locale: code }),\n );\n };\n\n const pending = ctx.getState().loading;\n if (pending) load(pending);\n ctx.watch(\n () => ctx.getState().loading,\n (code) => {\n if (code) load(code);\n },\n );\n };\n}\n","import type { I18nAction, I18nConfig, I18nState, Locale } from './types';\n\n/**\n * Pure and serializable — locale packs are data, so they live HERE, not in a\n * side-table. One source of truth: `t()` is a pure function of this state and\n * reactivity rides the kernel's one change stream (no emitters).\n */\n\nexport function initialI18nState(config: I18nConfig): I18nState {\n const fallbackLocale = config.fallbackLocale ?? 'en';\n const locales: Record<string, Locale> = {};\n for (const locale of config.locales ?? []) locales[locale.code] = locale;\n const locale = config.locale ?? fallbackLocale;\n // A startup locale that is a lazy pack: show the fallback chain until the\n // pack arrives — seeding `loading` makes effects fetch it at startup.\n const needsLoad = !locales[locale] && config.loaders?.[locale] !== undefined;\n return { locale, fallbackLocale, locales, loading: needsLoad ? locale : null };\n}\n\nexport function i18nReducer(state: I18nState, action: I18nAction): I18nState {\n switch (action.type) {\n case 'I18N/SET_LOCALE': {\n // Only registered packs can become current — lazy ones arrive via\n // LOAD_STARTED → REGISTER_LOCALE → SET_LOCALE (effects drive the middle).\n if (!state.locales[action.locale]) return state;\n if (state.locale === action.locale && state.loading === null) return state;\n return { ...state, locale: action.locale, loading: null };\n }\n case 'I18N/REGISTER_LOCALE':\n return { ...state, locales: { ...state.locales, [action.locale.code]: action.locale } };\n case 'I18N/LOAD_STARTED':\n return state.loading === action.locale ? state : { ...state, loading: action.locale };\n case 'I18N/LOAD_FAILED':\n return state.loading === action.locale ? { ...state, loading: null } : state;\n default:\n return state;\n }\n}\n","import { createCapabilityToken } from '@embedpdf/core';\n\n/**\n * @embedpdf/plugin-i18n — the contract.\n *\n * Translation is pure data over pure state: locale packs live IN the store\n * (not in a side-table), so `t()` is a pure read, every consumer is reactive\n * through the one kernel change stream, and the whole thing serializes (SSR,\n * snapshots, persist). Workspace-scoped, `requires: []`, never touches the\n * engine or the DOM — the capability is alive from `createKernel()`, before\n * a single WASM byte is fetched. The plugin ships NO strings: mechanism here,\n * content (locale packs) in the product that embeds it.\n */\n\n/** Nested tree of translation strings. Leaves interpolate `{param}` slots. */\nexport interface TranslationDictionary {\n readonly [key: string]: string | TranslationDictionary;\n}\n\nexport interface Locale {\n /** BCP-47 code: 'en', 'es', 'ar', 'zh-Hans'. */\n readonly code: string;\n /** Native display name: 'Español' — what a locale switcher shows. */\n readonly name: string;\n /** Text direction for viewer chrome. Defaults to 'ltr'. */\n readonly dir?: 'ltr' | 'rtl';\n readonly translations: TranslationDictionary;\n}\n\nexport interface I18nState {\n readonly locale: string;\n readonly fallbackLocale: string;\n /** Registered packs by code — the lookup table `t()` reads. */\n readonly locales: Readonly<Record<string, Locale>>;\n /** Code of a lazy pack currently being fetched (drives switcher spinners). */\n readonly loading: string | null;\n}\n\nexport type I18nAction =\n /** Switch to a REGISTERED locale. The reducer ignores unregistered codes —\n * loading a lazy pack goes through LOAD_STARTED (the capability routes). */\n | { type: 'I18N/SET_LOCALE'; locale: string }\n | { type: 'I18N/REGISTER_LOCALE'; locale: Locale }\n | { type: 'I18N/LOAD_STARTED'; locale: string }\n | { type: 'I18N/LOAD_FAILED'; locale: string };\n\nexport interface I18nConfig {\n /**\n * Startup locale. Compute platform inputs OUTSIDE the plugin — it never\n * touches the DOM:\n *\n * ```ts\n * i18nPlugin({ locale: negotiateLocale(codes, navigator.languages) ?? 'en' })\n * ```\n *\n * May name a `loaders` pack: the fallback locale shows until the pack\n * arrives (effects fetch it at startup). Defaults to `fallbackLocale`.\n */\n locale?: string;\n /** The pack tried when a key misses the current locale. Default 'en'. */\n fallbackLocale?: string;\n /** Eagerly available packs. */\n locales?: Locale[];\n /**\n * Lazy packs: code → loader. `setLocale(code)` fetches on demand (in\n * effects), registers the pack, then switches. Until a lazy pack loads, a\n * locale switcher shows its code as the name — register eagerly (packs are\n * small) when you want native names in the switcher up front.\n */\n loaders?: Record<string, () => Promise<Locale>>;\n}\n\nexport interface TranslateOptions {\n /**\n * `{slot}` interpolation values. When `count` is a number and the key\n * resolves to a branch object, the branch is picked by CLDR plural\n * category (`Intl.PluralRules`), falling back to `other`:\n *\n * ```ts\n * // pages: { one: '{count} page', other: '{count} pages' }\n * t('pages', { params: { count: 1 } }) // '1 page'\n * ```\n */\n params?: Record<string, string | number>;\n /** Returned (interpolated) when the key misses every pack — instead of the key. */\n fallback?: string;\n}\n\n/** A locale as a switcher sees it — registered packs plus not-yet-loaded lazy ones. */\nexport interface LocaleInfo {\n readonly code: string;\n /** Native name; the bare code until a lazy pack has loaded. */\n readonly name: string;\n readonly dir: 'ltr' | 'rtl';\n readonly loaded: boolean;\n}\n\nexport interface I18nCapability {\n /** Translate a key: current locale → fallback locale → `options.fallback` → the key. */\n t(key: string, options?: TranslateOptions): string;\n locale(): string;\n /** Text direction of the CURRENT locale — wire to `dir=` on the shell. */\n dir(): 'ltr' | 'rtl';\n /** Every known locale (registered + lazy), in registration order. */\n locales(): LocaleInfo[];\n /** Code of the lazy pack being fetched right now, if any. */\n loading(): string | null;\n /** Switch locale; fetches the pack first when it's a lazy one. */\n setLocale(code: string): void;\n /** Register a pack at runtime (customer-supplied translations). */\n registerLocale(locale: Locale): void;\n}\n\nexport const I18nToken = createCapabilityToken<I18nCapability>('i18n');\n","import { definePlugin } from '@embedpdf/core';\nimport { createI18nCapability } from './capability';\nimport { registerI18nEffects } from './effects';\nimport { i18nReducer, initialI18nState } from './reducer';\nimport { I18nToken } from './types';\nimport type { I18nAction, I18nCapability, I18nConfig, I18nState } from './types';\n\n/**\n * The i18n plugin: workspace-scoped (locale is a workspace concern) with NO\n * dependencies — engine-free, DOM-free. Its capability is built synchronously\n * in `createKernel()`, so the shell translates from the first frame, while\n * the engine is still booting.\n */\nexport const i18nPlugin = (config: I18nConfig = {}) =>\n definePlugin<I18nState, I18nAction, I18nCapability>({\n id: 'i18n',\n scope: 'workspace',\n token: I18nToken,\n initialState: () => initialI18nState(config),\n reduce: i18nReducer,\n capability: (ctx) => createI18nCapability(ctx, config),\n effects: registerI18nEffects(config),\n });\n","/**\n * Pure BCP-47 locale negotiation — strings in, string out. The plugin never\n * reads the platform; the EMBEDDER feeds it whatever request list its\n * environment offers:\n *\n * ```ts\n * // browser shell: negotiateLocale(['en', 'es', 'ar'], navigator.languages)\n * // server render: negotiateLocale(codes, parseAcceptLanguage(header))\n * ```\n *\n * Matching, per requested code in preference order:\n * 1. exact match (case-insensitive): 'es-MX' → 'es-MX'\n * 2. request narrowed to its language: 'en-GB' → 'en'\n * 3. any available dialect of the language: 'zh' → 'zh-Hans'\n *\n * Returns the matched code exactly as it appears in `available`, or null.\n */\nexport function negotiateLocale(\n available: readonly string[],\n requested: readonly string[],\n): string | null {\n const byCanon = new Map<string, string>();\n for (const code of available) {\n const canon = code.toLowerCase();\n if (!byCanon.has(canon)) byCanon.set(canon, code);\n }\n const languageOf = (code: string) => code.toLowerCase().split('-')[0];\n\n for (const want of requested) {\n const exact = byCanon.get(want.toLowerCase());\n if (exact !== undefined) return exact;\n }\n for (const want of requested) {\n const narrowed = byCanon.get(languageOf(want));\n if (narrowed !== undefined) return narrowed;\n }\n for (const want of requested) {\n const language = languageOf(want);\n for (const [canon, original] of byCanon) {\n if (languageOf(canon) === language) return original;\n }\n }\n return null;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.cjs","names":["I18nToken"],"sources":["../src/translate.ts","../src/capability.ts","../src/effects.ts","../src/reducer.ts","../src/i18n.plugin.ts","../src/negotiate.ts"],"sourcesContent":["import type { I18nState, TranslateOptions, TranslationDictionary } from './types';\n\n/**\n * The pure lookup core — state in, string out. No platform access anywhere\n * (`Intl.PluralRules` is core ECMAScript, not DOM); this is the part that\n * ports to Rust verbatim.\n */\n\n/** Walk a dotted path ('commands.zoom.in') through a dictionary tree. */\nfunction lookup(\n dict: TranslationDictionary | undefined,\n key: string,\n): string | TranslationDictionary | undefined {\n if (!dict) return undefined;\n let node: string | TranslationDictionary | undefined = dict;\n for (const part of key.split('.')) {\n if (node === undefined || typeof node === 'string') return undefined;\n node = node[part];\n }\n return node;\n}\n\n/** CLDR plural category for a count, with a safe fallback when the runtime\n * lacks ICU data for the locale. */\nfunction pluralCategory(locale: string, count: number): string {\n try {\n return new Intl.PluralRules(locale).select(count);\n } catch {\n return count === 1 ? 'one' : 'other';\n }\n}\n\n/** A leaf hit is a string; a branch object needs a plural pick via `count`. */\nfunction resolveNode(\n node: string | TranslationDictionary | undefined,\n locale: string,\n params?: Record<string, string | number>,\n): string | undefined {\n if (typeof node === 'string') return node;\n if (node !== undefined && typeof params?.count === 'number') {\n const branch = node[pluralCategory(locale, params.count)] ?? node['other'];\n if (typeof branch === 'string') return branch;\n }\n return undefined;\n}\n\n/** Replace `{slot}` markers; unknown slots stay verbatim (visible in QA). */\nexport function interpolate(text: string, params?: Record<string, string | number>): string {\n if (!params) return text;\n return text.replace(/\\{(\\w+)\\}/g, (match, slot) =>\n params[slot] !== undefined ? String(params[slot]) : match,\n );\n}\n\nexport interface TranslateResult {\n readonly text: string;\n /** False when the key missed every pack (text is then the fallback or the key). */\n readonly found: boolean;\n}\n\n/**\n * Resolve a key against the state: current locale → fallback locale →\n * `options.fallback` (interpolated too) → the key itself.\n */\nexport function translate(\n state: I18nState,\n key: string,\n options?: TranslateOptions,\n): TranslateResult {\n const { locale, fallbackLocale, locales } = state;\n const text =\n resolveNode(lookup(locales[locale]?.translations, key), locale, options?.params) ??\n resolveNode(\n lookup(locales[fallbackLocale]?.translations, key),\n fallbackLocale,\n options?.params,\n );\n if (text !== undefined) return { text: interpolate(text, options?.params), found: true };\n if (options?.fallback !== undefined)\n return { text: interpolate(options.fallback, options.params), found: false };\n return { text: key, found: false };\n}\n","import type { PluginContext } from '@embedpdf/core';\nimport { translate } from './translate';\nimport type { I18nAction, I18nCapability, I18nConfig, I18nState, LocaleInfo } from './types';\n\n/**\n * The i18n capability — selectors (pure reads over state) + intents (the only\n * writers). Built synchronously in `createKernel()`, so `t()` works before the\n * engine exists — including inside the viewer shell's loading UI.\n */\nexport function createI18nCapability(\n ctx: PluginContext<I18nState, I18nAction>,\n config: I18nConfig,\n): I18nCapability {\n // Dev signal, once per offender — a missing key otherwise fails silently\n // (by design: `t()` always returns a usable string).\n const warned = new Set<string>();\n const warnOnce = (id: string, message: string) => {\n if (warned.has(id)) return;\n warned.add(id);\n console.warn(message);\n };\n\n return {\n t: (key, options) => {\n const result = translate(ctx.getState(), key, options);\n if (!result.found && options?.fallback === undefined) {\n warnOnce(\n `key:${key}`,\n `[i18n] missing translation \"${key}\" (locale: ${ctx.getState().locale})`,\n );\n }\n return result.text;\n },\n locale: () => ctx.getState().locale,\n dir: () => {\n const { locales, locale } = ctx.getState();\n return locales[locale]?.dir ?? 'ltr';\n },\n locales: () => {\n const { locales } = ctx.getState();\n const known: LocaleInfo[] = Object.values(locales).map((locale) => ({\n code: locale.code,\n name: locale.name,\n dir: locale.dir ?? 'ltr',\n loaded: true,\n }));\n for (const code of Object.keys(config.loaders ?? {})) {\n if (!locales[code]) known.push({ code, name: code, dir: 'ltr', loaded: false });\n }\n return known;\n },\n loading: () => ctx.getState().loading,\n setLocale: (code) => {\n const state = ctx.getState();\n if (state.locales[code]) {\n ctx.dispatch({ type: 'I18N/SET_LOCALE', locale: code });\n } else if (config.loaders?.[code]) {\n ctx.dispatch({ type: 'I18N/LOAD_STARTED', locale: code });\n } else {\n warnOnce(\n `locale:${code}`,\n `[i18n] unknown locale \"${code}\" — not registered and no loader configured`,\n );\n }\n },\n registerLocale: (locale) => ctx.dispatch({ type: 'I18N/REGISTER_LOCALE', locale }),\n };\n}\n","import type { EffectContext } from '@embedpdf/core';\nimport type { I18nAction, I18nConfig, I18nState } from './types';\n\n/**\n * The plugin's ONE side-effect: fetching lazy locale packs. State-driven — a\n * load is `state.loading`, set by the capability (a switch to a lazy pack) or\n * by the initial state (a lazy startup locale), so a load requested before\n * effects ran is picked up here and nothing is lost to boot ordering.\n */\nexport function registerI18nEffects(config: I18nConfig) {\n return (ctx: EffectContext<I18nState, I18nAction>): void => {\n const load = (code: string) => {\n const loader = config.loaders?.[code];\n if (!loader) return; // unreachable via the capability; guards bad dispatches\n loader().then(\n (locale) => {\n ctx.dispatch({ type: 'I18N/REGISTER_LOCALE', locale });\n // Complete the switch only if this load is still the wanted one —\n // the user may have switched again while the pack was in flight.\n if (ctx.getState().loading === code) {\n ctx.dispatch({ type: 'I18N/SET_LOCALE', locale: locale.code });\n }\n },\n () => ctx.dispatch({ type: 'I18N/LOAD_FAILED', locale: code }),\n );\n };\n\n const pending = ctx.getState().loading;\n if (pending) load(pending);\n ctx.watch(\n () => ctx.getState().loading,\n (code) => {\n if (code) load(code);\n },\n );\n };\n}\n","import type { I18nAction, I18nConfig, I18nState, Locale } from './types';\n\n/**\n * Pure and serializable — locale packs are data, so they live HERE, not in a\n * side-table. One source of truth: `t()` is a pure function of this state and\n * reactivity rides the kernel's one change stream (no emitters).\n */\n\nexport function initialI18nState(config: I18nConfig): I18nState {\n const fallbackLocale = config.fallbackLocale ?? 'en';\n const locales: Record<string, Locale> = {};\n for (const locale of config.locales ?? []) locales[locale.code] = locale;\n const locale = config.locale ?? fallbackLocale;\n // A startup locale that is a lazy pack: show the fallback chain until the\n // pack arrives — seeding `loading` makes effects fetch it at startup.\n const needsLoad = !locales[locale] && config.loaders?.[locale] !== undefined;\n return { locale, fallbackLocale, locales, loading: needsLoad ? locale : null };\n}\n\nexport function i18nReducer(state: I18nState, action: I18nAction): I18nState {\n switch (action.type) {\n case 'I18N/SET_LOCALE': {\n // Only registered packs can become current — lazy ones arrive via\n // LOAD_STARTED → REGISTER_LOCALE → SET_LOCALE (effects drive the middle).\n if (!state.locales[action.locale]) return state;\n if (state.locale === action.locale && state.loading === null) return state;\n return { ...state, locale: action.locale, loading: null };\n }\n case 'I18N/REGISTER_LOCALE':\n return { ...state, locales: { ...state.locales, [action.locale.code]: action.locale } };\n case 'I18N/LOAD_STARTED':\n return state.loading === action.locale ? state : { ...state, loading: action.locale };\n case 'I18N/LOAD_FAILED':\n return state.loading === action.locale ? { ...state, loading: null } : state;\n default:\n return state;\n }\n}\n","import { definePlugin } from '@embedpdf/core';\nimport { createI18nCapability } from './capability';\nimport { registerI18nEffects } from './effects';\nimport { i18nReducer, initialI18nState } from './reducer';\nimport { I18nToken } from './types';\nimport type { I18nAction, I18nCapability, I18nConfig, I18nState } from './types';\n\n/**\n * The i18n plugin: workspace-scoped (locale is a workspace concern) with NO\n * dependencies — engine-free, DOM-free. Its capability is built synchronously\n * in `createKernel()`, so the shell translates from the first frame, while\n * the engine is still booting.\n */\nexport const i18nPlugin = (config: I18nConfig = {}) =>\n definePlugin<I18nState, I18nAction, I18nCapability>({\n id: 'i18n',\n scope: 'workspace',\n token: I18nToken,\n initialState: () => initialI18nState(config),\n reduce: i18nReducer,\n capability: (ctx) => createI18nCapability(ctx, config),\n effects: registerI18nEffects(config),\n });\n","/**\n * Pure BCP-47 locale negotiation — strings in, string out. The plugin never\n * reads the platform; the EMBEDDER feeds it whatever request list its\n * environment offers:\n *\n * ```ts\n * // browser shell: negotiateLocale(['en', 'es', 'ar'], navigator.languages)\n * // server render: negotiateLocale(codes, parseAcceptLanguage(header))\n * ```\n *\n * Matching, per requested code in preference order:\n * 1. exact match (case-insensitive): 'es-MX' → 'es-MX'\n * 2. request narrowed to its language: 'en-GB' → 'en'\n * 3. any available dialect of the language: 'zh' → 'zh-Hans'\n *\n * Returns the matched code exactly as it appears in `available`, or null.\n */\nexport function negotiateLocale(\n available: readonly string[],\n requested: readonly string[],\n): string | null {\n const byCanon = new Map<string, string>();\n for (const code of available) {\n const canon = code.toLowerCase();\n if (!byCanon.has(canon)) byCanon.set(canon, code);\n }\n const languageOf = (code: string) => code.toLowerCase().split('-')[0];\n\n for (const want of requested) {\n const exact = byCanon.get(want.toLowerCase());\n if (exact !== undefined) return exact;\n }\n for (const want of requested) {\n const narrowed = byCanon.get(languageOf(want));\n if (narrowed !== undefined) return narrowed;\n }\n for (const want of requested) {\n const language = languageOf(want);\n for (const [canon, original] of byCanon) {\n if (languageOf(canon) === language) return original;\n }\n }\n return null;\n}\n"],"mappings":";;;;;;;;;;AASA,SAAS,OACP,MACA,KAC4C;CAC5C,IAAI,CAAC,MAAM,OAAO,KAAA;CAClB,IAAI,OAAmD;CACvD,KAAK,MAAM,QAAQ,IAAI,MAAM,GAAG,GAAG;EACjC,IAAI,SAAS,KAAA,KAAa,OAAO,SAAS,UAAU,OAAO,KAAA;EAC3D,OAAO,KAAK;CACd;CACA,OAAO;AACT;;;AAIA,SAAS,eAAe,QAAgB,OAAuB;CAC7D,IAAI;EACF,OAAO,IAAI,KAAK,YAAY,MAAM,CAAC,CAAC,OAAO,KAAK;CAClD,QAAQ;EACN,OAAO,UAAU,IAAI,QAAQ;CAC/B;AACF;;AAGA,SAAS,YACP,MACA,QACA,QACoB;CACpB,IAAI,OAAO,SAAS,UAAU,OAAO;CACrC,IAAI,SAAS,KAAA,KAAa,OAAO,QAAQ,UAAU,UAAU;EAC3D,MAAM,SAAS,KAAK,eAAe,QAAQ,OAAO,KAAK,MAAM,KAAK;EAClE,IAAI,OAAO,WAAW,UAAU,OAAO;CACzC;AAEF;;AAGA,SAAgB,YAAY,MAAc,QAAkD;CAC1F,IAAI,CAAC,QAAQ,OAAO;CACpB,OAAO,KAAK,QAAQ,eAAe,OAAO,SACxC,OAAO,UAAU,KAAA,IAAY,OAAO,OAAO,KAAK,IAAI,KACtD;AACF;;;;;AAYA,SAAgB,UACd,OACA,KACA,SACiB;CACjB,MAAM,EAAE,QAAQ,gBAAgB,YAAY;CAC5C,MAAM,OACJ,YAAY,OAAO,QAAQ,OAAO,EAAE,cAAc,GAAG,GAAG,QAAQ,SAAS,MAAM,KAC/E,YACE,OAAO,QAAQ,eAAe,EAAE,cAAc,GAAG,GACjD,gBACA,SAAS,MACX;CACF,IAAI,SAAS,KAAA,GAAW,OAAO;EAAE,MAAM,YAAY,MAAM,SAAS,MAAM;EAAG,OAAO;CAAK;CACvF,IAAI,SAAS,aAAa,KAAA,GACxB,OAAO;EAAE,MAAM,YAAY,QAAQ,UAAU,QAAQ,MAAM;EAAG,OAAO;CAAM;CAC7E,OAAO;EAAE,MAAM;EAAK,OAAO;CAAM;AACnC;;;;;;;;ACxEA,SAAgB,qBACd,KACA,QACgB;CAGhB,MAAM,yBAAS,IAAI,IAAY;CAC/B,MAAM,YAAY,IAAY,YAAoB;EAChD,IAAI,OAAO,IAAI,EAAE,GAAG;EACpB,OAAO,IAAI,EAAE;EACb,QAAQ,KAAK,OAAO;CACtB;CAEA,OAAO;EACL,IAAI,KAAK,YAAY;GACnB,MAAM,SAAS,UAAU,IAAI,SAAS,GAAG,KAAK,OAAO;GACrD,IAAI,CAAC,OAAO,SAAS,SAAS,aAAa,KAAA,GACzC,SACE,OAAO,OACP,+BAA+B,IAAI,aAAa,IAAI,SAAS,CAAC,CAAC,OAAO,EACxE;GAEF,OAAO,OAAO;EAChB;EACA,cAAc,IAAI,SAAS,CAAC,CAAC;EAC7B,WAAW;GACT,MAAM,EAAE,SAAS,WAAW,IAAI,SAAS;GACzC,OAAO,QAAQ,OAAO,EAAE,OAAO;EACjC;EACA,eAAe;GACb,MAAM,EAAE,YAAY,IAAI,SAAS;GACjC,MAAM,QAAsB,OAAO,OAAO,OAAO,CAAC,CAAC,KAAK,YAAY;IAClE,MAAM,OAAO;IACb,MAAM,OAAO;IACb,KAAK,OAAO,OAAO;IACnB,QAAQ;GACV,EAAE;GACF,KAAK,MAAM,QAAQ,OAAO,KAAK,OAAO,WAAW,CAAC,CAAC,GACjD,IAAI,CAAC,QAAQ,OAAO,MAAM,KAAK;IAAE;IAAM,MAAM;IAAM,KAAK;IAAO,QAAQ;GAAM,CAAC;GAEhF,OAAO;EACT;EACA,eAAe,IAAI,SAAS,CAAC,CAAC;EAC9B,YAAY,SAAS;GAEnB,IADc,IAAI,SACV,CAAC,CAAC,QAAQ,OAChB,IAAI,SAAS;IAAE,MAAM;IAAmB,QAAQ;GAAK,CAAC;QACjD,IAAI,OAAO,UAAU,OAC1B,IAAI,SAAS;IAAE,MAAM;IAAqB,QAAQ;GAAK,CAAC;QAExD,SACE,UAAU,QACV,0BAA0B,KAAK,4CACjC;EAEJ;EACA,iBAAiB,WAAW,IAAI,SAAS;GAAE,MAAM;GAAwB;EAAO,CAAC;CACnF;AACF;;;;;;;;;AC1DA,SAAgB,oBAAoB,QAAoB;CACtD,QAAQ,QAAoD;EAC1D,MAAM,QAAQ,SAAiB;GAC7B,MAAM,SAAS,OAAO,UAAU;GAChC,IAAI,CAAC,QAAQ;GACb,OAAO,CAAC,CAAC,MACN,WAAW;IACV,IAAI,SAAS;KAAE,MAAM;KAAwB;IAAO,CAAC;IAGrD,IAAI,IAAI,SAAS,CAAC,CAAC,YAAY,MAC7B,IAAI,SAAS;KAAE,MAAM;KAAmB,QAAQ,OAAO;IAAK,CAAC;GAEjE,SACM,IAAI,SAAS;IAAE,MAAM;IAAoB,QAAQ;GAAK,CAAC,CAC/D;EACF;EAEA,MAAM,UAAU,IAAI,SAAS,CAAC,CAAC;EAC/B,IAAI,SAAS,KAAK,OAAO;EACzB,IAAI,YACI,IAAI,SAAS,CAAC,CAAC,UACpB,SAAS;GACR,IAAI,MAAM,KAAK,IAAI;EACrB,CACF;CACF;AACF;;;;;;;;AC5BA,SAAgB,iBAAiB,QAA+B;CAC9D,MAAM,iBAAiB,OAAO,kBAAkB;CAChD,MAAM,UAAkC,CAAC;CACzC,KAAK,MAAM,UAAU,OAAO,WAAW,CAAC,GAAG,QAAQ,OAAO,QAAQ;CAClE,MAAM,SAAS,OAAO,UAAU;CAIhC,OAAO;EAAE;EAAQ;EAAgB;EAAS,SADxB,CAAC,QAAQ,WAAW,OAAO,UAAU,YAAY,KAAA,IACJ,SAAS;CAAK;AAC/E;AAEA,SAAgB,YAAY,OAAkB,QAA+B;CAC3E,QAAQ,OAAO,MAAf;EACE,KAAK;GAGH,IAAI,CAAC,MAAM,QAAQ,OAAO,SAAS,OAAO;GAC1C,IAAI,MAAM,WAAW,OAAO,UAAU,MAAM,YAAY,MAAM,OAAO;GACrE,OAAO;IAAE,GAAG;IAAO,QAAQ,OAAO;IAAQ,SAAS;GAAK;EAE1D,KAAK,wBACH,OAAO;GAAE,GAAG;GAAO,SAAS;IAAE,GAAG,MAAM;KAAU,OAAO,OAAO,OAAO,OAAO;GAAO;EAAE;EACxF,KAAK,qBACH,OAAO,MAAM,YAAY,OAAO,SAAS,QAAQ;GAAE,GAAG;GAAO,SAAS,OAAO;EAAO;EACtF,KAAK,oBACH,OAAO,MAAM,YAAY,OAAO,SAAS;GAAE,GAAG;GAAO,SAAS;EAAK,IAAI;EACzE,SACE,OAAO;CACX;AACF;;;;;;;;;ACxBA,MAAa,cAAc,SAAqB,CAAC,OAAA,GAAA,eAAA,aAAA,CACK;CAClD,IAAI;CACJ,OAAO;CACP,OAAOA,iBAAAA;CACP,oBAAoB,iBAAiB,MAAM;CAC3C,QAAQ;CACR,aAAa,QAAQ,qBAAqB,KAAK,MAAM;CACrD,SAAS,oBAAoB,MAAM;AACrC,CAAC;;;;;;;;;;;;;;;;;;;;ACLH,SAAgB,gBACd,WACA,WACe;CACf,MAAM,0BAAU,IAAI,IAAoB;CACxC,KAAK,MAAM,QAAQ,WAAW;EAC5B,MAAM,QAAQ,KAAK,YAAY;EAC/B,IAAI,CAAC,QAAQ,IAAI,KAAK,GAAG,QAAQ,IAAI,OAAO,IAAI;CAClD;CACA,MAAM,cAAc,SAAiB,KAAK,YAAY,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC;CAEnE,KAAK,MAAM,QAAQ,WAAW;EAC5B,MAAM,QAAQ,QAAQ,IAAI,KAAK,YAAY,CAAC;EAC5C,IAAI,UAAU,KAAA,GAAW,OAAO;CAClC;CACA,KAAK,MAAM,QAAQ,WAAW;EAC5B,MAAM,WAAW,QAAQ,IAAI,WAAW,IAAI,CAAC;EAC7C,IAAI,aAAa,KAAA,GAAW,OAAO;CACrC;CACA,KAAK,MAAM,QAAQ,WAAW;EAC5B,MAAM,WAAW,WAAW,IAAI;EAChC,KAAK,MAAM,CAAC,OAAO,aAAa,SAC9B,IAAI,WAAW,KAAK,MAAM,UAAU,OAAO;CAE/C;CACA,OAAO;AACT"}
|
package/dist/index.d.cts
CHANGED
|
@@ -1,117 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
/**
|
|
3
|
-
* @embedpdf/plugin-i18n — the contract.
|
|
4
|
-
*
|
|
5
|
-
* Translation is pure data over pure state: locale packs live IN the store
|
|
6
|
-
* (not in a side-table), so `t()` is a pure read, every consumer is reactive
|
|
7
|
-
* through the one kernel change stream, and the whole thing serializes (SSR,
|
|
8
|
-
* snapshots, persist). Workspace-scoped, `requires: []`, never touches the
|
|
9
|
-
* engine or the DOM — the capability is alive from `createKernel()`, before
|
|
10
|
-
* a single WASM byte is fetched. The plugin ships NO strings: mechanism here,
|
|
11
|
-
* content (locale packs) in the product that embeds it.
|
|
12
|
-
*/
|
|
13
|
-
/** Nested tree of translation strings. Leaves interpolate `{param}` slots. */
|
|
14
|
-
interface TranslationDictionary {
|
|
15
|
-
readonly [key: string]: string | TranslationDictionary;
|
|
16
|
-
}
|
|
17
|
-
interface Locale {
|
|
18
|
-
/** BCP-47 code: 'en', 'es', 'ar', 'zh-Hans'. */
|
|
19
|
-
readonly code: string;
|
|
20
|
-
/** Native display name: 'Español' — what a locale switcher shows. */
|
|
21
|
-
readonly name: string;
|
|
22
|
-
/** Text direction for viewer chrome. Defaults to 'ltr'. */
|
|
23
|
-
readonly dir?: 'ltr' | 'rtl';
|
|
24
|
-
readonly translations: TranslationDictionary;
|
|
25
|
-
}
|
|
26
|
-
interface I18nState {
|
|
27
|
-
readonly locale: string;
|
|
28
|
-
readonly fallbackLocale: string;
|
|
29
|
-
/** Registered packs by code — the lookup table `t()` reads. */
|
|
30
|
-
readonly locales: Readonly<Record<string, Locale>>;
|
|
31
|
-
/** Code of a lazy pack currently being fetched (drives switcher spinners). */
|
|
32
|
-
readonly loading: string | null;
|
|
33
|
-
}
|
|
34
|
-
type I18nAction =
|
|
35
|
-
/** Switch to a REGISTERED locale. The reducer ignores unregistered codes —
|
|
36
|
-
* loading a lazy pack goes through LOAD_STARTED (the capability routes). */
|
|
37
|
-
{
|
|
38
|
-
type: 'I18N/SET_LOCALE';
|
|
39
|
-
locale: string;
|
|
40
|
-
} | {
|
|
41
|
-
type: 'I18N/REGISTER_LOCALE';
|
|
42
|
-
locale: Locale;
|
|
43
|
-
} | {
|
|
44
|
-
type: 'I18N/LOAD_STARTED';
|
|
45
|
-
locale: string;
|
|
46
|
-
} | {
|
|
47
|
-
type: 'I18N/LOAD_FAILED';
|
|
48
|
-
locale: string;
|
|
49
|
-
};
|
|
50
|
-
interface I18nConfig {
|
|
51
|
-
/**
|
|
52
|
-
* Startup locale. Compute platform inputs OUTSIDE the plugin — it never
|
|
53
|
-
* touches the DOM:
|
|
54
|
-
*
|
|
55
|
-
* ```ts
|
|
56
|
-
* i18nPlugin({ locale: negotiateLocale(codes, navigator.languages) ?? 'en' })
|
|
57
|
-
* ```
|
|
58
|
-
*
|
|
59
|
-
* May name a `loaders` pack: the fallback locale shows until the pack
|
|
60
|
-
* arrives (effects fetch it at startup). Defaults to `fallbackLocale`.
|
|
61
|
-
*/
|
|
62
|
-
locale?: string;
|
|
63
|
-
/** The pack tried when a key misses the current locale. Default 'en'. */
|
|
64
|
-
fallbackLocale?: string;
|
|
65
|
-
/** Eagerly available packs. */
|
|
66
|
-
locales?: Locale[];
|
|
67
|
-
/**
|
|
68
|
-
* Lazy packs: code → loader. `setLocale(code)` fetches on demand (in
|
|
69
|
-
* effects), registers the pack, then switches. Until a lazy pack loads, a
|
|
70
|
-
* locale switcher shows its code as the name — register eagerly (packs are
|
|
71
|
-
* small) when you want native names in the switcher up front.
|
|
72
|
-
*/
|
|
73
|
-
loaders?: Record<string, () => Promise<Locale>>;
|
|
74
|
-
}
|
|
75
|
-
interface TranslateOptions {
|
|
76
|
-
/**
|
|
77
|
-
* `{slot}` interpolation values. When `count` is a number and the key
|
|
78
|
-
* resolves to a branch object, the branch is picked by CLDR plural
|
|
79
|
-
* category (`Intl.PluralRules`), falling back to `other`:
|
|
80
|
-
*
|
|
81
|
-
* ```ts
|
|
82
|
-
* // pages: { one: '{count} page', other: '{count} pages' }
|
|
83
|
-
* t('pages', { params: { count: 1 } }) // '1 page'
|
|
84
|
-
* ```
|
|
85
|
-
*/
|
|
86
|
-
params?: Record<string, string | number>;
|
|
87
|
-
/** Returned (interpolated) when the key misses every pack — instead of the key. */
|
|
88
|
-
fallback?: string;
|
|
89
|
-
}
|
|
90
|
-
/** A locale as a switcher sees it — registered packs plus not-yet-loaded lazy ones. */
|
|
91
|
-
interface LocaleInfo {
|
|
92
|
-
readonly code: string;
|
|
93
|
-
/** Native name; the bare code until a lazy pack has loaded. */
|
|
94
|
-
readonly name: string;
|
|
95
|
-
readonly dir: 'ltr' | 'rtl';
|
|
96
|
-
readonly loaded: boolean;
|
|
97
|
-
}
|
|
98
|
-
interface I18nCapability {
|
|
99
|
-
/** Translate a key: current locale → fallback locale → `options.fallback` → the key. */
|
|
100
|
-
t(key: string, options?: TranslateOptions): string;
|
|
101
|
-
locale(): string;
|
|
102
|
-
/** Text direction of the CURRENT locale — wire to `dir=` on the shell. */
|
|
103
|
-
dir(): 'ltr' | 'rtl';
|
|
104
|
-
/** Every known locale (registered + lazy), in registration order. */
|
|
105
|
-
locales(): LocaleInfo[];
|
|
106
|
-
/** Code of the lazy pack being fetched right now, if any. */
|
|
107
|
-
loading(): string | null;
|
|
108
|
-
/** Switch locale; fetches the pack first when it's a lazy one. */
|
|
109
|
-
setLocale(code: string): void;
|
|
110
|
-
/** Register a pack at runtime (customer-supplied translations). */
|
|
111
|
-
registerLocale(locale: Locale): void;
|
|
112
|
-
}
|
|
113
|
-
declare const I18nToken: import("@embedpdf/core").CapabilityToken<I18nCapability>;
|
|
114
|
-
//#endregion
|
|
1
|
+
import { a as I18nToken, c as TranslateOptions, i as I18nState, l as TranslationDictionary, n as I18nCapability, o as Locale, r as I18nConfig, s as LocaleInfo, t as I18nAction } from "./contract-DCt9Ikqu.cjs";
|
|
115
2
|
//#region src/i18n.plugin.d.ts
|
|
116
3
|
/**
|
|
117
4
|
* The i18n plugin: workspace-scoped (locale is a workspace concern) with NO
|
package/dist/index.d.ts
CHANGED
|
@@ -1,117 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
/**
|
|
3
|
-
* @embedpdf/plugin-i18n — the contract.
|
|
4
|
-
*
|
|
5
|
-
* Translation is pure data over pure state: locale packs live IN the store
|
|
6
|
-
* (not in a side-table), so `t()` is a pure read, every consumer is reactive
|
|
7
|
-
* through the one kernel change stream, and the whole thing serializes (SSR,
|
|
8
|
-
* snapshots, persist). Workspace-scoped, `requires: []`, never touches the
|
|
9
|
-
* engine or the DOM — the capability is alive from `createKernel()`, before
|
|
10
|
-
* a single WASM byte is fetched. The plugin ships NO strings: mechanism here,
|
|
11
|
-
* content (locale packs) in the product that embeds it.
|
|
12
|
-
*/
|
|
13
|
-
/** Nested tree of translation strings. Leaves interpolate `{param}` slots. */
|
|
14
|
-
interface TranslationDictionary {
|
|
15
|
-
readonly [key: string]: string | TranslationDictionary;
|
|
16
|
-
}
|
|
17
|
-
interface Locale {
|
|
18
|
-
/** BCP-47 code: 'en', 'es', 'ar', 'zh-Hans'. */
|
|
19
|
-
readonly code: string;
|
|
20
|
-
/** Native display name: 'Español' — what a locale switcher shows. */
|
|
21
|
-
readonly name: string;
|
|
22
|
-
/** Text direction for viewer chrome. Defaults to 'ltr'. */
|
|
23
|
-
readonly dir?: 'ltr' | 'rtl';
|
|
24
|
-
readonly translations: TranslationDictionary;
|
|
25
|
-
}
|
|
26
|
-
interface I18nState {
|
|
27
|
-
readonly locale: string;
|
|
28
|
-
readonly fallbackLocale: string;
|
|
29
|
-
/** Registered packs by code — the lookup table `t()` reads. */
|
|
30
|
-
readonly locales: Readonly<Record<string, Locale>>;
|
|
31
|
-
/** Code of a lazy pack currently being fetched (drives switcher spinners). */
|
|
32
|
-
readonly loading: string | null;
|
|
33
|
-
}
|
|
34
|
-
type I18nAction =
|
|
35
|
-
/** Switch to a REGISTERED locale. The reducer ignores unregistered codes —
|
|
36
|
-
* loading a lazy pack goes through LOAD_STARTED (the capability routes). */
|
|
37
|
-
{
|
|
38
|
-
type: 'I18N/SET_LOCALE';
|
|
39
|
-
locale: string;
|
|
40
|
-
} | {
|
|
41
|
-
type: 'I18N/REGISTER_LOCALE';
|
|
42
|
-
locale: Locale;
|
|
43
|
-
} | {
|
|
44
|
-
type: 'I18N/LOAD_STARTED';
|
|
45
|
-
locale: string;
|
|
46
|
-
} | {
|
|
47
|
-
type: 'I18N/LOAD_FAILED';
|
|
48
|
-
locale: string;
|
|
49
|
-
};
|
|
50
|
-
interface I18nConfig {
|
|
51
|
-
/**
|
|
52
|
-
* Startup locale. Compute platform inputs OUTSIDE the plugin — it never
|
|
53
|
-
* touches the DOM:
|
|
54
|
-
*
|
|
55
|
-
* ```ts
|
|
56
|
-
* i18nPlugin({ locale: negotiateLocale(codes, navigator.languages) ?? 'en' })
|
|
57
|
-
* ```
|
|
58
|
-
*
|
|
59
|
-
* May name a `loaders` pack: the fallback locale shows until the pack
|
|
60
|
-
* arrives (effects fetch it at startup). Defaults to `fallbackLocale`.
|
|
61
|
-
*/
|
|
62
|
-
locale?: string;
|
|
63
|
-
/** The pack tried when a key misses the current locale. Default 'en'. */
|
|
64
|
-
fallbackLocale?: string;
|
|
65
|
-
/** Eagerly available packs. */
|
|
66
|
-
locales?: Locale[];
|
|
67
|
-
/**
|
|
68
|
-
* Lazy packs: code → loader. `setLocale(code)` fetches on demand (in
|
|
69
|
-
* effects), registers the pack, then switches. Until a lazy pack loads, a
|
|
70
|
-
* locale switcher shows its code as the name — register eagerly (packs are
|
|
71
|
-
* small) when you want native names in the switcher up front.
|
|
72
|
-
*/
|
|
73
|
-
loaders?: Record<string, () => Promise<Locale>>;
|
|
74
|
-
}
|
|
75
|
-
interface TranslateOptions {
|
|
76
|
-
/**
|
|
77
|
-
* `{slot}` interpolation values. When `count` is a number and the key
|
|
78
|
-
* resolves to a branch object, the branch is picked by CLDR plural
|
|
79
|
-
* category (`Intl.PluralRules`), falling back to `other`:
|
|
80
|
-
*
|
|
81
|
-
* ```ts
|
|
82
|
-
* // pages: { one: '{count} page', other: '{count} pages' }
|
|
83
|
-
* t('pages', { params: { count: 1 } }) // '1 page'
|
|
84
|
-
* ```
|
|
85
|
-
*/
|
|
86
|
-
params?: Record<string, string | number>;
|
|
87
|
-
/** Returned (interpolated) when the key misses every pack — instead of the key. */
|
|
88
|
-
fallback?: string;
|
|
89
|
-
}
|
|
90
|
-
/** A locale as a switcher sees it — registered packs plus not-yet-loaded lazy ones. */
|
|
91
|
-
interface LocaleInfo {
|
|
92
|
-
readonly code: string;
|
|
93
|
-
/** Native name; the bare code until a lazy pack has loaded. */
|
|
94
|
-
readonly name: string;
|
|
95
|
-
readonly dir: 'ltr' | 'rtl';
|
|
96
|
-
readonly loaded: boolean;
|
|
97
|
-
}
|
|
98
|
-
interface I18nCapability {
|
|
99
|
-
/** Translate a key: current locale → fallback locale → `options.fallback` → the key. */
|
|
100
|
-
t(key: string, options?: TranslateOptions): string;
|
|
101
|
-
locale(): string;
|
|
102
|
-
/** Text direction of the CURRENT locale — wire to `dir=` on the shell. */
|
|
103
|
-
dir(): 'ltr' | 'rtl';
|
|
104
|
-
/** Every known locale (registered + lazy), in registration order. */
|
|
105
|
-
locales(): LocaleInfo[];
|
|
106
|
-
/** Code of the lazy pack being fetched right now, if any. */
|
|
107
|
-
loading(): string | null;
|
|
108
|
-
/** Switch locale; fetches the pack first when it's a lazy one. */
|
|
109
|
-
setLocale(code: string): void;
|
|
110
|
-
/** Register a pack at runtime (customer-supplied translations). */
|
|
111
|
-
registerLocale(locale: Locale): void;
|
|
112
|
-
}
|
|
113
|
-
declare const I18nToken: import("@embedpdf/core").CapabilityToken<I18nCapability>;
|
|
114
|
-
//#endregion
|
|
1
|
+
import { a as I18nToken, c as TranslateOptions, i as I18nState, l as TranslationDictionary, n as I18nCapability, o as Locale, r as I18nConfig, s as LocaleInfo, t as I18nAction } from "./contract-DCt9Ikqu.js";
|
|
115
2
|
//#region src/i18n.plugin.d.ts
|
|
116
3
|
/**
|
|
117
4
|
* The i18n plugin: workspace-scoped (locale is a workspace concern) with NO
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { t as I18nToken } from "./contract-D08iYq9-.js";
|
|
2
|
+
import { definePlugin } from "@embedpdf/core";
|
|
2
3
|
//#region src/translate.ts
|
|
3
4
|
/**
|
|
4
5
|
* The pure lookup core — state in, string out. No platform access anywhere
|
|
@@ -198,9 +199,6 @@ function i18nReducer(state, action) {
|
|
|
198
199
|
}
|
|
199
200
|
}
|
|
200
201
|
//#endregion
|
|
201
|
-
//#region src/types.ts
|
|
202
|
-
const I18nToken = createCapabilityToken("i18n");
|
|
203
|
-
//#endregion
|
|
204
202
|
//#region src/i18n.plugin.ts
|
|
205
203
|
/**
|
|
206
204
|
* The i18n plugin: workspace-scoped (locale is a workspace concern) with NO
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../src/translate.ts","../src/capability.ts","../src/effects.ts","../src/reducer.ts","../src/types.ts","../src/i18n.plugin.ts","../src/negotiate.ts"],"sourcesContent":["import type { I18nState, TranslateOptions, TranslationDictionary } from './types';\n\n/**\n * The pure lookup core — state in, string out. No platform access anywhere\n * (`Intl.PluralRules` is core ECMAScript, not DOM); this is the part that\n * ports to Rust verbatim.\n */\n\n/** Walk a dotted path ('commands.zoom.in') through a dictionary tree. */\nfunction lookup(\n dict: TranslationDictionary | undefined,\n key: string,\n): string | TranslationDictionary | undefined {\n if (!dict) return undefined;\n let node: string | TranslationDictionary | undefined = dict;\n for (const part of key.split('.')) {\n if (node === undefined || typeof node === 'string') return undefined;\n node = node[part];\n }\n return node;\n}\n\n/** CLDR plural category for a count, with a safe fallback when the runtime\n * lacks ICU data for the locale. */\nfunction pluralCategory(locale: string, count: number): string {\n try {\n return new Intl.PluralRules(locale).select(count);\n } catch {\n return count === 1 ? 'one' : 'other';\n }\n}\n\n/** A leaf hit is a string; a branch object needs a plural pick via `count`. */\nfunction resolveNode(\n node: string | TranslationDictionary | undefined,\n locale: string,\n params?: Record<string, string | number>,\n): string | undefined {\n if (typeof node === 'string') return node;\n if (node !== undefined && typeof params?.count === 'number') {\n const branch = node[pluralCategory(locale, params.count)] ?? node['other'];\n if (typeof branch === 'string') return branch;\n }\n return undefined;\n}\n\n/** Replace `{slot}` markers; unknown slots stay verbatim (visible in QA). */\nexport function interpolate(text: string, params?: Record<string, string | number>): string {\n if (!params) return text;\n return text.replace(/\\{(\\w+)\\}/g, (match, slot) =>\n params[slot] !== undefined ? String(params[slot]) : match,\n );\n}\n\nexport interface TranslateResult {\n readonly text: string;\n /** False when the key missed every pack (text is then the fallback or the key). */\n readonly found: boolean;\n}\n\n/**\n * Resolve a key against the state: current locale → fallback locale →\n * `options.fallback` (interpolated too) → the key itself.\n */\nexport function translate(\n state: I18nState,\n key: string,\n options?: TranslateOptions,\n): TranslateResult {\n const { locale, fallbackLocale, locales } = state;\n const text =\n resolveNode(lookup(locales[locale]?.translations, key), locale, options?.params) ??\n resolveNode(\n lookup(locales[fallbackLocale]?.translations, key),\n fallbackLocale,\n options?.params,\n );\n if (text !== undefined) return { text: interpolate(text, options?.params), found: true };\n if (options?.fallback !== undefined)\n return { text: interpolate(options.fallback, options.params), found: false };\n return { text: key, found: false };\n}\n","import type { PluginContext } from '@embedpdf/core';\nimport { translate } from './translate';\nimport type { I18nAction, I18nCapability, I18nConfig, I18nState, LocaleInfo } from './types';\n\n/**\n * The i18n capability — selectors (pure reads over state) + intents (the only\n * writers). Built synchronously in `createKernel()`, so `t()` works before the\n * engine exists — including inside the viewer shell's loading UI.\n */\nexport function createI18nCapability(\n ctx: PluginContext<I18nState, I18nAction>,\n config: I18nConfig,\n): I18nCapability {\n // Dev signal, once per offender — a missing key otherwise fails silently\n // (by design: `t()` always returns a usable string).\n const warned = new Set<string>();\n const warnOnce = (id: string, message: string) => {\n if (warned.has(id)) return;\n warned.add(id);\n console.warn(message);\n };\n\n return {\n t: (key, options) => {\n const result = translate(ctx.getState(), key, options);\n if (!result.found && options?.fallback === undefined) {\n warnOnce(\n `key:${key}`,\n `[i18n] missing translation \"${key}\" (locale: ${ctx.getState().locale})`,\n );\n }\n return result.text;\n },\n locale: () => ctx.getState().locale,\n dir: () => {\n const { locales, locale } = ctx.getState();\n return locales[locale]?.dir ?? 'ltr';\n },\n locales: () => {\n const { locales } = ctx.getState();\n const known: LocaleInfo[] = Object.values(locales).map((locale) => ({\n code: locale.code,\n name: locale.name,\n dir: locale.dir ?? 'ltr',\n loaded: true,\n }));\n for (const code of Object.keys(config.loaders ?? {})) {\n if (!locales[code]) known.push({ code, name: code, dir: 'ltr', loaded: false });\n }\n return known;\n },\n loading: () => ctx.getState().loading,\n setLocale: (code) => {\n const state = ctx.getState();\n if (state.locales[code]) {\n ctx.dispatch({ type: 'I18N/SET_LOCALE', locale: code });\n } else if (config.loaders?.[code]) {\n ctx.dispatch({ type: 'I18N/LOAD_STARTED', locale: code });\n } else {\n warnOnce(\n `locale:${code}`,\n `[i18n] unknown locale \"${code}\" — not registered and no loader configured`,\n );\n }\n },\n registerLocale: (locale) => ctx.dispatch({ type: 'I18N/REGISTER_LOCALE', locale }),\n };\n}\n","import type { EffectContext } from '@embedpdf/core';\nimport type { I18nAction, I18nConfig, I18nState } from './types';\n\n/**\n * The plugin's ONE side-effect: fetching lazy locale packs. State-driven — a\n * load is `state.loading`, set by the capability (a switch to a lazy pack) or\n * by the initial state (a lazy startup locale), so a load requested before\n * effects ran is picked up here and nothing is lost to boot ordering.\n */\nexport function registerI18nEffects(config: I18nConfig) {\n return (ctx: EffectContext<I18nState, I18nAction>): void => {\n const load = (code: string) => {\n const loader = config.loaders?.[code];\n if (!loader) return; // unreachable via the capability; guards bad dispatches\n loader().then(\n (locale) => {\n ctx.dispatch({ type: 'I18N/REGISTER_LOCALE', locale });\n // Complete the switch only if this load is still the wanted one —\n // the user may have switched again while the pack was in flight.\n if (ctx.getState().loading === code) {\n ctx.dispatch({ type: 'I18N/SET_LOCALE', locale: locale.code });\n }\n },\n () => ctx.dispatch({ type: 'I18N/LOAD_FAILED', locale: code }),\n );\n };\n\n const pending = ctx.getState().loading;\n if (pending) load(pending);\n ctx.watch(\n () => ctx.getState().loading,\n (code) => {\n if (code) load(code);\n },\n );\n };\n}\n","import type { I18nAction, I18nConfig, I18nState, Locale } from './types';\n\n/**\n * Pure and serializable — locale packs are data, so they live HERE, not in a\n * side-table. One source of truth: `t()` is a pure function of this state and\n * reactivity rides the kernel's one change stream (no emitters).\n */\n\nexport function initialI18nState(config: I18nConfig): I18nState {\n const fallbackLocale = config.fallbackLocale ?? 'en';\n const locales: Record<string, Locale> = {};\n for (const locale of config.locales ?? []) locales[locale.code] = locale;\n const locale = config.locale ?? fallbackLocale;\n // A startup locale that is a lazy pack: show the fallback chain until the\n // pack arrives — seeding `loading` makes effects fetch it at startup.\n const needsLoad = !locales[locale] && config.loaders?.[locale] !== undefined;\n return { locale, fallbackLocale, locales, loading: needsLoad ? locale : null };\n}\n\nexport function i18nReducer(state: I18nState, action: I18nAction): I18nState {\n switch (action.type) {\n case 'I18N/SET_LOCALE': {\n // Only registered packs can become current — lazy ones arrive via\n // LOAD_STARTED → REGISTER_LOCALE → SET_LOCALE (effects drive the middle).\n if (!state.locales[action.locale]) return state;\n if (state.locale === action.locale && state.loading === null) return state;\n return { ...state, locale: action.locale, loading: null };\n }\n case 'I18N/REGISTER_LOCALE':\n return { ...state, locales: { ...state.locales, [action.locale.code]: action.locale } };\n case 'I18N/LOAD_STARTED':\n return state.loading === action.locale ? state : { ...state, loading: action.locale };\n case 'I18N/LOAD_FAILED':\n return state.loading === action.locale ? { ...state, loading: null } : state;\n default:\n return state;\n }\n}\n","import { createCapabilityToken } from '@embedpdf/core';\n\n/**\n * @embedpdf/plugin-i18n — the contract.\n *\n * Translation is pure data over pure state: locale packs live IN the store\n * (not in a side-table), so `t()` is a pure read, every consumer is reactive\n * through the one kernel change stream, and the whole thing serializes (SSR,\n * snapshots, persist). Workspace-scoped, `requires: []`, never touches the\n * engine or the DOM — the capability is alive from `createKernel()`, before\n * a single WASM byte is fetched. The plugin ships NO strings: mechanism here,\n * content (locale packs) in the product that embeds it.\n */\n\n/** Nested tree of translation strings. Leaves interpolate `{param}` slots. */\nexport interface TranslationDictionary {\n readonly [key: string]: string | TranslationDictionary;\n}\n\nexport interface Locale {\n /** BCP-47 code: 'en', 'es', 'ar', 'zh-Hans'. */\n readonly code: string;\n /** Native display name: 'Español' — what a locale switcher shows. */\n readonly name: string;\n /** Text direction for viewer chrome. Defaults to 'ltr'. */\n readonly dir?: 'ltr' | 'rtl';\n readonly translations: TranslationDictionary;\n}\n\nexport interface I18nState {\n readonly locale: string;\n readonly fallbackLocale: string;\n /** Registered packs by code — the lookup table `t()` reads. */\n readonly locales: Readonly<Record<string, Locale>>;\n /** Code of a lazy pack currently being fetched (drives switcher spinners). */\n readonly loading: string | null;\n}\n\nexport type I18nAction =\n /** Switch to a REGISTERED locale. The reducer ignores unregistered codes —\n * loading a lazy pack goes through LOAD_STARTED (the capability routes). */\n | { type: 'I18N/SET_LOCALE'; locale: string }\n | { type: 'I18N/REGISTER_LOCALE'; locale: Locale }\n | { type: 'I18N/LOAD_STARTED'; locale: string }\n | { type: 'I18N/LOAD_FAILED'; locale: string };\n\nexport interface I18nConfig {\n /**\n * Startup locale. Compute platform inputs OUTSIDE the plugin — it never\n * touches the DOM:\n *\n * ```ts\n * i18nPlugin({ locale: negotiateLocale(codes, navigator.languages) ?? 'en' })\n * ```\n *\n * May name a `loaders` pack: the fallback locale shows until the pack\n * arrives (effects fetch it at startup). Defaults to `fallbackLocale`.\n */\n locale?: string;\n /** The pack tried when a key misses the current locale. Default 'en'. */\n fallbackLocale?: string;\n /** Eagerly available packs. */\n locales?: Locale[];\n /**\n * Lazy packs: code → loader. `setLocale(code)` fetches on demand (in\n * effects), registers the pack, then switches. Until a lazy pack loads, a\n * locale switcher shows its code as the name — register eagerly (packs are\n * small) when you want native names in the switcher up front.\n */\n loaders?: Record<string, () => Promise<Locale>>;\n}\n\nexport interface TranslateOptions {\n /**\n * `{slot}` interpolation values. When `count` is a number and the key\n * resolves to a branch object, the branch is picked by CLDR plural\n * category (`Intl.PluralRules`), falling back to `other`:\n *\n * ```ts\n * // pages: { one: '{count} page', other: '{count} pages' }\n * t('pages', { params: { count: 1 } }) // '1 page'\n * ```\n */\n params?: Record<string, string | number>;\n /** Returned (interpolated) when the key misses every pack — instead of the key. */\n fallback?: string;\n}\n\n/** A locale as a switcher sees it — registered packs plus not-yet-loaded lazy ones. */\nexport interface LocaleInfo {\n readonly code: string;\n /** Native name; the bare code until a lazy pack has loaded. */\n readonly name: string;\n readonly dir: 'ltr' | 'rtl';\n readonly loaded: boolean;\n}\n\nexport interface I18nCapability {\n /** Translate a key: current locale → fallback locale → `options.fallback` → the key. */\n t(key: string, options?: TranslateOptions): string;\n locale(): string;\n /** Text direction of the CURRENT locale — wire to `dir=` on the shell. */\n dir(): 'ltr' | 'rtl';\n /** Every known locale (registered + lazy), in registration order. */\n locales(): LocaleInfo[];\n /** Code of the lazy pack being fetched right now, if any. */\n loading(): string | null;\n /** Switch locale; fetches the pack first when it's a lazy one. */\n setLocale(code: string): void;\n /** Register a pack at runtime (customer-supplied translations). */\n registerLocale(locale: Locale): void;\n}\n\nexport const I18nToken = createCapabilityToken<I18nCapability>('i18n');\n","import { definePlugin } from '@embedpdf/core';\nimport { createI18nCapability } from './capability';\nimport { registerI18nEffects } from './effects';\nimport { i18nReducer, initialI18nState } from './reducer';\nimport { I18nToken } from './types';\nimport type { I18nAction, I18nCapability, I18nConfig, I18nState } from './types';\n\n/**\n * The i18n plugin: workspace-scoped (locale is a workspace concern) with NO\n * dependencies — engine-free, DOM-free. Its capability is built synchronously\n * in `createKernel()`, so the shell translates from the first frame, while\n * the engine is still booting.\n */\nexport const i18nPlugin = (config: I18nConfig = {}) =>\n definePlugin<I18nState, I18nAction, I18nCapability>({\n id: 'i18n',\n scope: 'workspace',\n token: I18nToken,\n initialState: () => initialI18nState(config),\n reduce: i18nReducer,\n capability: (ctx) => createI18nCapability(ctx, config),\n effects: registerI18nEffects(config),\n });\n","/**\n * Pure BCP-47 locale negotiation — strings in, string out. The plugin never\n * reads the platform; the EMBEDDER feeds it whatever request list its\n * environment offers:\n *\n * ```ts\n * // browser shell: negotiateLocale(['en', 'es', 'ar'], navigator.languages)\n * // server render: negotiateLocale(codes, parseAcceptLanguage(header))\n * ```\n *\n * Matching, per requested code in preference order:\n * 1. exact match (case-insensitive): 'es-MX' → 'es-MX'\n * 2. request narrowed to its language: 'en-GB' → 'en'\n * 3. any available dialect of the language: 'zh' → 'zh-Hans'\n *\n * Returns the matched code exactly as it appears in `available`, or null.\n */\nexport function negotiateLocale(\n available: readonly string[],\n requested: readonly string[],\n): string | null {\n const byCanon = new Map<string, string>();\n for (const code of available) {\n const canon = code.toLowerCase();\n if (!byCanon.has(canon)) byCanon.set(canon, code);\n }\n const languageOf = (code: string) => code.toLowerCase().split('-')[0];\n\n for (const want of requested) {\n const exact = byCanon.get(want.toLowerCase());\n if (exact !== undefined) return exact;\n }\n for (const want of requested) {\n const narrowed = byCanon.get(languageOf(want));\n if (narrowed !== undefined) return narrowed;\n }\n for (const want of requested) {\n const language = languageOf(want);\n for (const [canon, original] of byCanon) {\n if (languageOf(canon) === language) return original;\n }\n }\n return null;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../src/translate.ts","../src/capability.ts","../src/effects.ts","../src/reducer.ts","../src/i18n.plugin.ts","../src/negotiate.ts"],"sourcesContent":["import type { I18nState, TranslateOptions, TranslationDictionary } from './types';\n\n/**\n * The pure lookup core — state in, string out. No platform access anywhere\n * (`Intl.PluralRules` is core ECMAScript, not DOM); this is the part that\n * ports to Rust verbatim.\n */\n\n/** Walk a dotted path ('commands.zoom.in') through a dictionary tree. */\nfunction lookup(\n dict: TranslationDictionary | undefined,\n key: string,\n): string | TranslationDictionary | undefined {\n if (!dict) return undefined;\n let node: string | TranslationDictionary | undefined = dict;\n for (const part of key.split('.')) {\n if (node === undefined || typeof node === 'string') return undefined;\n node = node[part];\n }\n return node;\n}\n\n/** CLDR plural category for a count, with a safe fallback when the runtime\n * lacks ICU data for the locale. */\nfunction pluralCategory(locale: string, count: number): string {\n try {\n return new Intl.PluralRules(locale).select(count);\n } catch {\n return count === 1 ? 'one' : 'other';\n }\n}\n\n/** A leaf hit is a string; a branch object needs a plural pick via `count`. */\nfunction resolveNode(\n node: string | TranslationDictionary | undefined,\n locale: string,\n params?: Record<string, string | number>,\n): string | undefined {\n if (typeof node === 'string') return node;\n if (node !== undefined && typeof params?.count === 'number') {\n const branch = node[pluralCategory(locale, params.count)] ?? node['other'];\n if (typeof branch === 'string') return branch;\n }\n return undefined;\n}\n\n/** Replace `{slot}` markers; unknown slots stay verbatim (visible in QA). */\nexport function interpolate(text: string, params?: Record<string, string | number>): string {\n if (!params) return text;\n return text.replace(/\\{(\\w+)\\}/g, (match, slot) =>\n params[slot] !== undefined ? String(params[slot]) : match,\n );\n}\n\nexport interface TranslateResult {\n readonly text: string;\n /** False when the key missed every pack (text is then the fallback or the key). */\n readonly found: boolean;\n}\n\n/**\n * Resolve a key against the state: current locale → fallback locale →\n * `options.fallback` (interpolated too) → the key itself.\n */\nexport function translate(\n state: I18nState,\n key: string,\n options?: TranslateOptions,\n): TranslateResult {\n const { locale, fallbackLocale, locales } = state;\n const text =\n resolveNode(lookup(locales[locale]?.translations, key), locale, options?.params) ??\n resolveNode(\n lookup(locales[fallbackLocale]?.translations, key),\n fallbackLocale,\n options?.params,\n );\n if (text !== undefined) return { text: interpolate(text, options?.params), found: true };\n if (options?.fallback !== undefined)\n return { text: interpolate(options.fallback, options.params), found: false };\n return { text: key, found: false };\n}\n","import type { PluginContext } from '@embedpdf/core';\nimport { translate } from './translate';\nimport type { I18nAction, I18nCapability, I18nConfig, I18nState, LocaleInfo } from './types';\n\n/**\n * The i18n capability — selectors (pure reads over state) + intents (the only\n * writers). Built synchronously in `createKernel()`, so `t()` works before the\n * engine exists — including inside the viewer shell's loading UI.\n */\nexport function createI18nCapability(\n ctx: PluginContext<I18nState, I18nAction>,\n config: I18nConfig,\n): I18nCapability {\n // Dev signal, once per offender — a missing key otherwise fails silently\n // (by design: `t()` always returns a usable string).\n const warned = new Set<string>();\n const warnOnce = (id: string, message: string) => {\n if (warned.has(id)) return;\n warned.add(id);\n console.warn(message);\n };\n\n return {\n t: (key, options) => {\n const result = translate(ctx.getState(), key, options);\n if (!result.found && options?.fallback === undefined) {\n warnOnce(\n `key:${key}`,\n `[i18n] missing translation \"${key}\" (locale: ${ctx.getState().locale})`,\n );\n }\n return result.text;\n },\n locale: () => ctx.getState().locale,\n dir: () => {\n const { locales, locale } = ctx.getState();\n return locales[locale]?.dir ?? 'ltr';\n },\n locales: () => {\n const { locales } = ctx.getState();\n const known: LocaleInfo[] = Object.values(locales).map((locale) => ({\n code: locale.code,\n name: locale.name,\n dir: locale.dir ?? 'ltr',\n loaded: true,\n }));\n for (const code of Object.keys(config.loaders ?? {})) {\n if (!locales[code]) known.push({ code, name: code, dir: 'ltr', loaded: false });\n }\n return known;\n },\n loading: () => ctx.getState().loading,\n setLocale: (code) => {\n const state = ctx.getState();\n if (state.locales[code]) {\n ctx.dispatch({ type: 'I18N/SET_LOCALE', locale: code });\n } else if (config.loaders?.[code]) {\n ctx.dispatch({ type: 'I18N/LOAD_STARTED', locale: code });\n } else {\n warnOnce(\n `locale:${code}`,\n `[i18n] unknown locale \"${code}\" — not registered and no loader configured`,\n );\n }\n },\n registerLocale: (locale) => ctx.dispatch({ type: 'I18N/REGISTER_LOCALE', locale }),\n };\n}\n","import type { EffectContext } from '@embedpdf/core';\nimport type { I18nAction, I18nConfig, I18nState } from './types';\n\n/**\n * The plugin's ONE side-effect: fetching lazy locale packs. State-driven — a\n * load is `state.loading`, set by the capability (a switch to a lazy pack) or\n * by the initial state (a lazy startup locale), so a load requested before\n * effects ran is picked up here and nothing is lost to boot ordering.\n */\nexport function registerI18nEffects(config: I18nConfig) {\n return (ctx: EffectContext<I18nState, I18nAction>): void => {\n const load = (code: string) => {\n const loader = config.loaders?.[code];\n if (!loader) return; // unreachable via the capability; guards bad dispatches\n loader().then(\n (locale) => {\n ctx.dispatch({ type: 'I18N/REGISTER_LOCALE', locale });\n // Complete the switch only if this load is still the wanted one —\n // the user may have switched again while the pack was in flight.\n if (ctx.getState().loading === code) {\n ctx.dispatch({ type: 'I18N/SET_LOCALE', locale: locale.code });\n }\n },\n () => ctx.dispatch({ type: 'I18N/LOAD_FAILED', locale: code }),\n );\n };\n\n const pending = ctx.getState().loading;\n if (pending) load(pending);\n ctx.watch(\n () => ctx.getState().loading,\n (code) => {\n if (code) load(code);\n },\n );\n };\n}\n","import type { I18nAction, I18nConfig, I18nState, Locale } from './types';\n\n/**\n * Pure and serializable — locale packs are data, so they live HERE, not in a\n * side-table. One source of truth: `t()` is a pure function of this state and\n * reactivity rides the kernel's one change stream (no emitters).\n */\n\nexport function initialI18nState(config: I18nConfig): I18nState {\n const fallbackLocale = config.fallbackLocale ?? 'en';\n const locales: Record<string, Locale> = {};\n for (const locale of config.locales ?? []) locales[locale.code] = locale;\n const locale = config.locale ?? fallbackLocale;\n // A startup locale that is a lazy pack: show the fallback chain until the\n // pack arrives — seeding `loading` makes effects fetch it at startup.\n const needsLoad = !locales[locale] && config.loaders?.[locale] !== undefined;\n return { locale, fallbackLocale, locales, loading: needsLoad ? locale : null };\n}\n\nexport function i18nReducer(state: I18nState, action: I18nAction): I18nState {\n switch (action.type) {\n case 'I18N/SET_LOCALE': {\n // Only registered packs can become current — lazy ones arrive via\n // LOAD_STARTED → REGISTER_LOCALE → SET_LOCALE (effects drive the middle).\n if (!state.locales[action.locale]) return state;\n if (state.locale === action.locale && state.loading === null) return state;\n return { ...state, locale: action.locale, loading: null };\n }\n case 'I18N/REGISTER_LOCALE':\n return { ...state, locales: { ...state.locales, [action.locale.code]: action.locale } };\n case 'I18N/LOAD_STARTED':\n return state.loading === action.locale ? state : { ...state, loading: action.locale };\n case 'I18N/LOAD_FAILED':\n return state.loading === action.locale ? { ...state, loading: null } : state;\n default:\n return state;\n }\n}\n","import { definePlugin } from '@embedpdf/core';\nimport { createI18nCapability } from './capability';\nimport { registerI18nEffects } from './effects';\nimport { i18nReducer, initialI18nState } from './reducer';\nimport { I18nToken } from './types';\nimport type { I18nAction, I18nCapability, I18nConfig, I18nState } from './types';\n\n/**\n * The i18n plugin: workspace-scoped (locale is a workspace concern) with NO\n * dependencies — engine-free, DOM-free. Its capability is built synchronously\n * in `createKernel()`, so the shell translates from the first frame, while\n * the engine is still booting.\n */\nexport const i18nPlugin = (config: I18nConfig = {}) =>\n definePlugin<I18nState, I18nAction, I18nCapability>({\n id: 'i18n',\n scope: 'workspace',\n token: I18nToken,\n initialState: () => initialI18nState(config),\n reduce: i18nReducer,\n capability: (ctx) => createI18nCapability(ctx, config),\n effects: registerI18nEffects(config),\n });\n","/**\n * Pure BCP-47 locale negotiation — strings in, string out. The plugin never\n * reads the platform; the EMBEDDER feeds it whatever request list its\n * environment offers:\n *\n * ```ts\n * // browser shell: negotiateLocale(['en', 'es', 'ar'], navigator.languages)\n * // server render: negotiateLocale(codes, parseAcceptLanguage(header))\n * ```\n *\n * Matching, per requested code in preference order:\n * 1. exact match (case-insensitive): 'es-MX' → 'es-MX'\n * 2. request narrowed to its language: 'en-GB' → 'en'\n * 3. any available dialect of the language: 'zh' → 'zh-Hans'\n *\n * Returns the matched code exactly as it appears in `available`, or null.\n */\nexport function negotiateLocale(\n available: readonly string[],\n requested: readonly string[],\n): string | null {\n const byCanon = new Map<string, string>();\n for (const code of available) {\n const canon = code.toLowerCase();\n if (!byCanon.has(canon)) byCanon.set(canon, code);\n }\n const languageOf = (code: string) => code.toLowerCase().split('-')[0];\n\n for (const want of requested) {\n const exact = byCanon.get(want.toLowerCase());\n if (exact !== undefined) return exact;\n }\n for (const want of requested) {\n const narrowed = byCanon.get(languageOf(want));\n if (narrowed !== undefined) return narrowed;\n }\n for (const want of requested) {\n const language = languageOf(want);\n for (const [canon, original] of byCanon) {\n if (languageOf(canon) === language) return original;\n }\n }\n return null;\n}\n"],"mappings":";;;;;;;;;AASA,SAAS,OACP,MACA,KAC4C;CAC5C,IAAI,CAAC,MAAM,OAAO,KAAA;CAClB,IAAI,OAAmD;CACvD,KAAK,MAAM,QAAQ,IAAI,MAAM,GAAG,GAAG;EACjC,IAAI,SAAS,KAAA,KAAa,OAAO,SAAS,UAAU,OAAO,KAAA;EAC3D,OAAO,KAAK;CACd;CACA,OAAO;AACT;;;AAIA,SAAS,eAAe,QAAgB,OAAuB;CAC7D,IAAI;EACF,OAAO,IAAI,KAAK,YAAY,MAAM,CAAC,CAAC,OAAO,KAAK;CAClD,QAAQ;EACN,OAAO,UAAU,IAAI,QAAQ;CAC/B;AACF;;AAGA,SAAS,YACP,MACA,QACA,QACoB;CACpB,IAAI,OAAO,SAAS,UAAU,OAAO;CACrC,IAAI,SAAS,KAAA,KAAa,OAAO,QAAQ,UAAU,UAAU;EAC3D,MAAM,SAAS,KAAK,eAAe,QAAQ,OAAO,KAAK,MAAM,KAAK;EAClE,IAAI,OAAO,WAAW,UAAU,OAAO;CACzC;AAEF;;AAGA,SAAgB,YAAY,MAAc,QAAkD;CAC1F,IAAI,CAAC,QAAQ,OAAO;CACpB,OAAO,KAAK,QAAQ,eAAe,OAAO,SACxC,OAAO,UAAU,KAAA,IAAY,OAAO,OAAO,KAAK,IAAI,KACtD;AACF;;;;;AAYA,SAAgB,UACd,OACA,KACA,SACiB;CACjB,MAAM,EAAE,QAAQ,gBAAgB,YAAY;CAC5C,MAAM,OACJ,YAAY,OAAO,QAAQ,OAAO,EAAE,cAAc,GAAG,GAAG,QAAQ,SAAS,MAAM,KAC/E,YACE,OAAO,QAAQ,eAAe,EAAE,cAAc,GAAG,GACjD,gBACA,SAAS,MACX;CACF,IAAI,SAAS,KAAA,GAAW,OAAO;EAAE,MAAM,YAAY,MAAM,SAAS,MAAM;EAAG,OAAO;CAAK;CACvF,IAAI,SAAS,aAAa,KAAA,GACxB,OAAO;EAAE,MAAM,YAAY,QAAQ,UAAU,QAAQ,MAAM;EAAG,OAAO;CAAM;CAC7E,OAAO;EAAE,MAAM;EAAK,OAAO;CAAM;AACnC;;;;;;;;ACxEA,SAAgB,qBACd,KACA,QACgB;CAGhB,MAAM,yBAAS,IAAI,IAAY;CAC/B,MAAM,YAAY,IAAY,YAAoB;EAChD,IAAI,OAAO,IAAI,EAAE,GAAG;EACpB,OAAO,IAAI,EAAE;EACb,QAAQ,KAAK,OAAO;CACtB;CAEA,OAAO;EACL,IAAI,KAAK,YAAY;GACnB,MAAM,SAAS,UAAU,IAAI,SAAS,GAAG,KAAK,OAAO;GACrD,IAAI,CAAC,OAAO,SAAS,SAAS,aAAa,KAAA,GACzC,SACE,OAAO,OACP,+BAA+B,IAAI,aAAa,IAAI,SAAS,CAAC,CAAC,OAAO,EACxE;GAEF,OAAO,OAAO;EAChB;EACA,cAAc,IAAI,SAAS,CAAC,CAAC;EAC7B,WAAW;GACT,MAAM,EAAE,SAAS,WAAW,IAAI,SAAS;GACzC,OAAO,QAAQ,OAAO,EAAE,OAAO;EACjC;EACA,eAAe;GACb,MAAM,EAAE,YAAY,IAAI,SAAS;GACjC,MAAM,QAAsB,OAAO,OAAO,OAAO,CAAC,CAAC,KAAK,YAAY;IAClE,MAAM,OAAO;IACb,MAAM,OAAO;IACb,KAAK,OAAO,OAAO;IACnB,QAAQ;GACV,EAAE;GACF,KAAK,MAAM,QAAQ,OAAO,KAAK,OAAO,WAAW,CAAC,CAAC,GACjD,IAAI,CAAC,QAAQ,OAAO,MAAM,KAAK;IAAE;IAAM,MAAM;IAAM,KAAK;IAAO,QAAQ;GAAM,CAAC;GAEhF,OAAO;EACT;EACA,eAAe,IAAI,SAAS,CAAC,CAAC;EAC9B,YAAY,SAAS;GAEnB,IADc,IAAI,SACV,CAAC,CAAC,QAAQ,OAChB,IAAI,SAAS;IAAE,MAAM;IAAmB,QAAQ;GAAK,CAAC;QACjD,IAAI,OAAO,UAAU,OAC1B,IAAI,SAAS;IAAE,MAAM;IAAqB,QAAQ;GAAK,CAAC;QAExD,SACE,UAAU,QACV,0BAA0B,KAAK,4CACjC;EAEJ;EACA,iBAAiB,WAAW,IAAI,SAAS;GAAE,MAAM;GAAwB;EAAO,CAAC;CACnF;AACF;;;;;;;;;AC1DA,SAAgB,oBAAoB,QAAoB;CACtD,QAAQ,QAAoD;EAC1D,MAAM,QAAQ,SAAiB;GAC7B,MAAM,SAAS,OAAO,UAAU;GAChC,IAAI,CAAC,QAAQ;GACb,OAAO,CAAC,CAAC,MACN,WAAW;IACV,IAAI,SAAS;KAAE,MAAM;KAAwB;IAAO,CAAC;IAGrD,IAAI,IAAI,SAAS,CAAC,CAAC,YAAY,MAC7B,IAAI,SAAS;KAAE,MAAM;KAAmB,QAAQ,OAAO;IAAK,CAAC;GAEjE,SACM,IAAI,SAAS;IAAE,MAAM;IAAoB,QAAQ;GAAK,CAAC,CAC/D;EACF;EAEA,MAAM,UAAU,IAAI,SAAS,CAAC,CAAC;EAC/B,IAAI,SAAS,KAAK,OAAO;EACzB,IAAI,YACI,IAAI,SAAS,CAAC,CAAC,UACpB,SAAS;GACR,IAAI,MAAM,KAAK,IAAI;EACrB,CACF;CACF;AACF;;;;;;;;AC5BA,SAAgB,iBAAiB,QAA+B;CAC9D,MAAM,iBAAiB,OAAO,kBAAkB;CAChD,MAAM,UAAkC,CAAC;CACzC,KAAK,MAAM,UAAU,OAAO,WAAW,CAAC,GAAG,QAAQ,OAAO,QAAQ;CAClE,MAAM,SAAS,OAAO,UAAU;CAIhC,OAAO;EAAE;EAAQ;EAAgB;EAAS,SADxB,CAAC,QAAQ,WAAW,OAAO,UAAU,YAAY,KAAA,IACJ,SAAS;CAAK;AAC/E;AAEA,SAAgB,YAAY,OAAkB,QAA+B;CAC3E,QAAQ,OAAO,MAAf;EACE,KAAK;GAGH,IAAI,CAAC,MAAM,QAAQ,OAAO,SAAS,OAAO;GAC1C,IAAI,MAAM,WAAW,OAAO,UAAU,MAAM,YAAY,MAAM,OAAO;GACrE,OAAO;IAAE,GAAG;IAAO,QAAQ,OAAO;IAAQ,SAAS;GAAK;EAE1D,KAAK,wBACH,OAAO;GAAE,GAAG;GAAO,SAAS;IAAE,GAAG,MAAM;KAAU,OAAO,OAAO,OAAO,OAAO;GAAO;EAAE;EACxF,KAAK,qBACH,OAAO,MAAM,YAAY,OAAO,SAAS,QAAQ;GAAE,GAAG;GAAO,SAAS,OAAO;EAAO;EACtF,KAAK,oBACH,OAAO,MAAM,YAAY,OAAO,SAAS;GAAE,GAAG;GAAO,SAAS;EAAK,IAAI;EACzE,SACE,OAAO;CACX;AACF;;;;;;;;;ACxBA,MAAa,cAAc,SAAqB,CAAC,MAC/C,aAAoD;CAClD,IAAI;CACJ,OAAO;CACP,OAAO;CACP,oBAAoB,iBAAiB,MAAM;CAC3C,QAAQ;CACR,aAAa,QAAQ,qBAAqB,KAAK,MAAM;CACrD,SAAS,oBAAoB,MAAM;AACrC,CAAC;;;;;;;;;;;;;;;;;;;;ACLH,SAAgB,gBACd,WACA,WACe;CACf,MAAM,0BAAU,IAAI,IAAoB;CACxC,KAAK,MAAM,QAAQ,WAAW;EAC5B,MAAM,QAAQ,KAAK,YAAY;EAC/B,IAAI,CAAC,QAAQ,IAAI,KAAK,GAAG,QAAQ,IAAI,OAAO,IAAI;CAClD;CACA,MAAM,cAAc,SAAiB,KAAK,YAAY,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC;CAEnE,KAAK,MAAM,QAAQ,WAAW;EAC5B,MAAM,QAAQ,QAAQ,IAAI,KAAK,YAAY,CAAC;EAC5C,IAAI,UAAU,KAAA,GAAW,OAAO;CAClC;CACA,KAAK,MAAM,QAAQ,WAAW;EAC5B,MAAM,WAAW,QAAQ,IAAI,WAAW,IAAI,CAAC;EAC7C,IAAI,aAAa,KAAA,GAAW,OAAO;CACrC;CACA,KAAK,MAAM,QAAQ,WAAW;EAC5B,MAAM,WAAW,WAAW,IAAI;EAChC,KAAK,MAAM,CAAC,OAAO,aAAa,SAC9B,IAAI,WAAW,KAAK,MAAM,UAAU,OAAO;CAE/C;CACA,OAAO;AACT"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@embedpdf/plugin-i18n",
|
|
3
|
-
"version": "3.0.0-next.
|
|
3
|
+
"version": "3.0.0-next.12",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Workspace-scoped translation: locale packs as store state (pure lookup, reactive for free), lazy pack loading via effects, pure BCP-47 negotiation. Engine-free and DOM-free — alive before the engine boots.",
|
|
6
6
|
"type": "module",
|
|
@@ -12,13 +12,17 @@
|
|
|
12
12
|
"import": "./dist/index.js",
|
|
13
13
|
"require": "./dist/index.cjs"
|
|
14
14
|
},
|
|
15
|
+
"./contract": {
|
|
16
|
+
"import": "./dist/contract.js",
|
|
17
|
+
"require": "./dist/contract.cjs"
|
|
18
|
+
},
|
|
15
19
|
"./package.json": "./package.json"
|
|
16
20
|
},
|
|
17
21
|
"files": [
|
|
18
22
|
"dist"
|
|
19
23
|
],
|
|
20
24
|
"dependencies": {
|
|
21
|
-
"@embedpdf/core": "3.0.0-next.
|
|
25
|
+
"@embedpdf/core": "3.0.0-next.12"
|
|
22
26
|
},
|
|
23
27
|
"devDependencies": {
|
|
24
28
|
"typescript": "^5.0.0",
|