{"_id":"@ayinza_dev/i18n-config","_rev":"5-d4442b903001fe82dcb2ec34cc2472aa","name":"@ayinza_dev/i18n-config","dist-tags":{"latest":"1.5.1"},"versions":{"1.3.0":{"name":"@ayinza_dev/i18n-config","version":"1.3.0","keywords":[],"author":{"name":"ayinza"},"license":"MIT","_id":"@ayinza_dev/i18n-config@1.3.0","maintainers":[{"name":"ayinza_dev","email":"ayinzadev@gmail.com"}],"homepage":"https://github.com/sseris/ayinza-i18n-config#readme","bugs":{"url":"https://github.com/sseris/ayinza-i18n-config/issues"},"dist":{"shasum":"d072f61039fa7bb0f1e3030d0f7c6d1aefc5b40c","tarball":"https://registry.npmjs.org/@ayinza_dev/i18n-config/-/i18n-config-1.3.0.tgz","fileCount":21,"integrity":"sha512-8kayPlZe1w2+7OjIREyn7jJhiR76eQOSgnAKzFEQ0++huIfenYujVfL1X7wVOtcPCjIaoPCmbDZTLeSfYnbDsA==","signatures":[{"sig":"MEUCIQCfiQxTkAXazrsGi5RKlCyP2DnGYVJTR7ZF2U/Aq4B5dAIgFH0Zfi9RVRZJZu6EVg5u4XBmLbeZVv4way0xLTYKQtM=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":65563},"main":"dist/index.js","type":"module","types":"dist/index.d.ts","gitHead":"a3301c9539ec85ca83d4377e9aaf42c5d6a184e0","scripts":{"test":"jest","build":"tsc","test:watch":"jest --watch","build:watch":"tsc --watch"},"_npmUser":{"name":"ayinza_dev","email":"ayinzadev@gmail.com"},"repository":{"url":"git+https://github.com/sseris/ayinza-i18n-config.git","type":"git"},"_npmVersion":"10.8.2","description":"Shared i18next configuration for multiple portals","directories":{},"_nodeVersion":"20.20.2","dependencies":{"i18next":"^25.6.1","i18next-http-backend":"^3.0.2","i18next-browser-languagedetector":"^8.2.0"},"_hasShrinkwrap":false,"devDependencies":{"jest":"^30.2.0","ts-jest":"^29.4.5","typescript":"^5.9.3","@types/jest":"^30.0.0","@types/node":"^24.10.0","@types/react":"^19.2.2","i18next-parser":"^9.0.2","@types/react-dom":"^19.2.2","jest-environment-jsdom":"^30.2.0"},"peerDependencies":{"react":"^19.1.1","react-i18next":"^16.2.4"},"_npmOperationalInternal":{"tmp":"tmp/i18n-config_1.3.0_1781774940237_0.7626277111087996","host":"s3://npm-registry-packages-npm-production"}},"1.4.0":{"name":"@ayinza_dev/i18n-config","version":"1.4.0","keywords":[],"author":{"name":"ayinza"},"license":"MIT","_id":"@ayinza_dev/i18n-config@1.4.0","maintainers":[{"name":"ayinza_dev","email":"ayinzadev@gmail.com"}],"homepage":"https://github.com/sseris/ayinza-i18n-config#readme","bugs":{"url":"https://github.com/sseris/ayinza-i18n-config/issues"},"dist":{"shasum":"a8627c7ed261d9d6932ae56eb3e1609bc8485f44","tarball":"https://registry.npmjs.org/@ayinza_dev/i18n-config/-/i18n-config-1.4.0.tgz","fileCount":21,"integrity":"sha512-gazGGVL91F+1BlsCOZQgE+z4wpJsNJKwspaGKKOp4aVk08c9aUVLzag4bdSHXjfhrmMUuQn19zjshWv/TiQhmg==","signatures":[{"sig":"MEUCIHYRxpl/OcEHi2G5fdPLNS6TfD8yt9hFGHV9dZ04GdRaAiEA81BIWxv1l70+X2wTB7D6BA5LI7ocalZhlnCmhaoKPOM=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":66271},"main":"dist/index.js","type":"module","types":"dist/index.d.ts","gitHead":"fb64ca3887388f3303bc4b842f1b3c32427dd1aa","scripts":{"test":"jest","build":"tsc","test:watch":"jest --watch","build:watch":"tsc --watch"},"_npmUser":{"name":"ayinza_dev","email":"ayinzadev@gmail.com"},"repository":{"url":"git+https://github.com/sseris/ayinza-i18n-config.git","type":"git"},"_npmVersion":"10.8.2","description":"Shared i18next configuration for multiple portals","directories":{},"_nodeVersion":"20.20.2","dependencies":{"i18next":"^25.6.1","i18next-http-backend":"^3.0.2","i18next-browser-languagedetector":"^8.2.0"},"_hasShrinkwrap":false,"devDependencies":{"jest":"^30.2.0","ts-jest":"^29.4.5","typescript":"^5.9.3","@types/jest":"^30.0.0","@types/node":"^24.10.0","@types/react":"^19.2.2","i18next-parser":"^9.0.2","@types/react-dom":"^19.2.2","jest-environment-jsdom":"^30.2.0"},"peerDependencies":{"react":"^19.1.1","react-i18next":"^16.2.4"},"_npmOperationalInternal":{"tmp":"tmp/i18n-config_1.4.0_1782481687138_0.32103648962525866","host":"s3://npm-registry-packages-npm-production"}},"1.4.1":{"name":"@ayinza_dev/i18n-config","version":"1.4.1","keywords":[],"author":{"name":"ayinza"},"license":"MIT","_id":"@ayinza_dev/i18n-config@1.4.1","maintainers":[{"name":"ayinza_dev","email":"ayinzadev@gmail.com"}],"homepage":"https://github.com/sseris/ayinza-i18n-config#readme","bugs":{"url":"https://github.com/sseris/ayinza-i18n-config/issues"},"dist":{"shasum":"653f053e8df66cd32a3ead50442aa64868471ef3","tarball":"https://registry.npmjs.org/@ayinza_dev/i18n-config/-/i18n-config-1.4.1.tgz","fileCount":21,"integrity":"sha512-qZqZlkmFGnRpUQS0ssztaJl1brG6m9SvViULOzCxcogm7LTsHtcnJcdBAF/330Yqs148ixrMRg0/h/D2eMAgmg==","signatures":[{"sig":"MEUCIHBgNTqHs096hph/ORjdhV/uS4R1Nw0RUOSZLu7Ns3EfAiEA14UDb6ZJbaJmn/HlYnBFwPkVC+NOKcrI7hRiayHAbbo=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":66567},"main":"dist/index.js","type":"module","types":"dist/index.d.ts","gitHead":"5f6a2bb9221f65603e603edb942e9d3d92bccb32","scripts":{"test":"jest","build":"tsc","test:watch":"jest --watch","build:watch":"tsc --watch"},"_npmUser":{"name":"ayinza_dev","email":"ayinzadev@gmail.com"},"repository":{"url":"git+https://github.com/sseris/ayinza-i18n-config.git","type":"git"},"_npmVersion":"10.8.2","description":"Shared i18next configuration for multiple portals","directories":{},"_nodeVersion":"20.20.2","dependencies":{"i18next":"^25.6.1","i18next-http-backend":"^3.0.2","i18next-browser-languagedetector":"^8.2.0"},"_hasShrinkwrap":false,"devDependencies":{"jest":"^30.2.0","ts-jest":"^29.4.5","typescript":"^5.9.3","@types/jest":"^30.0.0","@types/node":"^24.10.0","@types/react":"^19.2.2","i18next-parser":"^9.0.2","@types/react-dom":"^19.2.2","jest-environment-jsdom":"^30.2.0"},"peerDependencies":{"react":"^19.1.1","react-i18next":"^16.2.4"},"_npmOperationalInternal":{"tmp":"tmp/i18n-config_1.4.1_1782829305394_0.3545942344078721","host":"s3://npm-registry-packages-npm-production"}},"1.5.0":{"name":"@ayinza_dev/i18n-config","version":"1.5.0","keywords":[],"author":{"name":"ayinza"},"license":"MIT","_id":"@ayinza_dev/i18n-config@1.5.0","maintainers":[{"name":"ayinza_dev","email":"ayinzadev@gmail.com"}],"homepage":"https://github.com/sseris/ayinza-i18n-config#readme","bugs":{"url":"https://github.com/sseris/ayinza-i18n-config/issues"},"dist":{"shasum":"34a34915a097372f507cb631c178097900b71ad6","tarball":"https://registry.npmjs.org/@ayinza_dev/i18n-config/-/i18n-config-1.5.0.tgz","fileCount":21,"integrity":"sha512-4+8THeQdw5EIdgraOuiQX2ZL8riQIvgqEp76QnTcf2wqvA0ASc7jO+eruD0GkfB45ufAc9uRkGN8tKMOmutJNw==","signatures":[{"sig":"MEUCIQCniqk+RQ+RjpS8PN7lrKUmmwIAG8QC2J1tOqibkMApogIgDkOTS2EvfELOgs0CRWKjJHk1fRuA75TaHMgfdYSF2xE=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":71333},"main":"dist/index.js","type":"module","types":"dist/index.d.ts","gitHead":"f35be75314015efb7245d3b0b1d32605d8e8d6f9","scripts":{"test":"jest","build":"tsc","test:watch":"jest --watch","build:watch":"tsc --watch"},"_npmUser":{"name":"ayinza_dev","email":"ayinzadev@gmail.com"},"repository":{"url":"git+https://github.com/sseris/ayinza-i18n-config.git","type":"git"},"_npmVersion":"10.8.2","description":"Shared i18next configuration for multiple portals","directories":{},"_nodeVersion":"20.20.2","dependencies":{"i18next":"^25.6.1","i18next-http-backend":"^3.0.2","i18next-browser-languagedetector":"^8.2.0"},"_hasShrinkwrap":false,"devDependencies":{"jest":"^30.2.0","ts-jest":"^29.4.5","typescript":"^5.9.3","@types/jest":"^30.0.0","@types/node":"^24.10.0","@types/react":"^19.2.2","i18next-parser":"^9.0.2","@types/react-dom":"^19.2.2","jest-environment-jsdom":"^30.2.0"},"peerDependencies":{"react":"^19.1.1","react-i18next":"^16.2.4"},"_npmOperationalInternal":{"tmp":"tmp/i18n-config_1.5.0_1782873780141_0.7222240982334611","host":"s3://npm-registry-packages-npm-production"}},"1.5.1":{"name":"@ayinza_dev/i18n-config","version":"1.5.1","type":"module","main":"dist/index.js","types":"dist/index.d.ts","scripts":{"build":"tsc","build:watch":"tsc --watch","test":"jest","test:watch":"jest --watch"},"keywords":[],"author":{"name":"ayinza"},"license":"MIT","description":"Shared i18next configuration for multiple portals","peerDependencies":{"react":"^19.1.1","react-i18next":"^16.2.4"},"dependencies":{"i18next":"^25.6.1","i18next-browser-languagedetector":"^8.2.0","i18next-http-backend":"^3.0.2"},"devDependencies":{"@types/jest":"^30.0.0","@types/node":"^24.10.0","@types/react":"^19.2.2","@types/react-dom":"^19.2.2","i18next-parser":"^9.0.2","jest":"^30.2.0","jest-environment-jsdom":"^30.2.0","ts-jest":"^29.4.5","typescript":"^5.9.3"},"repository":{"type":"git","url":"git+https://github.com/sseris/ayinza-i18n-config.git"},"bugs":{"url":"https://github.com/sseris/ayinza-i18n-config/issues"},"homepage":"https://github.com/sseris/ayinza-i18n-config#readme","_id":"@ayinza_dev/i18n-config@1.5.1","gitHead":"14b20384f147141d008f3ab2cbaf352f6ca181eb","_nodeVersion":"20.20.2","_npmVersion":"10.8.2","dist":{"integrity":"sha512-w7jEa0W7LpK8SYFX6t3TkJ/D+zKhDksfuJlCuoIMVj0skhMpE0z8JR3c4FOMrHizdh/golzoTElLoNdThWgVzQ==","shasum":"e6d1bfd56ed27c38712f05e029ac1e246610933a","tarball":"https://registry.npmjs.org/@ayinza_dev/i18n-config/-/i18n-config-1.5.1.tgz","fileCount":21,"unpackedSize":73979,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIGBUZXjVkYIln1QMttr/RJYn0CxAkvwSJs8qUsh/BjfIAiEA/u+pxNcSccMJmS3Ga3RBDchNKVwqDXuscZ+qH/+vpT0="}]},"_npmUser":{"name":"ayinza_dev","email":"ayinzadev@gmail.com"},"directories":{},"maintainers":[{"name":"ayinza_dev","email":"ayinzadev@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/i18n-config_1.5.1_1782994502850_0.17351129775489293"},"_hasShrinkwrap":false}},"time":{"created":"2026-06-18T09:29:00.116Z","modified":"2026-07-02T12:15:03.117Z","1.3.0":"2026-06-18T09:29:00.372Z","1.4.0":"2026-06-26T13:48:07.268Z","1.4.1":"2026-06-30T14:21:45.565Z","1.5.0":"2026-07-01T02:43:00.304Z","1.5.1":"2026-07-02T12:15:02.998Z"},"bugs":{"url":"https://github.com/sseris/ayinza-i18n-config/issues"},"author":{"name":"ayinza"},"license":"MIT","homepage":"https://github.com/sseris/ayinza-i18n-config#readme","keywords":[],"repository":{"type":"git","url":"git+https://github.com/sseris/ayinza-i18n-config.git"},"description":"Shared i18next configuration for multiple portals","maintainers":[{"name":"ayinza_dev","email":"ayinzadev@gmail.com"}],"readme":"# ayinza-i18n-config\n\nShared i18next configuration and formatting utilities for Ayinza portals.\n\nThis package centralizes i18n initialization, sensible defaults, and a set of\nformatters (currency, number, percent, date/time, relative time) that are\nintegrated with i18next's interpolation system. It also provides React hooks\nfor easy consumption in React apps.\n\n## Table of contents\n\n- About\n- Installation\n- Quick start\n- API\n- Configuration\n- Examples\n- Translation extraction\n- Testing\n- Development\n- License\n\n## About\n\nThe library bundles a default i18n configuration (detection, backend, namespaces,\nand formatters) and exposes helpers to initialize i18next, access the global\ni18n instance, and consume localization + formatting helpers in React\ncomponents.\n\nIt is intentionally lightweight and designed to be used as a shared\ndependency across multiple portals that want consistent localization behavior.\n\n## Installation\n\nInstall the package and peer dependencies (peer dependencies are required by\nconsumers and not bundled):\n\n```bash\n# Using npm\nnpm install @ayinza_dev/i18n-config i18next react react-i18next\n\n# Using yarn\nyarn add @ayinza_dev/i18n-config i18next react react-i18next\n```\n\nAlso install the optional runtime backends used by this package (the package\ndeclares them as dependencies):\n\n```bash\nnpm install i18next-http-backend i18next-browser-languagedetector\n```\n\nNote: This package declares `i18next`, `react`, and `react-i18next` as\npeerDependencies — install versions compatible with your app.\n\n## Quick start\n\nInitialize i18n at application startup (for example in `src/main.tsx` or\n`src/index.tsx` in a React app):\n\n```ts\nimport React from \"react\";\nimport { createRoot } from \"react-dom/client\";\nimport App from \"./App\";\nimport { initializeI18n } from \"@ayinza_dev/i18n-config\";\n\n// Optional: pass overrides to customize backend, supported languages, or\n// formatters.\ninitializeI18n({\n  config: {\n    portalName: \"My Portal\",\n    backend: {\n      loadPath: \"/locales/{{lng}}/{{ns}}.json\",\n      customHeaders: { \"X-Portal\": \"my-portal\" },\n    },\n    supportedLngs: [\"en\", \"ar\", \"fr\"],\n  },\n})\n  .then(() => {\n    const root = createRoot(document.getElementById(\"root\")!);\n    root.render(<App />);\n  })\n  .catch((err) => console.error(err));\n```\n\nThe function returns the initialized i18next instance and also registers a set\nof custom formatters so you may use them inside translations (interpolation\nformatters) or via the provided hooks.\n\n> **SSR note:** `initializeI18n` touches browser-only globals to set the\n> `dir` attribute. The implementation now guards those calls, but you should\n> still run initialization on the client (e.g., inside a `useEffect` or\n> Next.js `use client` entry point) to ensure detectors and DOM updates work.\n\n## Loading translations from localization-service\n\nAyinza serves its shared translation catalog from `localization-service`, whose\nresponses are wrapped in an envelope: `{ \"data\": { \"translations\": { ... } } }`.\nPass a `localization` block and the library fetches that catalog for each active\nlanguage and overlays it (deep-merge, **remote values win**) on top of whatever\nthe bundled `backend.loadPath` provides as an offline base:\n\n```ts\nimport { initializeI18n } from \"@ayinza_dev/i18n-config\";\n\nawait initializeI18n({\n  config: {\n    localization: {\n      baseUrl: import.meta.env.VITE_LOCALIZATION_API_BASE_URL, // e.g. https://localization.example/api/v1\n      // path defaults to \"/l10n/translations/{{lng}}\"\n      // category: \"sseris\",   // optional ?category= filter (notification-service scopes; UIs usually omit)\n      // headers: { ... },\n    },\n    supportedLngs: [\"en-US\", \"ar-SA\"],\n    fallbackLng: \"en-US\",\n  },\n});\n```\n\nBehavior:\n\n- **Single fetch per language, lazy.** The active + fallback languages load\n  before init resolves; any other language loads the first time it's selected\n  (via `languageChanged`). No app should fetch the catalog itself.\n- **Envelope-aware.** `{ data: { translations } }` (or a bare `{ translations }`\n  / flat map) is unwrapped for you.\n- **Resilient.** A failed load never throws — the bundled base (and inline `t()`\n  defaults) remain; a later language switch retries.\n- **RTL + formatters** continue to work unchanged.\n\nThis replaces the old per-app pattern of pointing `backend.loadPath` at the API\nand manually re-fetching to unwrap the envelope.\n\n## API\n\nTop-level exports (from `src/index.ts`):\n\n- `initializeI18n(options?: I18nInitOptions): Promise<i18n>` — initialize the\n  i18next instance with defaults merged with your overrides.\n- `getI18nInstance(): i18n` — access the i18next singleton.\n- `getFormatters(): I18nFormatters` — get the formatter instance (throws if\n  not initialized).\n- `defaultConfig` — the default configuration object used by\n  `initializeI18n`.\n- `createI18nConfig(partial?: Partial<I18nConfig>)` — returns a fully merged\n  config object without initializing i18next; useful for building configs in\n  build-time tooling or sharing defaults across portals.\n- `createTranslationSnapshot(options: CreateTranslationSnapshotOptions)` —\n  flatten translation JSON trees into a comparable snapshot for diffing.\n- `collectNewTranslationKeys(options: CollectNewTranslationKeysOptions)` —\n  compute keys that were added between two snapshots.\n- `handleNewTranslationKeys(options: HandleNewTranslationKeysOptions)` — push\n  detected keys to a remote endpoint or log them during dry runs.\n- `useFormatting()` — React hook providing formatting helpers bound to the\n  current language.\n- `useI18n()` — combined hook returning both `useTranslation()` props and\n  formatting helpers.\n- Both hooks expect `initializeI18n` to have completed; call initialization in\n  your app bootstrap before rendering components that use them, otherwise\n  `getFormatters()` will throw.\n- Re-exports: `useTranslation`, `Trans`, `Translation` from `react-i18next`.\n\nTypes exported (from `src/types.ts`):\n\n- `I18nConfig` — top-level configuration object shape.\n- `I18nInitOptions` — options for `initializeI18n`.\n- `FormattersConfig`, `LocaleMapping` — configuration shapes for formatters.\n\nFormatters class: `I18nFormatters` provides methods such as:\n\n- `formatCurrency(amount, language, currency?, options?)`\n- `formatNumber(value, language, options?)`\n- `formatPercent(value, language, options?)`\n- `formatDate(date, language, options?)`\n- `formatTime(date, language, options?)`\n- `formatDateTime(date, language, options?)`\n- `formatRelativeTime(value, unit, language, options?)`\n\nThese are already wired into i18next as interpolation formatters named\n`currency`, `number`, `percent`, `date`, `time`, `datetime`, and `relative`.\n\nEvery formatter catches `Intl` errors and falls back to simple strings (for\nexample, returning `INVALID 100` for a bad currency code or `toLocaleString()`\nfor an invalid date). This keeps your UI from crashing, but you may still see\nconsole warnings when supplying malformed input.\n\n## Configuration\n\n`defaultConfig` (summary):\n\n- backend: { loadPath }\n- detection: browser language detection configuration\n- fallbackLng: `en`\n- supportedLngs: `[\"en\",\"ar\",\"fr\",\"es\"]` (override to match your portal to\n  avoid loading unused bundles)\n- defaultNS / ns: namespaces used (feel free to switch to `common` if that is\n  your primary namespace)\n- interpolation.escapeValue: false\n- formatters: default formatter configuration (defaultCurrency `USD`,\n  `fallbackLocale: \"en-US\"` when no locale mapping matches)\n- react: `{ useSuspense: true }` but you can extend it with\n  `bindI18n`/`bindI18nStore` to match your React rendering mode\n\nYou can override only the pieces you need — `initializeI18n` merges defaults\nwith your partial config. If you need a pure helper (no side effects) to\nassemble configs, use `createI18nConfig({ ...overrides })` and feed the result\ninto your own bootstrap logic.\n\nCommon overrides:\n\n- `supportedLngs`: keep this list scoped to the locales your portal actually\n  serves so language detection stays predictable and bundles stay small.\n- `ns` / `defaultNS`: if you share a `common` namespace across portals,\n  consider setting `defaultNS: \"common\"` and trimming the `ns` array.\n- `react`: set `useSuspense: false` for legacy React renderers or provide\n  `bindI18n: \"languageChanged\"` when coordinating with data-fetching layers.\n- `formatters.fallbackLocale`: change this if your organization defaults to a\n  locale other than English; it is used whenever a language code is missing\n  from the locale mapping tables.\n- **Module format:** The published package currently ships as an ES module build\n  (per `tsconfig.json`). If your tooling expects CommonJS, configure your bundler\n  to transpile ESM or consider contributing a dual-build setup.\n\n## Examples\n\nUse translation + formatting together in a React component:\n\n```tsx\nimport React from \"react\";\nimport { useI18n } from \"@ayinza_dev/i18n-config\";\n\nfunction Price({ amount }: { amount: number }) {\n  const { t, formatCurrency } = useI18n();\n\n  return (\n    <div>\n      <h3>{t(\"priceHeading\")}</h3>\n      <p>{formatCurrency(amount)}</p>\n    </div>\n  );\n}\n```\n\nUsing formatters directly (non-React):\n\n```ts\nimport { initializeI18n, getFormatters } from \"@ayinza_dev/i18n-config\";\n\nasync function start() {\n  await initializeI18n();\n  const fmt = getFormatters();\n  console.log(fmt.formatCurrency(19.99, \"en\", \"USD\"));\n}\n```\n\nUsing interpolation in translation strings (example `en/common.json`):\n\n```json\n{\n  \"price\": \"{{value, currency}}\"\n}\n```\n\nThen `t('price', { value: 19.99, currency: 'EUR' })` will use the registered\n`currency` formatter.\n\n## Integrating Across Multiple Portals\n\nWhen sharing this package across portals, keep initialization centralized so\neach shell bootstraps consistently:\n\n1. Create a thin wrapper (e.g., `packages/i18n/client.ts`) that calls\n   `initializeI18n` with portal-specific overrides such as namespace lists or\n   branding headers.\n2. Import only that wrapper from each portal entry point to keep behavior\n   aligned and avoid forgetting required detectors/backends.\n3. Re-export helpers (`useI18n`, `getFormatters`) from your shell layer so\n   downstream micro frontends consume the same singleton instance.\n4. For SSR/Next.js, run `initializeI18n` inside client components or a `useEffect`\n   guard to allow detectors to access browser APIs, then hydrate shared hooks.\n\nExample shared bootstrap that portals can reuse:\n\n```ts\n// packages/i18n/bootstrap.ts\nimport {\n  initializeI18n,\n  getFormatters,\n  useI18n,\n} from \"@ayinza_dev/i18n-config\";\n\nexport async function setupPortalI18n(portalName: string) {\n  await initializeI18n({\n    config: {\n      portalName,\n      backend: {\n        loadPath: `/locales/${portalName}/{{lng}}/{{ns}}.json`,\n      },\n      supportedLngs: [\"en\", \"fr\", \"sw\"],\n    },\n  });\n\n  return {\n    i18n: getFormatters(),\n    useI18n,\n  };\n}\n\n// portal-a/src/main.tsx\nimport { setupPortalI18n } from \"@ayinza/portal-shared/i18n\";\n\nsetupPortalI18n(\"portal-a\").then(() => {\n  // mount React app here, all child components can call useI18n()\n});\n```\n\nThis pattern keeps each portal lightweight while ensuring updates to the core\nlocalization stack propagate everywhere by upgrading just this package.\n\n## Translation extraction\n\nThe package now ships with light wrappers around\n[`i18next-parser`](https://github.com/i18next/i18next-parser) so every portal\ncan reuse the same extraction defaults and push workflow:\n\n1. **Config helper.** Create `i18next-parser.config.mjs` (or `.cjs`) that simply\n   exports `createI18nextParserConfig({ /* overrides */ })`. The helper sets\n   consistent defaults (lexers, separators, indentation, `createOldCatalogs`,\n   etc.) so every portal parses sources the same way.\n\n   ```ts\n   // i18next-parser.config.mjs\n   import { createI18nextParserConfig } from \"@ayinza_dev/i18n-config\";\n\n   export default createI18nextParserConfig({\n     input: [\"src/**/*.{ts,tsx}\"],\n     locales: [\"en\"],\n     output: \"locales/$LOCALE/$NAMESPACE.json\",\n   });\n   ```\n\n2. **Detect new keys.** Capture a snapshot before and after running the parser\n   (usually for the default locale) by loading your locale JSON and passing it\n   to `createTranslationSnapshot`, then call `collectNewTranslationKeys` to\n   compute the delta.\n\n   ```ts\n   import { readFile } from \"node:fs/promises\";\n   import path from \"node:path\";\n   import {\n     createTranslationSnapshot,\n     collectNewTranslationKeys,\n     handleNewTranslationKeys,\n   } from \"@ayinza_dev/i18n-config\";\n\n   const localesRoot = path.resolve(\"locales\");\n   const namespaces = [\"translation\", \"common\"];\n\n   async function loadNamespaces(locale: string) {\n     const entries = await Promise.all(\n       namespaces.map(async (namespace) => {\n         const filePath = path.join(localesRoot, locale, `${namespace}.json`);\n         const raw = await readFile(filePath, \"utf8\");\n         return [namespace, JSON.parse(raw) as Record<string, unknown>];\n       })\n     );\n\n     return Object.fromEntries(entries) as Record<\n       string,\n       Record<string, unknown>\n     >;\n   }\n\n   const before = createTranslationSnapshot({\n     locale: \"en\",\n     namespaces: await loadNamespaces(\"en\"),\n   });\n\n   // Run `npx i18next --config i18next-parser.config.mjs \"src/**/*.{ts,tsx}\"`\n\n   const after = createTranslationSnapshot({\n     locale: \"en\",\n     namespaces: await loadNamespaces(\"en\"),\n   });\n\n   const newKeys = collectNewTranslationKeys({ previous: before, next: after });\n   ```\n\n3. **Push or log.** Pass the detected keys to `handleNewTranslationKeys` to run\n   a dry-run or POST them to your translation management service. Configure the\n   helper with portal-specific metadata so CI logs stay readable.\n\n   ```ts\n   await handleNewTranslationKeys({\n     newKeys,\n     pushConfig: {\n       portalName: \"admin-shell\",\n       pushUrl: process.env.TRANSLATION_PUSH_URL,\n       authorizationToken: process.env.TRANSLATION_PUSH_TOKEN,\n       dryRun: process.env.CI === \"true\" && process.env.DRY_RUN === \"true\",\n     },\n   });\n   ```\n\nIf `pushUrl` is omitted or `dryRun` is `true`, the helper only logs detected\nkeys. Provide a custom `fetchImpl` via `ParserPushConfig` when running on Node\nversions older than 18 (which lack `global.fetch`).\n\n## Testing\n\nThere are unit tests for `I18nFormatters` (see `src/formatters.test.ts`) and\nfor the config merge helper (see `src/config.test.ts`). Run tests with the\nprovided npm scripts:\n\n```bash\nnpm test\n# watch mode during development\nnpm run test:watch\n```\n\nNote: this repository includes Jest devDependencies configured for TypeScript.\n\n## Development\n\n- Build: `npm run build` (compiles to `dist/` using `tsc`)\n- Watch: `npm run build:watch`\n- Test: `npm test` or `npm run test:watch`\n\nIf you intend to contribute, please run tests and add coverage for new\nfeatures.\n\n## License\n\nMIT — see the `LICENSE` file in this repository.\n\n## Next steps & suggestions\n\n- Add CI (GitHub Actions) to run tests and build on push/PR.\n- Add usage examples / Storybook snippets for React components that depend on\n  formatting.\n- Consider publishing with changelog and semantic-release for automated\n  releases.\n- Allow consumers to provide a custom logger/debug handler so initialization\n  logs can be routed through their monitoring stack instead of console.\n","readmeFilename":"README.md"}