{"id":16031750,"url":"https://github.com/crimx/val-i18n","last_synced_at":"2025-09-08T04:41:29.667Z","repository":{"id":65343259,"uuid":"586718427","full_name":"crimx/val-i18n","owner":"crimx","description":"Reactive i18n with value-enhancer.","archived":false,"fork":false,"pushed_at":"2024-09-25T04:39:52.000Z","size":249,"stargazers_count":4,"open_issues_count":0,"forks_count":0,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-08-17T07:49:46.429Z","etag":null,"topics":["gettext","globalization","i18n","internationalization","l10n","localization","react","reactjs","svelte","sveltejs","translation"],"latest_commit_sha":null,"homepage":"https://crimx.github.io/val-i18n/","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/crimx.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2023-01-09T03:48:40.000Z","updated_at":"2024-09-25T04:39:22.000Z","dependencies_parsed_at":"2024-10-08T08:58:51.283Z","dependency_job_id":"2c738c13-67b7-4a85-b439-56c34a6b1c6b","html_url":"https://github.com/crimx/val-i18n","commit_stats":null,"previous_names":[],"tags_count":11,"template":false,"template_full_name":null,"purl":"pkg:github/crimx/val-i18n","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/crimx%2Fval-i18n","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/crimx%2Fval-i18n/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/crimx%2Fval-i18n/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/crimx%2Fval-i18n/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/crimx","download_url":"https://codeload.github.com/crimx/val-i18n/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/crimx%2Fval-i18n/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":274135323,"owners_count":25228203,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-09-08T02:00:09.813Z","response_time":121,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["gettext","globalization","i18n","internationalization","l10n","localization","react","reactjs","svelte","sveltejs","translation"],"created_at":"2024-10-08T21:05:21.161Z","updated_at":"2025-09-08T04:41:29.588Z","avatar_url":"https://github.com/crimx.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# [val-i18n](https://github.com/crimx/val-i18n)\n\n\u003cp align=\"center\"\u003e\n  \u003cimg width=\"200\" src=\"https://raw.githubusercontent.com/crimx/val-i18n/main/assets/val-i18n.svg\"\u003e\n\u003c/p\u003e\n\n[![Build Status](https://github.com/crimx/val-i18n/actions/workflows/build.yml/badge.svg)](https://github.com/crimx/val-i18n/actions/workflows/build.yml)\n[![npm-version](https://img.shields.io/npm/v/val-i18n.svg)](https://www.npmjs.com/package/val-i18n)\n[![Coverage Status](https://img.shields.io/coveralls/github/crimx/val-i18n/master)](https://coveralls.io/github/crimx/val-i18n?branch=master)\n[![minified-size](https://img.shields.io/bundlephobia/minzip/val-i18n)](https://bundlephobia.com/package/val-i18n)\n\n[![Commitizen friendly](https://img.shields.io/badge/commitizen-friendly-brightgreen.svg?maxAge=2592000)](http://commitizen.github.io/cz-cli/)\n[![Conventional Commits](https://img.shields.io/badge/Conventional%20Commits-1.0.0-brightgreen.svg?maxAge=2592000)](https://conventionalcommits.org)\n[![code style: prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg?style=flat-square)](https://github.com/prettier/prettier)\n\nReactive i18n with val-i18n.\n\n## Install\n\n```bash\nnpm add val-i18n value-enhancer\n```\n\n## Features\n\n- Subscribable reactive `lang$`, `t$` and `locales$`.\n- Lightweight and fast `t()` translation.\n- Nested locale messages.\n- Message formatting and pluralization.\n- Easy dynamic locale loading.\n- Locale namespaces.\n- Framework integration friendly ([React, Svelte, etc.](#reactive-i18n)).\n\n## Usage\n\nCreate an I18n instance with static locales:\n\n```ts\nimport { I18n, type Locales } from \"val-i18n\";\n\nconst locales: Locales = {\n  en: {\n    stock: {\n      fruit: \"apple\",\n    },\n  },\n};\n\nconst i18n = new I18n(\"en\", locales);\ni18n.t(\"stock.fruit\"); // apple\n\n// add more locales later\nconst zhCN = await import(`./locales/zh-CN.json`);\ni18n.addLocale(\"zh-CN\", zhCN);\n\n// or replace all locales manually\nconst zhTW = await import(`./locales/zh-TW.json`);\ni18n.locales$.set({ \"zh-TW\": zhTW });\n```\n\nYou can also create an I18n instance with preloaded dynamic locales:\n\n```ts\nimport { I18n } from \"val-i18n\";\n\nconst i18n = await I18n.preload(\"en\", lang =\u003e import(`./locales/${lang}.json`));\n// Locale `./locales/en.json` is preloaded\n\nawait i18n.switchLang(\"zh-CN\"); // Locale `./locales/zh-CN.json` is loaded\n```\n\n### Detect Language\n\nYou can detect language of browser/nodejs via `detectLang`. [BCP 47 tags and sub-tags](https://www.rfc-editor.org/rfc/rfc4647.html#section-3.4) are supported.\n\n```ts\nimport { detectLang } from \"val-i18n\";\n\ndetectLang(); // \"en-US\"\n\nconst i18n = await I18n.preload(\n  // language sub-tag is matched\n  detectLang([\"en\", \"zh-CN\"]) || \"zh-TW\", // \"en\"\n  lang =\u003e import(`./locales/${lang}.json`)\n);\n```\n\n### Message Formatting\n\nMessage keys are surrounded by double curly brackets:\n\n```ts\nimport { I18n, type Locales } from \"val-i18n\";\n\nconst locales: Locales = {\n  en: {\n    stock: {\n      fruit: \"apple\",\n    },\n    fav_fruit: \"I love {{fruit}}\",\n  },\n};\n\nconst i18n = new I18n(\"en\", locales);\nconst fruit = i18n.t(\"stock.fruit\"); // apple\ni18n.t(\"fav_fruit\", { fruit }); // I love apple\n```\n\nIt also works with array:\n\n```ts\nimport { I18n, type Locales } from \"val-i18n\";\n\nconst locales: Locales = {\n  en: {\n    fav_fruit: \"I love {{0}} and {{1}}\",\n  },\n};\n\nconst i18n = new I18n(\"en\", locales);\ni18n.t(\"fav_fruit\", [\"apple\", \"banana\"]); // I love apple and banana\n```\n\n### Pluralization\n\nMessage formatting supports a special key `:option` whose value will be appended to the key-path.\n\nFor example:\n\n```ts\ni18n.t(\"a.b.c\", { \":option\": \"d\" });\n```\n\nIt will look for `\"a.b.c.d\"` and fallback to `\"a.b.c.other\"` if not found.\n\nSo for pluralization we can simply use `:option` as number count.\n\n```ts\nimport { I18n, type Locales } from \"val-i18n\";\n\nconst locales: Locales = {\n  en: {\n    apples: {\n      0: \"No apple\",\n      1: \"An apple\",\n      other: \"{{:option}} apples\",\n    },\n  },\n};\n\nconst i18n = new I18n(\"en\", locales);\ni18n.t(\"apples\", { \":option\": 0 }); // No apple\ni18n.t(\"apples\", { \":option\": 1 }); // An apple\ni18n.t(\"apples\", { \":option\": 3 }); // 3 apples\n```\n\n### Reactive I18n\n\n`i18n.lang$`, `i18n.t$` and `i18n.locales$` are subscribable values.\n\nSee [value-enhancer](https://github.com/crimx/value-enhancer#value-enhancer) for more details.\n\n```js\ni18n.lang$.reaction(lang =\u003e {\n  // logs lang on changed\n  console.log(lang);\n});\n\ni18n.lang$.subscribe(lang =\u003e {\n  // logs lang immediately and on changed\n  console.log(lang);\n});\n```\n\n### Namespace\n\nI18n instance is cheap to create. You can create multiple instances for different namespaces.\n\n```ts\nimport { I18n } from \"val-i18n\";\n\n// Module Login\nasync function moduleLogin() {\n  const i18n = await I18n.preload(\n    \"en\",\n    lang =\u003e import(`./locales/login/${lang}.json`)\n  );\n\n  console.log(i18n.t(\"password\"));\n}\n\n// Module About\nasync function moduleAbout() {\n  const i18n = await I18n.preload(\n    \"en\",\n    lang =\u003e import(`./locales/about/${lang}.json`)\n  );\n\n  console.log(i18n.t(\"author\"));\n}\n```\n\n## Hot Module Replacement\n\nTo use [Vite HMR](https://vitejs.dev/guide/api-hmr.html) for locales:\n\n```ts\nconst i18n = await I18n.preload(\"en\", lang =\u003e import(`./locales/${lang}.json`));\n\nif (import.meta.hot) {\n  import.meta.hot.accept(\n    [\"./locales/en.json\", \"./locales/zh-CN.json\"],\n    ([en, zhCN]) =\u003e {\n      i18n.locales$.set({\n        ...i18n.locales,\n        en: en?.default || i18n.locales.en,\n        \"zh-CN\": zhCN?.default || i18n.locales[\"zh-CN\"],\n      });\n    }\n  );\n}\n```\n\n## Dynamic Import\n\nAlthough you can simply use `import()` to dynamically load locales, with bundler API you can do more.\n\nFor example with Vite you can use [glob import](https://vitejs.dev/guide/features.html#glob-import) to statically get info of all locales.\nThis way allows you to add or remove locales without changing source code.\n\n```ts\nimport { I18n, detectLang, type Locale, type LocaleLang } from \"val-i18n\";\n\nexport const i18nLoader = (): Promise\u003cI18n\u003e =\u003e {\n  const localeModules = import.meta.glob\u003cboolean, string, Locale\u003e(\n    \"./locales/*.json\",\n    { import: \"default\" }\n  );\n\n  const localeLoaders = Object.keys(localeModules).reduce((loaders, path) =\u003e {\n    if (localeModules[path]) {\n      const langMatch = path.match(/\\/([^/]+)\\.json$/);\n      if (langMatch) {\n        loaders[langMatch[1]] = localeModules[path];\n      }\n    }\n    return loaders;\n  }, {} as Record\u003cLocaleLang, () =\u003e Promise\u003cLocale\u003e\u003e);\n\n  const langs = Object.keys(localeLoaders);\n\n  return I18n.preload(\n    detectLang(langs) || (localeLoaders.en ? \"en\" : langs[0]),\n    lang =\u003e localeLoaders[lang]()\n  );\n};\n```\n\n## Framework Integration\n\n### Svelte\n\nIn Svelte you can just pass `i18n.t$` as component props and use `$t` directly.\n\n```html\n\u003cscript\u003e\n  export let t;\n\u003c/script\u003e\n\n\u003cdiv\u003e\n  \u003ch1\u003e{$t(\"title\")}\u003c/h1\u003e\n\u003c/div\u003e\n```\n\n```js\nnew MySvelteComponent({\n  target: document.body,\n  props: {\n    t: i18n.t$,\n  },\n});\n```\n\nYou can also set `i18n.t$` to a Svelte [context](https://svelte.dev/docs#run-time-svelte-setcontext).\n\nFor more advance usages checkout [val-i18n-svelte](https://github.com/crimx/val-i18n-svelte).\n\n### React\n\nIt is recommended to also install [`val-i18n-react`](https://github.com/crimx/val-i18n-react) which includes some handy hooks.\n\nOr you can just use the hooks for `value-enhancer`: [`use-value-enhancer`](https://github.com/crimx/use-value-enhancer).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcrimx%2Fval-i18n","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fcrimx%2Fval-i18n","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcrimx%2Fval-i18n/lists"}