@entifix/i18n 0.1.1 → 0.1.3
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/README.md +2 -0
- package/dist/catalog-shape.d.ts +12 -0
- package/dist/catalog-shape.d.ts.map +1 -0
- package/dist/catalog-shape.js +10 -0
- package/dist/catalog-shape.js.map +1 -0
- package/dist/fill.d.ts +10 -0
- package/dist/fill.d.ts.map +1 -0
- package/dist/fill.js +12 -0
- package/dist/fill.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/instance.d.ts +2 -2
- package/dist/instance.js +2 -2
- package/dist/instance.js.map +1 -1
- package/package.json +2 -5
package/README.md
CHANGED
|
@@ -12,6 +12,8 @@ pnpm add @entifix/i18n i18next
|
|
|
12
12
|
|
|
13
13
|
## Entry points
|
|
14
14
|
|
|
15
|
+
`fill` fills `{{name}}` placeholders of a string already translated, and `CatalogShape<T>` is the type a second locale's catalog `satisfies`.
|
|
16
|
+
|
|
15
17
|
| Import | What it holds |
|
|
16
18
|
| ----------------------- | ------------------------------------------------------------------------------------------------ |
|
|
17
19
|
| `@entifix/i18n` | `defineCatalogs`, `createI18n`, `getServerTFor` |
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The same keys as `T`, every leaf a string.
|
|
3
|
+
*
|
|
4
|
+
* A second locale's catalog is declared `satisfies CatalogShape<typeof en>`,
|
|
5
|
+
* and `satisfies` checks an object literal both ways: a key missing from it
|
|
6
|
+
* and a key only it has are each a type error, so a catalog cannot drift from
|
|
7
|
+
* the one it translates without failing the build.
|
|
8
|
+
*/
|
|
9
|
+
export type CatalogShape<T> = {
|
|
10
|
+
readonly [K in keyof T]: T[K] extends string ? string : CatalogShape<T[K]>;
|
|
11
|
+
};
|
|
12
|
+
//# sourceMappingURL=catalog-shape.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"catalog-shape.d.ts","sourceRoot":"","sources":["../src/catalog-shape.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,MAAM,MAAM,YAAY,CAAC,CAAC,IAAI;IAC5B,QAAQ,EAAE,CAAC,IAAI,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,SAAS,MAAM,GAAG,MAAM,GAAG,YAAY,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;CAC3E,CAAC"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The same keys as `T`, every leaf a string.
|
|
3
|
+
*
|
|
4
|
+
* A second locale's catalog is declared `satisfies CatalogShape<typeof en>`,
|
|
5
|
+
* and `satisfies` checks an object literal both ways: a key missing from it
|
|
6
|
+
* and a key only it has are each a type error, so a catalog cannot drift from
|
|
7
|
+
* the one it translates without failing the build.
|
|
8
|
+
*/ export { };
|
|
9
|
+
|
|
10
|
+
//# sourceMappingURL=catalog-shape.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/catalog-shape.ts"],"sourcesContent":["/**\n * The same keys as `T`, every leaf a string.\n *\n * A second locale's catalog is declared `satisfies CatalogShape<typeof en>`,\n * and `satisfies` checks an object literal both ways: a key missing from it\n * and a key only it has are each a type error, so a catalog cannot drift from\n * the one it translates without failing the build.\n */\nexport type CatalogShape<T> = {\n readonly [K in keyof T]: T[K] extends string ? string : CatalogShape<T[K]>;\n};\n"],"names":[],"mappings":"AAAA;;;;;;;CAOC,GACD,WAEE"}
|
package/dist/fill.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A catalog string with its `{{name}}` placeholders filled in, for copy a
|
|
3
|
+
* component receives already translated: `fill('{{n}} active', { n: 2 })`.
|
|
4
|
+
* A placeholder with no value stays as written.
|
|
5
|
+
*
|
|
6
|
+
* Pure, and free of i18next: a client component can fill a string the server
|
|
7
|
+
* translated without installing a catalog.
|
|
8
|
+
*/
|
|
9
|
+
export declare function fill(template: string, values: Readonly<Record<string, string | number>>): string;
|
|
10
|
+
//# sourceMappingURL=fill.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fill.d.ts","sourceRoot":"","sources":["../src/fill.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,wBAAgB,IAAI,CAClB,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,CAAC,GAChD,MAAM,CAIR"}
|
package/dist/fill.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A catalog string with its `{{name}}` placeholders filled in, for copy a
|
|
3
|
+
* component receives already translated: `fill('{{n}} active', { n: 2 })`.
|
|
4
|
+
* A placeholder with no value stays as written.
|
|
5
|
+
*
|
|
6
|
+
* Pure, and free of i18next: a client component can fill a string the server
|
|
7
|
+
* translated without installing a catalog.
|
|
8
|
+
*/ export function fill(template, values) {
|
|
9
|
+
return template.replace(/\{\{(\w+)\}\}/g, (placeholder, name)=>name in values ? String(values[name]) : placeholder);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
//# sourceMappingURL=fill.js.map
|
package/dist/fill.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/fill.ts"],"sourcesContent":["/**\n * A catalog string with its `{{name}}` placeholders filled in, for copy a\n * component receives already translated: `fill('{{n}} active', { n: 2 })`.\n * A placeholder with no value stays as written.\n *\n * Pure, and free of i18next: a client component can fill a string the server\n * translated without installing a catalog.\n */\nexport function fill(\n template: string,\n values: Readonly<Record<string, string | number>>,\n): string {\n return template.replace(/\\{\\{(\\w+)\\}\\}/g, (placeholder, name: string) =>\n name in values ? String(values[name]) : placeholder,\n );\n}\n"],"names":["fill","template","values","replace","placeholder","name","String"],"mappings":"AAAA;;;;;;;CAOC,GACD,OAAO,SAASA,KACdC,QAAgB,EAChBC,MAAiD;IAEjD,OAAOD,SAASE,OAAO,CAAC,kBAAkB,CAACC,aAAaC,OACtDA,QAAQH,SAASI,OAAOJ,MAAM,CAACG,KAAK,IAAID;AAE5C"}
|
package/dist/index.d.ts
CHANGED
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,eAAe,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,oBAAoB,CAAC;AACnC,cAAc,WAAW,CAAC;AAC1B,cAAc,eAAe,CAAC"}
|
package/dist/index.js
CHANGED
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["export * from './instance.js';\n"],"names":[],"mappings":"AAAA,cAAc,gBAAgB"}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["export * from './catalog-shape.js';\nexport * from './fill.js';\nexport * from './instance.js';\n"],"names":[],"mappings":"AAAA,cAAc,qBAAqB;AACnC,cAAc,YAAY;AAC1B,cAAc,gBAAgB"}
|
package/dist/instance.d.ts
CHANGED
|
@@ -57,8 +57,8 @@ export declare function createI18n(locale: Locale, modules?: readonly ThirdParty
|
|
|
57
57
|
* request-reading counterpart is `getServerT` in `@entifix/next-i18n`,
|
|
58
58
|
* which re-exports this one for symmetry at app call sites.
|
|
59
59
|
*
|
|
60
|
-
* It lives here rather than in the Next i18n shell because
|
|
61
|
-
*
|
|
60
|
+
* It lives here rather than in the Next i18n shell because the shells share
|
|
61
|
+
* T4 and the tier register declares no edge between them — and the storefront
|
|
62
62
|
* shell, which has a real `[locale]` segment and therefore never reads a
|
|
63
63
|
* request, is exactly the caller that needs it.
|
|
64
64
|
*/
|
package/dist/instance.js
CHANGED
|
@@ -66,8 +66,8 @@ const required = ()=>{
|
|
|
66
66
|
* request-reading counterpart is `getServerT` in `@entifix/next-i18n`,
|
|
67
67
|
* which re-exports this one for symmetry at app call sites.
|
|
68
68
|
*
|
|
69
|
-
* It lives here rather than in the Next i18n shell because
|
|
70
|
-
*
|
|
69
|
+
* It lives here rather than in the Next i18n shell because the shells share
|
|
70
|
+
* T4 and the tier register declares no edge between them — and the storefront
|
|
71
71
|
* shell, which has a real `[locale]` segment and therefore never reads a
|
|
72
72
|
* request, is exactly the caller that needs it.
|
|
73
73
|
*/ export function getServerTFor(locale, ns) {
|
package/dist/instance.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/instance.ts"],"sourcesContent":["import { DEFAULT_LOCALE, EntifixBuildError, type Locale } from '@entifix/core';\nimport i18next, {\n type i18n,\n type Namespace,\n type ThirdPartyModule,\n} from 'i18next';\n\n/** One locale's catalogs, keyed by namespace. */\nexport type LocaleCatalogs = Readonly<Record<string, object>>;\n\n/**\n * Every catalog an application renders: entifix's framework namespaces plus the\n * host's own, already merged.\n *\n * ⚠️ **Supplied by the host, never assembled here.** This package used to import\n * five namespaces directly, two of which were r10c product copy — `'r10c Admin'`\n * shipped inside the framework. Taking them as a value is what lets two\n * applications with different copy share one i18next setup.\n */\nexport interface I18nCatalogs {\n readonly resources: Readonly<Record<string, LocaleCatalogs>>;\n readonly namespaces: readonly string[];\n readonly defaultNS: string;\n}\n\nlet installed: I18nCatalogs | undefined;\n\n/**\n * Hand this package the application's catalogs. Call it once, from a module\n * every entry point imports.\n *\n * ⚠️ **A registry rather than an argument, and the reason is the call site.**\n * Every other seam in entifix takes its value at a composition root — a `Layer`,\n * a React provider, a hook option. `getServerT` has neither: it is called\n * directly inside React server components scattered across an application, with\n * nothing in between to thread a parameter through. So the seam is module\n * initialization, and the failure mode is made loud below rather than silent.\n *\n * Locale *routing* needs none of this. `@entifix/core`'s locale helpers carry\n * no catalogs, so Next middleware reads a cookie and a header without ever\n * loading i18next or a single translation.\n */\nexport function defineCatalogs(catalogs: I18nCatalogs): void {\n installed = catalogs;\n}\n\nconst required = (): I18nCatalogs => {\n if (installed === undefined) {\n throw new EntifixBuildError(\n 'No i18n catalogs are installed. Call `defineCatalogs({ resources, ' +\n 'namespaces, defaultNS })` once from a module every entry point ' +\n 'imports, before the first server render.',\n );\n }\n return installed;\n};\n\n/**\n * Builds an isolated i18next instance for one locale.\n *\n * Always an *instance*, never the `i18next` singleton: a server renders several\n * locales concurrently, and a shared singleton would let one request's `lng`\n * leak into another's markup.\n *\n * Catalogs are bundled rather than fetched, so `init` completes synchronously\n * (`initAsync: false`) and the first paint is already translated — no Suspense\n * boundary, no flash of untranslated keys.\n *\n * `modules` is how the React binding passes `initReactI18next` without this\n * package taking a dependency on React.\n */\nexport function createI18n(\n locale: Locale,\n modules: readonly ThirdPartyModule[] = [],\n): i18n {\n const catalogs = required();\n const instance = modules.reduce<i18n>(\n (acc, module) => acc.use(module),\n i18next.createInstance(),\n );\n\n instance.init({\n lng: locale,\n fallbackLng: DEFAULT_LOCALE,\n ns: [...catalogs.namespaces],\n defaultNS: catalogs.defaultNS,\n resources: catalogs.resources,\n initAsync: false,\n interpolation: {\n // React escapes for us; double-escaping turns \"Café\" into \"Café\".\n escapeValue: false,\n },\n });\n\n return instance;\n}\n\n/**\n * A translate function for a locale the caller already holds — a route param,\n * typically — bound to one namespace.\n *\n * Request-free by construction, and that is the point: a component built on it\n * touches neither `headers()` nor `cookies()`, so the page containing it stays\n * statically renderable and can be prerendered once per locale. Its\n * request-reading counterpart is `getServerT` in `@entifix/next-i18n`,\n * which re-exports this one for symmetry at app call sites.\n *\n * It lives here rather than in the Next i18n shell because
|
|
1
|
+
{"version":3,"sources":["../src/instance.ts"],"sourcesContent":["import { DEFAULT_LOCALE, EntifixBuildError, type Locale } from '@entifix/core';\nimport i18next, {\n type i18n,\n type Namespace,\n type ThirdPartyModule,\n} from 'i18next';\n\n/** One locale's catalogs, keyed by namespace. */\nexport type LocaleCatalogs = Readonly<Record<string, object>>;\n\n/**\n * Every catalog an application renders: entifix's framework namespaces plus the\n * host's own, already merged.\n *\n * ⚠️ **Supplied by the host, never assembled here.** This package used to import\n * five namespaces directly, two of which were r10c product copy — `'r10c Admin'`\n * shipped inside the framework. Taking them as a value is what lets two\n * applications with different copy share one i18next setup.\n */\nexport interface I18nCatalogs {\n readonly resources: Readonly<Record<string, LocaleCatalogs>>;\n readonly namespaces: readonly string[];\n readonly defaultNS: string;\n}\n\nlet installed: I18nCatalogs | undefined;\n\n/**\n * Hand this package the application's catalogs. Call it once, from a module\n * every entry point imports.\n *\n * ⚠️ **A registry rather than an argument, and the reason is the call site.**\n * Every other seam in entifix takes its value at a composition root — a `Layer`,\n * a React provider, a hook option. `getServerT` has neither: it is called\n * directly inside React server components scattered across an application, with\n * nothing in between to thread a parameter through. So the seam is module\n * initialization, and the failure mode is made loud below rather than silent.\n *\n * Locale *routing* needs none of this. `@entifix/core`'s locale helpers carry\n * no catalogs, so Next middleware reads a cookie and a header without ever\n * loading i18next or a single translation.\n */\nexport function defineCatalogs(catalogs: I18nCatalogs): void {\n installed = catalogs;\n}\n\nconst required = (): I18nCatalogs => {\n if (installed === undefined) {\n throw new EntifixBuildError(\n 'No i18n catalogs are installed. Call `defineCatalogs({ resources, ' +\n 'namespaces, defaultNS })` once from a module every entry point ' +\n 'imports, before the first server render.',\n );\n }\n return installed;\n};\n\n/**\n * Builds an isolated i18next instance for one locale.\n *\n * Always an *instance*, never the `i18next` singleton: a server renders several\n * locales concurrently, and a shared singleton would let one request's `lng`\n * leak into another's markup.\n *\n * Catalogs are bundled rather than fetched, so `init` completes synchronously\n * (`initAsync: false`) and the first paint is already translated — no Suspense\n * boundary, no flash of untranslated keys.\n *\n * `modules` is how the React binding passes `initReactI18next` without this\n * package taking a dependency on React.\n */\nexport function createI18n(\n locale: Locale,\n modules: readonly ThirdPartyModule[] = [],\n): i18n {\n const catalogs = required();\n const instance = modules.reduce<i18n>(\n (acc, module) => acc.use(module),\n i18next.createInstance(),\n );\n\n instance.init({\n lng: locale,\n fallbackLng: DEFAULT_LOCALE,\n ns: [...catalogs.namespaces],\n defaultNS: catalogs.defaultNS,\n resources: catalogs.resources,\n initAsync: false,\n interpolation: {\n // React escapes for us; double-escaping turns \"Café\" into \"Café\".\n escapeValue: false,\n },\n });\n\n return instance;\n}\n\n/**\n * A translate function for a locale the caller already holds — a route param,\n * typically — bound to one namespace.\n *\n * Request-free by construction, and that is the point: a component built on it\n * touches neither `headers()` nor `cookies()`, so the page containing it stays\n * statically renderable and can be prerendered once per locale. Its\n * request-reading counterpart is `getServerT` in `@entifix/next-i18n`,\n * which re-exports this one for symmetry at app call sites.\n *\n * It lives here rather than in the Next i18n shell because the shells share\n * T4 and the tier register declares no edge between them — and the storefront\n * shell, which has a real `[locale]` segment and therefore never reads a\n * request, is exactly the caller that needs it.\n */\nexport function getServerTFor<N extends Namespace>(locale: Locale, ns?: N) {\n // `Namespace` is i18next's own, so it resolves to whatever union the *host*\n // declared in `CustomTypeOptions`. This package names no namespace of its own\n // and imports no catalog — which is the whole point — while a caller still\n // gets a `t` bound to the namespace it asked for, and an unknown key stays a\n // compile error.\n return createI18n(locale).getFixedT(locale, ns ?? null);\n}\n\nlet shared: i18n | undefined;\n\n/**\n * The instance used when no provider is mounted — Storybook, a unit spec, a\n * screen that forgot.\n *\n * One per process, shared across packages on purpose. Each React package\n * resolves copy through its own `useTranslation`, and react-i18next falls back\n * to its *uninitialized* global when no provider is in the tree — which renders\n * raw keys at the user. Handing every caller the same initialized default\n * instance makes that failure mode \"the fleet default locale\" instead.\n *\n * Built lazily, so an app that always mounts a provider never pays for it.\n */\nexport function sharedFallbackI18n(\n modules: readonly ThirdPartyModule[] = [],\n): i18n {\n shared ??= createI18n(DEFAULT_LOCALE, modules);\n return shared;\n}\n\n/** Drops the installed catalogs and the shared instance. For specs. */\nexport function resetCatalogs(): void {\n installed = undefined;\n shared = undefined;\n}\n"],"names":["DEFAULT_LOCALE","EntifixBuildError","i18next","installed","defineCatalogs","catalogs","required","undefined","createI18n","locale","modules","instance","reduce","acc","module","use","createInstance","init","lng","fallbackLng","ns","namespaces","defaultNS","resources","initAsync","interpolation","escapeValue","getServerTFor","getFixedT","shared","sharedFallbackI18n","resetCatalogs"],"mappings":"AAAA,SAASA,cAAc,EAAEC,iBAAiB,QAAqB,gBAAgB;AAC/E,OAAOC,aAIA,UAAU;AAoBjB,IAAIC;AAEJ;;;;;;;;;;;;;;CAcC,GACD,OAAO,SAASC,eAAeC,QAAsB;IACnDF,YAAYE;AACd;AAEA,MAAMC,WAAW;IACf,IAAIH,cAAcI,WAAW;QAC3B,MAAM,IAAIN,kBACR,uEACE,oEACA;IAEN;IACA,OAAOE;AACT;AAEA;;;;;;;;;;;;;CAaC,GACD,OAAO,SAASK,WACdC,MAAc,EACdC,UAAuC,EAAE;IAEzC,MAAML,WAAWC;IACjB,MAAMK,WAAWD,QAAQE,MAAM,CAC7B,CAACC,KAAKC,SAAWD,IAAIE,GAAG,CAACD,SACzBZ,QAAQc,cAAc;IAGxBL,SAASM,IAAI,CAAC;QACZC,KAAKT;QACLU,aAAanB;QACboB,IAAI;eAAIf,SAASgB,UAAU;SAAC;QAC5BC,WAAWjB,SAASiB,SAAS;QAC7BC,WAAWlB,SAASkB,SAAS;QAC7BC,WAAW;QACXC,eAAe;YACb,uEAAuE;YACvEC,aAAa;QACf;IACF;IAEA,OAAOf;AACT;AAEA;;;;;;;;;;;;;;CAcC,GACD,OAAO,SAASgB,cAAmClB,MAAc,EAAEW,EAAM;IACvE,4EAA4E;IAC5E,8EAA8E;IAC9E,2EAA2E;IAC3E,6EAA6E;IAC7E,iBAAiB;IACjB,OAAOZ,WAAWC,QAAQmB,SAAS,CAACnB,QAAQW,aAAAA,KAAM;AACpD;AAEA,IAAIS;AAEJ;;;;;;;;;;;CAWC,GACD,OAAO,SAASC,mBACdpB,UAAuC,EAAE;IAEzCmB,iBAAAA,SAAAA,SAAWrB,WAAWR,gBAAgBU;IACtC,OAAOmB;AACT;AAEA,qEAAqE,GACrE,OAAO,SAASE;IACd5B,YAAYI;IACZsB,SAAStB;AACX"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@entifix/i18n",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "i18next binding: install each package's catalogs once, translate with typed keys, and negotiate a locale.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -37,9 +37,6 @@
|
|
|
37
37
|
],
|
|
38
38
|
"nx": {
|
|
39
39
|
"tags": [
|
|
40
|
-
"layer:entifix",
|
|
41
|
-
"scope:shared",
|
|
42
|
-
"entifix:client",
|
|
43
40
|
"tier:2"
|
|
44
41
|
],
|
|
45
42
|
"sourceRoot": "packages/ts/i18n/src",
|
|
@@ -61,7 +58,7 @@
|
|
|
61
58
|
},
|
|
62
59
|
"dependencies": {
|
|
63
60
|
"@swc/helpers": "0.5.23",
|
|
64
|
-
"@entifix/core": "0.1.
|
|
61
|
+
"@entifix/core": "0.1.3"
|
|
65
62
|
},
|
|
66
63
|
"peerDependencies": {
|
|
67
64
|
"i18next": ">=26 <27"
|