@entifix/i18n 0.1.0 → 0.1.2

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 ADDED
@@ -0,0 +1,22 @@
1
+ # @entifix/i18n
2
+
3
+ The i18next binding. Every entifix package that renders copy ships its own catalogs; an application installs them once with `defineCatalogs` and composes the typed-key shape that makes a mistyped key a compile error.
4
+
5
+ See [the first thing you have to write](https://github.com/r10c-technologies/entifix#the-first-thing-you-have-to-write) — the type augmentation only an application can declare.
6
+
7
+ ## Install
8
+
9
+ ```sh
10
+ pnpm add @entifix/i18n i18next
11
+ ```
12
+
13
+ ## Entry points
14
+
15
+ | Import | What it holds |
16
+ | ----------------------- | ------------------------------------------------------------------------------------------------ |
17
+ | `@entifix/i18n` | `defineCatalogs`, `createI18n`, `getServerTFor` |
18
+ | `@entifix/i18n/routing` | Locale negotiation and path helpers, with no i18next import — safe for an edge middleware bundle |
19
+
20
+ ---
21
+
22
+ Tier 2 (adapters) of entifix — a package depends only on its own tier or below. See [the tiers](https://github.com/r10c-technologies/entifix#the-tiers-and-what-you-are-allowed-to-install). MIT licensed.
@@ -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 a `layer:shell`
61
- * package may not depend on another `layer:shell` package — and the storefront
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 a `layer:shell`
70
- * package may not depend on another `layer:shell` package — and the storefront
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) {
@@ -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&#233;\".\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 a `layer:shell`\n * package may not depend on another `layer:shell` package — 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"}
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&#233;\".\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,7 @@
1
1
  {
2
2
  "name": "@entifix/i18n",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
+ "description": "i18next binding: install each package's catalogs once, translate with typed keys, and negotiate a locale.",
4
5
  "license": "MIT",
5
6
  "repository": {
6
7
  "type": "git",
@@ -36,9 +37,6 @@
36
37
  ],
37
38
  "nx": {
38
39
  "tags": [
39
- "layer:entifix",
40
- "scope:shared",
41
- "entifix:client",
42
40
  "tier:2"
43
41
  ],
44
42
  "sourceRoot": "packages/ts/i18n/src",
@@ -60,7 +58,7 @@
60
58
  },
61
59
  "dependencies": {
62
60
  "@swc/helpers": "0.5.23",
63
- "@entifix/core": "0.1.0"
61
+ "@entifix/core": "0.1.2"
64
62
  },
65
63
  "peerDependencies": {
66
64
  "i18next": ">=26 <27"