{"id":13605777,"url":"https://github.com/nanostores/i18n","last_synced_at":"2026-04-13T17:08:55.723Z","repository":{"id":42124124,"uuid":"420559291","full_name":"nanostores/i18n","owner":"nanostores","description":"A tiny (≈600 bytes) i18n library for React/Preact/Vue/Svelte","archived":false,"fork":false,"pushed_at":"2024-02-20T23:28:11.000Z","size":633,"stargazers_count":215,"open_issues_count":3,"forks_count":12,"subscribers_count":4,"default_branch":"main","last_synced_at":"2024-04-25T06:44:03.920Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","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/nanostores.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":".github/FUNDING.yml","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},"funding":{"github":"ai"}},"created_at":"2021-10-24T01:19:45.000Z","updated_at":"2024-08-04T00:40:54.229Z","dependencies_parsed_at":"2024-01-14T06:21:47.488Z","dependency_job_id":"1c287fc7-c946-42b9-92e3-4c2a95c722f2","html_url":"https://github.com/nanostores/i18n","commit_stats":{"total_commits":99,"total_committers":9,"mean_commits":11.0,"dds":"0.11111111111111116","last_synced_commit":"32c8daff2392e58fd75c8042ffb1ab255411be9f"},"previous_names":[],"tags_count":18,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nanostores%2Fi18n","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nanostores%2Fi18n/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nanostores%2Fi18n/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nanostores%2Fi18n/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/nanostores","download_url":"https://codeload.github.com/nanostores/i18n/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247284942,"owners_count":20913704,"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","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":[],"created_at":"2024-08-01T19:01:02.632Z","updated_at":"2026-04-13T17:08:55.717Z","avatar_url":"https://github.com/nanostores.png","language":"TypeScript","funding_links":["https://github.com/sponsors/ai"],"categories":["I18N","TypeScript"],"sub_categories":["Reactive Programming"],"readme":"# Nano Stores I18n\n\n\u003cimg align=\"right\" width=\"92\" height=\"92\" title=\"Nano Stores logo\"\n     src=\"https://nanostores.github.io/nanostores/logo.svg\"\u003e\n\nTiny and flexible JS library to make your web application translatable.\nUses [Nano Stores] state manager and [JS Internationalization API].\n\n- **Small.** Around 1 KB (minified and brotlied). Zero dependencies.\n- Works with **React**, **Preact**, **Vue**, **Svelte**, and plain JS.\n- Supports **tree-shaking** and translation **on-demand download**.\n- **Plain flat JSON** translations compatible with\n  online translation services like [Weblate].\n- Out of the box **TypeScript** support for translations.\n- **Flexible variable translations**. You can change translation,\n  for instance, depends on screen size.\n\n```tsx\n// components/post.jsx\nimport { params, count } from '@nanostores/i18n' // You can use own functions\nimport { useStore } from '@nanostores/react'\nimport { i18n, format } from '../stores/i18n.js'\n\nexport const messages = i18n('post', {\n  title: 'Post details',\n  published: params('Was published at {at}'), // TypeScript will get `at` type\n  comments: count({\n    one: '{count} comment',\n    many: '{count} comments'\n  })\n})\n\nexport const Post = ({ author, comments, publishedAt }) =\u003e {\n  const t = useStore(messages)\n  const { time } = useStore(format)\n  return (\n    \u003carticle\u003e\n      \u003ch1\u003e{t.title}\u003c/h1\u003e\n      \u003cp\u003e{t.published({ at: time(publishedAt) })}\u003c/p\u003e\n      \u003cp\u003e{t.comments(comments.length)}\u003c/p\u003e\n    \u003c/article\u003e\n  )\n}\n```\n\n```ts\n// stores/i18n.js\nimport { createI18n, localeFrom, browser, formatter } from '@nanostores/i18n'\nimport { persistentAtom } from '@nanostores/persistent'\n\nexport const setting = persistentAtom\u003cstring | undefined\u003e('locale', undefined)\n\nexport const locale = localeFrom(\n  setting, // User’s locale from localStorage\n  browser({\n    // or browser’s locale auto-detect\n    available: ['en', 'fr', 'ru'],\n    fallback: 'en'\n  })\n)\n\nexport const format = formatter(locale)\n\nexport const i18n = createI18n(locale, {\n  get(code) {\n    return fetchJSON(`/translations/${code}.json`)\n  }\n})\n```\n\n```js\n// public/translations/ru.json\n{\n  \"post\": {\n    \"title\": \"Данные о публикации\",\n    \"published\": \"Опубликован {at}\",\n    \"comments\": {\n      \"one\": \"{count} комментарий\",\n      \"few\": \"{count} комментария\",\n      \"many\": \"{count} комментариев\",\n    }\n  },\n  // Translations for all other components\n}\n```\n\n[JS Internationalization API]: https://hacks.mozilla.org/2014/12/introducing-the-javascript-internationalization-api/\n[Nano Stores]: https://github.com/nanostores/nanostores\n[Weblate]: https://weblate.org/\n\n---\n\n\u003cimg src=\"https://cdn.evilmartians.com/badges/logo-no-label.svg\" alt=\"\" width=\"22\" height=\"16\" /\u003e Made at \u003cb\u003e\u003ca href=\"https://evilmartians.com/devtools?utm_source=nanostores-i18n\u0026utm_campaign=devtools-button\u0026utm_medium=github\"\u003eEvil Martians\u003c/a\u003e\u003c/b\u003e, product consulting for \u003cb\u003edeveloper tools\u003c/b\u003e.\n\n---\n\n## Install\n\n```sh\nnpm install nanostores @nanostores/i18n\n```\n\nFor Astro you need also [`astro-nanostores-i18n`](https://github.com/openscript/astro-i18n/tree/main/libs/astro-nanostores-i18n).\n\n## Usage\n\nWe store locale, time/number formatting functions and translations\nin Nano Stores’ atoms. See [Nano Stores docs] to learn how to use atoms\nin your framework.\n\n[Nano Stores docs]: https://github.com/nanostores/nanostores#guide\n\n### Locale\n\nLocale is a code of user’s language and dialect like `hi` (Hindi), `de-AT`\n(German as used in Austria). We use [Intl locale format].\n\nCurrent locale should be stored in store. We have `localeFrom()` store\nbuilder to find user’s locale in first available source:\n\n```js\nimport { localeFrom } from '@nanostores/i18n'\n\nexport const locale = localeFrom(store1, store2, store3)\n```\n\nWe have store with a locale from browser settings. You need to pass list\nof available translations of your application. If store will not find common\nlocale, it will use fallback locale (`en`, but can be changed\nby `fallback` option).\n\n```js\nimport { localeFrom, browser } from '@nanostores/i18n'\n\nexport const locale = localeFrom(\n  …,\n  browser({ available: ['en', 'fr', 'ru'] as const })\n)\n```\n\nBefore `browser` store, you can put a store, which will allow user to override\nlocale manually. For instance, you can keep an locale’s override\nin `localStorage`.\n\n```ts\nimport { persistentAtom } from '@nanostores/persistent'\n\nconst LOCALES = ['en', 'fr', 'ru'] as const\ntype Locale = (typeof LOCALES)[number]\n\nexport const localeSettings = persistentAtom\u003cLocale\u003e('locale', 'en')\n\nexport const locale = localeFrom(\n  localeSettings,\n  browser({ available: LOCALES })\n)\n```\n\nOr you can take user’s locale from URL router:\n\n```ts\nimport { computed } from 'nanostores'\nimport { router } from './router.js'\n\nconst LOCALES = ['en', 'fr', 'ru'] as const\ntype Locale = (typeof LOCALES)[number]\n\nfunction validate(locale: string): Locale {\n  return LOCALES.includes(locale) ? locale : 'en'\n}\n\nconst urlLocale = computed(router, page =\u003e validate(page?.params.locale))\n\nexport const locale = localeFrom(urlLocale, browser({ available: LOCALES }))\n```\n\nYou can use locale as any Nano Store:\n\n```jsx\nimport { useStore } from '@nanostores/react'\nimport { locale } from '../stores/i18n.js'\n\n// Pure JS example\nlocale.listen(code =\u003e {\n  console.log(`Locale was changed to ${code}`)\n})\n\n// React example\nexport const CurrentLocale = () =\u003e {\n  let code = useStore(locale)\n  return `Your current locale: ${code}`\n}\n```\n\nFor tests you can use simple atom:\n\n```ts\nimport { atom } from 'nanostores'\n\nconst locale = atom('en')\nlocale.set('fr')\n```\n\n[Intl locale format]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl#locale_identification_and_negotiation\n\n### Date, Number \u0026 Relative Time Format\n\n`formatter()` creates a store with a functions to format number and time.\n\n```ts\nimport { formatter } from '@nanostores/i18n'\n\nexport const format = formatter(locale)\n```\n\nThis store will have `time()`, `number()` and `relativeTime()` functions.\n\n```js\nimport { useStore } from '@nanostores/react'\nimport { format } from '../stores/i18n.js'\n\nexport const Date = date =\u003e {\n  let { time } = useStore(format)\n  return time(date)\n}\n```\n\nThese functions accepts options\nof [`Intl.DateTimeFormat`], [`Intl.NumberFormat`]\nand [`Intl.RelativeTimeFormat`].\n\n```ts\ntime(date, {\n  hour12: false,\n  month: 'long',\n  day: 'numeric',\n  hour: 'numeric',\n  minute: 'numeric'\n}) //=\u003e \"November 1, 01:56:33\"\n\nrelativeTime(-1, 'day', { numeric: 'auto' }) //=\u003e \"yesterday\"\n```\n\n[`Intl.DateTimeFormat`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat\n[`Intl.NumberFormat`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat\n[`Intl.RelativeTimeFormat`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/RelativeTimeFormat\n\n### I18n Object\n\nI18n objects is used to define new component and download translations\non locale changes.\n\n```ts\nimport { createI18n } from '@nanostores/i18n'\n\nexport const i18n = createI18n(locale, {\n  async get(code) {\n    return await fetchJSON(`/translations/${code}.json`)\n  }\n})\n```\n\nIn every component you will have base translation with functions and types.\nThis translation will not be download from the server. By default, you should\nuse English. You can change base locale in components with `baseLocale` option.\n\n### Translations\n\nWe have 2 types of translations:\n\n**Base translation.** Developers write it in component sources. It is used\nfor TypeScript types and translation functions (`count()`, `params()`, etc).\n\n```ts\nexport const messages = i18n('post', {\n  title: 'Post details',\n  published: params('Was published at {at}'),\n  comments: count({\n    one: '{count} comment',\n    many: '{count} comments'\n  })\n})\n```\n\n**Other translations** They use JSON format and will be created by translators.\n\n```json\n{\n  \"post\": {\n    \"title\": \"Данные о публикации\",\n    \"published\": \"Опубликован {at}\",\n    \"comments\": {\n      \"one\": \"{count} комментарий\",\n      \"few\": \"{count} комментария\",\n      \"many\": \"{count} комментариев\"\n    }\n  }\n}\n```\n\nTranslations should be a flat structure (key → translation), without a nested\nkeys. pluralization (`count()`) and other helpers doesn’t introduce\nadditional nesting, since they are count as an translation.\n\n#### Parameters\n\n`params()` translation transform replaces parameters in translation string.\n\n```js\nimport { useStore } from '@nanostores/react'\nimport { params } from '@nanostores/i18n'\nimport { i18n } from '../stores/i18n.js'\n\nexport const messages = i18n('hi', {\n  hello: params('Hello, {name}')\n})\n\nexport const Robots = ({ name }) =\u003e {\n  const t = useStore(messages)\n  return t.hello({ name })\n}\n```\n\nYou can use `time()`, `number()` and `relativeTime()` [formatting functions].\n\n[formatting functions]: https://github.com/nanostores/i18n/#date--number-format\n\nAnd you can also use the [`count()`](https://github.com/nanostores/i18n#pluralization) function:\n\n```ts\nimport { count, params } from '@nanostores/i18n'\nimport { i18n } from '../stores/i18n'\n\nexport const messages = i18n('pagination', {\n  page: params\u003c{ category: string }\u003e(\n    count({\n      one: 'One page in {category}',\n      many: '{count} pages in {category}'\n    })\n  )\n})\n\nexport const RobotsListInfo = ({ count }) =\u003e {\n  const t = useStore(messages)\n  return t.page({ category: 'robots' })(count)\n}\n```\n\n#### Pluralization\n\nIn many languages, text could be different depends on items count.\nCompare `1 robot`/`2 robots` in English with\n`1 робот`/`2 робота`/`3 робота`/`21 робот` in Russian.\n\nWe hide this complexity with `count()` translation transform:\n\n```js\nimport { useStore } from '@nanostores/react'\nimport { count } from '@nanostores/i18n'\nimport { i18n } from '../stores/i18n.js'\n\nexport const messages = i18n('robots', {\n  howMany: count({\n    one: '{count} robot',\n    many: '{count} robots'\n  })\n})\n\nexport const Robots = ({ robots }) =\u003e {\n  const t = useStore(messages)\n  return t.howMany(robots.length)\n}\n```\n\n```json\n{\n  \"robots\": {\n    \"howMany\": {\n      \"one\": \"{count} робот\",\n      \"few\": \"{count} робота\",\n      \"many\": \"{count} роботов\"\n    }\n  }\n}\n```\n\n`count()` uses [`Intl.PluralRules`] to get pluralization rules for each locale.\n\n[`Intl.PluralRules`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/PluralRules\n\n#### Custom Variable Translations\n\nIn additional to `params()` and `count()` you can define your own translation\ntransforms. Or you can change pluralization or parameters syntax by replacing\n`count()` and `params()`.\n\n```js\nimport { transform, strings } from '@nanostores/i18n'\n\n// Add parameters syntax like hello: \"Hi, %1\"\nexport const paramsList = transform((locale, translation, ...args) =\u003e {\n  return strings(translation, str =\u003e {\n    return str.replace(/%\\d/g, pattern =\u003e args[pattern.slice(1)])\n  })\n})\n```\n\n```ts\nimport { paramsList } from '../lib/paramsList.ts'\n\nexport const messages = i18n('hi', {\n  hello: paramsList('Hello, %1')\n})\n```\n\n### Translation Process\n\nThe good I18n support is not about the I18n library,\nbut about translation process.\n\n1. Developer creates base translation in component’s source and export\n   it as `messages`.\n\n   ```ts\n   export const messages = i18n('welcome', {\n     hello: params('Hello, %1')\n   })\n   ```\n\n2. CI runs script to extract base translation to JSON.\n\n   ```ts\n   import { messagesToJSON } from '@nanostores/i18n'\n\n   const components = await glob('./src/*.tsx', { absolute: true })\n   const translations = await Promise.all(\n     components.map(async file =\u003e {\n       return await import(file).messages // Replace import if you export\n       // i18n() result with a different name\n     })\n   )\n   const json = messagesToJSON(...translations)\n   ```\n\n3. CI uploads JSON with base translation to online translation service.\n4. Translators translate application on this service.\n5. CI or translation service download translation JSONs to the project.\n\n### Lazy Loading\n\nIn general case developer pass `get` function like this to fetch all\ntranslations on locale change.\n\n```ts\nexport const i18n = createI18n(locale, {\n  async get(code) {\n    return fetchJSON(`/translations/${code}.json`)\n  }\n})\n```\n\nThen define `post` component with `i18n`.\n\n```ts\nexport const messages = i18n('post', {\n  post: 'Post details'\n})\n```\n\nMany application parts are rarely used, so there is a way to get\ntranslations for them partial.\n\n1.  We can use component names like `main/post` or `settings/user`.\n\n    ```ts\n    export const messages = i18n('main/post', {\n      post: 'Post details'\n    })\n    ```\n\n2.  We can define that components are more commonly used and give them\n    same prefixes like `main/heading` , `main/post` and `main/comment`.\n\n3.  Translations should be named:\n\n    ```js\n    // public/translations/ru/main.json\n    {\n      \"main/post\": {\n        \"post\": \"Данные о публикации\"\n      },\n      \"main/heading\": {\n        \"heading\": \"Заголовок\"\n      },\n      \"main/comment\": {\n        \"comment\": \"Комментарий\"\n      }\n    }\n    // public/translations/ru/settings.json\n    ```\n\n4.  During rendering `i18n` saves all component names that are used.\n    When locale changed `i18n` send names to `get` function.\n\n5.  We can pass `get` function that split the prefixes, filter unique\n    of them and make fetch for needed translations.\n\n        ```ts\n        export const i18n = createI18n(locale, {\n          async get(code, components) {\n            let prefixes = components.map(name =\u003e name.split('/')[0])\n            let unique = Array.from(new Set(prefixes))\n            return Promise.all(\n              unique.map(chunk =\u003e\n                fetchJSON(`/translations/${code}/${chunk}.json`)\n              )\n            )\n          }\n        })\n        ```\n\n6.  After each of new renderings `i18n` checks translations in cache.\n    If not in cache:\n    _ Splits component unique prefix or get name without prefix.\n    _ Checks if translations for it were fetched, but response not\n    received yet. \\* Calls `get` function for component name if needed -\n    `main` or `settings`.\n\n7.  Fetch will be called for all new rendered component with unique name. To prevent this we might want to give them same prefixes.\n\n### Server-Side Rendering\n\nFor SSR you may want to use own `locale` store and set `cache` options\nin custom `i18n` to avoid translations loading:\n\n```js\nimport { createI18n } from '@nanostores/i18n'\nimport { atom } from 'nanostores'\n\nlet locale, i18n\n\nif (isServer) {\n  locale = atom(db.getUser(userId).locale || parseHttpLocaleHeader())\n  i18n = createI18n(locale, {\n    async get () {\n      return {}\n    },\n    cache: {\n      fr: frMessages\n    }\n  })\n} else {\n  …\n}\n\nexport { locale, i18n }\n```\n\n### Server-Only Rendering\n\nWhen rendering content completely on the server without client hydration,\nyou can create a `loadTranslations` helper. It ensures the translations\nwill be loaded before you use them.\n\n```jsx\n// components/post.jsx\nimport { loadTranslations } from '@nanostores/i18n'\nimport { i18n } from '../stores/i18n.js'\n\nexport const messages = i18n('post', {\n  post: 'Post details'\n})\n\nasync function Post() {\n  const t = await loadTranslations(messages)\n  return \u003cspan\u003e{t.post}\u003c/span\u003e\n}\n```\n\n### Preprocessors\n\nYou can change all messages in your translation by preprocessors.\n\nFor instance, you can apply typography rules.\n\n```ts\nimport { createI18n, eachMessage } from '@nanostores/i18n'\n\nexport const i18n = createI18n(locale, {\n  …\n  preprocessors: [\n    eachMessage(str =\u003e str.toLocaleLowerCase())\n  ]\n})\n```\n\n### Processors\n\nYou can register own custom type for translations to choose translation\naccording to some state (and change translation on state changes).\n\nFor instance, here is an example of changing translation\ndepending on screen size:\n\n```js\n// stores/i18n.js\nimport { atom, onMount } from 'nanostores'\nimport { createI18n, createProcessor } from '@nanostores/i18n'\n\nconst screenSize = atom('big')\nonMount(screenSize, () =\u003e {\n  let media = window.matchMedia('(min-width: 600px)')\n  const check = () =\u003e {\n    screenSize.set(media.matches ? 'big' : 'small')\n  }\n  media.addEventListener('change', check)\n  return () =\u003e {\n    media.removeEventListener('change', check)\n  }\n})\n\nexport const size = createProcessor(screenSize)\n\nexport const i18n = createI18n(locale, {\n  get: …,\n  processors: [\n    size\n  ]\n})\n```\n\n```js\n// components/send-to-user.jsx\nimport { i18n, size } from '../stores/i18n.js'\n\nexport const messages = i18n({\n  send: size({\n    big: 'Send message',\n    small: 'send'\n  }),\n  name: 'User name'\n})\n\nexport const SendLabel = () =\u003e {\n  const t = useStore(messages)\n  return t.send()\n}\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnanostores%2Fi18n","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fnanostores%2Fi18n","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnanostores%2Fi18n/lists"}