{"id":19165474,"url":"https://github.com/mxjp/u27n-core","last_synced_at":"2025-08-16T23:11:59.103Z","repository":{"id":57168687,"uuid":"384937969","full_name":"mxjp/u27n-core","owner":"mxjp","description":"Universal localization framework","archived":false,"fork":false,"pushed_at":"2025-02-06T20:27:59.000Z","size":970,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-08-08T23:47:23.742Z","etag":null,"topics":["g11n","globalization","i18n","internationalization","l10n","localization","translation","u27n"],"latest_commit_sha":null,"homepage":"https://www.npmjs.com/package/@u27n/core","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/mxjp.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":"2021-07-11T12:05:07.000Z","updated_at":"2025-02-06T20:28:03.000Z","dependencies_parsed_at":"2023-10-16T03:23:18.183Z","dependency_job_id":"bcf9e5e8-7918-4a2b-bc55-a5db73e49a1b","html_url":"https://github.com/mxjp/u27n-core","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/mxjp/u27n-core","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mxjp%2Fu27n-core","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mxjp%2Fu27n-core/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mxjp%2Fu27n-core/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mxjp%2Fu27n-core/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/mxjp","download_url":"https://codeload.github.com/mxjp/u27n-core/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mxjp%2Fu27n-core/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":270781393,"owners_count":24643820,"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-08-16T02:00:11.002Z","response_time":91,"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":["g11n","globalization","i18n","internationalization","l10n","localization","translation","u27n"],"created_at":"2024-11-09T09:27:59.528Z","updated_at":"2025-08-16T23:11:59.083Z","avatar_url":"https://github.com/mxjp.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# U27N Core\nU27N is a _universal internationalization_ framework that aims to provide an end to end solution for authoring, maintaining and shipping translations by using the following workflow:\n+ Code is written in a single locale of choice.\n+ U27n keeps track of all translatable text fragments by assigning project wide unique ids.\n+ Translations are written in an external editor or directly in your IDE and then bundled for runtime use.\n\n## Content\n+ [Configuration](#configuration)\n+ [Command Line Interface](#command-line-interface)\n+ [Runtime API](#runtime-api)\n  + [Controller](#controller)\n  + [Context](#content)\n  + [Text Fragments](#text-fragments)\n    + [Interpolation \u0026 Formatting](#interpolation--formatting)\n    + [Pluralization](#pluralization)\n  + [Concurrent Locales](#concurrent-locales)\n  + [Setting the document locale](#setting-the-document-locale)\n  + [Usage with SolidJS](#usage-with-solidjs)\n  + [Usage with React / Preact](#usage-with-react--preact)\n+ [Toolchain API](#toolchain-api)\n  + [Configuration](#configuration-1)\n  + [Plugins](#plugins)\n    + [Sources](#sources)\n    + [Data Adapters](#data-adapters)\n  + [Projects](#projects)\n+ [Changelog](./CHANGELOG.md)\n\n## Packages\n+ [@u27n/typescript](https://www.npmjs.com/package/@u27n/typescript) - Plugin for handling typescript and javascript source code.\n+ [@u27n/webpack](https://www.npmjs.com/package/@u27n/webpack) - Webpack plugin and runtime.\n\n\u003cbr\u003e\n\n\n\n# Configuration\nThe configuration is stored in a file usually called **u27n.json**:\n```js\n{\n  // Optional. The filename where to store translation data.\n  // This filename may be used differently or not at all if a custom data adapter is used.\n  \"data\": \"./u27n-data.json\",\n\n  // Optional. The namespace for this project. This should be\n  // a unique string such as an npm package name.\n  // (Default is an empty string)\n  \"namespace\": \"\",\n\n  // Optional. An array of patterns which sources are translated.\n  // Patterns should be picomatch compatible.\n  \"include\": [\n    \"./src/**/*\"\n  ],\n\n  // An array of locales.\n  // The first locale is the one that source code is written in.\n  // (Default is [\"en\"])\n  \"locales\": [\n    \"en\",\n    \"de\"\n  ],\n\n  // An array of plugins modules:\n  // (Default is [])\n  \"plugins\": [\n    // Without config:\n    \"@u27n/typescript\",\n\n    // Or with config:\n    {\n      \"entry\": \"@u27n/typescript\",\n      \"config\": {\n        // ...\n      }\n    }\n  ],\n\n  \"obsolete\": {\n    // Optional. Controls what obsolete translations are discarded.\n    // + \"untranslated\": Discard obsolete fragments that have no translations.\n    // + \"outdated\": Discard obsolete fragments that\n    //               have no or only outdated translations.\n    // + \"all\": Discard all obsolete fragments.\n    \"discard\": \"all\"\n  },\n\n  \"output\": {\n    // Optional. The filename where to store bundled locales.\n    // \"[locale]\" is replaced with the target locale.\n    //\n    // If this is set to null, no output is written.\n    \"filename\": \"./dist/locale/[locale].json\",\n\n    // Optional. If true, outdated translations are not\n    // included in the locale bundles.\n    \"includeOutdated\": false,\n\n    // Optional. The directory where to store the output manifest.\n    //\n    // If this is set to null, no output manifest is written.\n    \"manifestPath\": \"./dist\",\n  },\n\n  // Optional. An object to configure diagnostic severity:\n  //\n  // Supported severities are:\n  // - \"error\": Show as an error. This will cause the cli process to exit with \"1\" when building.\n  // - \"warning\": Show as a warning.\n  // - \"info\": Show as information.\n  // - \"ignore\": Ignore the diagnostic.\n  //\n  // By default, all diagnostics are treated as errors.\n  \"diagnostics\": {\n    // Fallback for unconfigured diagnostics:\n    \"*\": \"error\",\n\n    // Severity for specific diagnostics:\n    \"outdatedTranslations\": \"warning\",\n  }\n}\n```\n\n# Command Line Interface\n```bash\nnpx u27n [...options]\n\n# Usually, one of the following commands is used:\n\n# During development:\nnpx u27n --watch\n# Or to run diagnostics and bundle locales:\nnpx u27n\n```\n+ `--config \u003cfilename\u003e`: Specify the config filename.\n+ `--watch`: Watch for changes during development.\n+ `--no-output`: Disable writing output bundles.\n+ `--modify`/`--no-modify`: Enable or disable updating source code when unique ids must be assigned or changed.\n  This is automatically enabled in watch mode.\n+ `--delay`: Time to wait in milliseconds after changes on disk are detected. Default is 100.\n\n\u003cbr\u003e\n\n\n\n# Runtime API\n\n## Controller\nUsually, there is one controller per web application that loads and manages locale data.\n```ts\nimport { U27N, FetchClient, defaultLocaleFactory } from \"@u27n/core/runtime\";\n\n// Create a global controller:\nconst u27n = new U27N({\n  // An array of clients that are used to load locale data:\n  clients: [\n    new FetchClient(\"/locale/[locale].json\"),\n  ],\n\n  // A function that is used to create new locale instances:\n  localeFactory: defaultLocaleFactory,\n});\n\n// When your application loads, detect and load the locale:\nawait u27n.setLocaleAuto([\"en\", \"de\"]);\n```\n\n## Context\nA context provides translation functions for a specific namespace.\n```ts\nimport { Context } from \"@u27n/core/runtime\";\n\n// Create a context for the namespace \"example\" and source locale \"en\":\nconst context = new Context(u27n, \"example\", \"en\");\n\n// Export the translation function for use in other modules:\nexport const t = context.t;\n```\n\n## Text Fragments\nThe **t** function of the controller is used to get a translation from the controller or just return the value if the current locale is the source locale of the context.\n\n_Note, that fragment ids are omitted from the following code examples._\n```ts\nt(\"simple text\");\n```\n\n### Interpolation \u0026 Formatting\nIf a `fields` object is passed, interpolation and formatting is enabled:\n```ts\nt(\"The current time is {now}\", { fields: {\n  now: new Date().toLocaleTimeString(u27n.locale.code),\n} });\n```\n\nTo automatically format values, formatters with a name or for specific value types can be registered on the controller or passed directly to the translation function:\n```ts\n// Register a named formatter:\nu27n.formatters.set(\"Time\", (value, locale) =\u003e {\n  return value.toLocaleTimeString(locale.code);\n});\n\n// Register a formatter for a specific prototype:\nu27n.formatters.set(Date, (value, locale) =\u003e {\n  return value.toLocaleTimeString(locale.code);\n});\n\n// Register a formatter for a specific primitive type:\nu27n.formatters.set(\"number\", (value) =\u003e {\n  return String(Math.floor(value * 100) / 100);\n});\n\n// Use a named formatter:\nt(\"The current time is {now, Time}\", { fields: { now: new Date() } });\n// Or select a formatter based on the value type:\nt(\"The current time is {now}\", { fields: { now: new Date() } });\nt(\"{value} meters\", { fields: { value: 42.1234 } });\n```\n\n### Pluralization\nIf a `count` option is passed the correct plural form for the current locale is selected:\n```ts\nt([\"apple\", \"apples\"], { count: 42 });\n```\nInterpolation and formatting can also be used in plural values:\n```ts\nt([\"{count} apple\", \"{count} apples\"], { count: 42 });\n```\n\nNote, that the number and order of plural forms depends on the locale. Pluralization is supported for all locales defined in [this file](./resources/plurals.json5).\n\n## Concurrent Locales\nFor things like server side rendering, it may be necessary to switch between locales depending on external factors e.g. which user makes a request. For this purpose, it is recommended to use multiple controllers in parallel and pass the translation function for the correct locale to the part of the application that needs to be translated:\n```tsx\nasync function createLocale(locale) {\n  const u27n = new U27N({\n    clients: [\n      new FetchClient(\"/locale/[locale].json\"),\n    ],\n    localeFactory: defaultLocaleFactory,\n  });\n  await u27n.setLocale(locale);\n  const context = new Context(u27n, \"example\", \"en\");\n  return context.t;\n}\n\n// The following code shows how this setup could be used:\n\nconst locales = {\n  en: await createLocale(\"en\"),\n  de: await createLocale(\"de\"),\n};\n\nfunction renderPage(t) {\n  return \u003cPage\u003e\n    \u003ch1\u003e{t(\"Hello World!\", \"42\")}\u003c/h1\u003e\n  \u003c/Page\u003e;\n}\n\nserver.onRequest(user =\u003e {\n  return renderPage(locales[user.locale] ?? locales.en);\n});\n```\n\n## Setting the document locale\nA web page should indicate it's current language by setting the **lang** attribute.\n```ts\nu27n.updateHandlers.add(() =\u003e {\n  document.documentElement.lang = u27n.locale?.code ?? \"\";\n});\n```\n\n## Usage with SolidJS\n```tsx\nimport { createSignal } from \"solid-js\";\nimport { wrapSignalT } from \"@u27n/core/runtime\";\n\n// Create a signal that is updated when the locale changes:\nconst [useLocale, setLocale] = createSignal(u27n.locale);\nu27n.updateHandlers.add(() =\u003e {\n  setLocale(u27n.locale);\n});\n\n// Wrap the translation function to use the locale signal:\nexport const t = wrapSignalT(context.t, useLocale);\n\nfunction Example() {\n  return \u003ch1\u003e{t(\"Hello World!\")}\u003c/h1\u003e;\n}\n```\n\n## Usage with React / Preact\n```tsx\n// When using react:\nimport { signal } from \"@preact/signals-react\";\n// When using preact:\nimport { signal } from \"@preact/signals\";\n\nimport { wrapSignalT } from \"@u27n/core/runtime\";\n\n// Create a signal that is updated when the locale changes:\nconst localeSignal = signal(u27n.locale);\nu27n.updateHandlers.add(() =\u003e {\n  localeSignal.value = u27n.locale;\n});\n\n// Wrap the translation function to use the locale signal:\nexport const t = wrapSignalT(context.t, () =\u003e localeSignal.value);\n\nfunction Example() {\n  return \u003ch1\u003e{t(\"Hello World!\")}\u003c/h1\u003e;\n}\n```\n\n\u003cbr\u003e\n\n\n\n# Toolchain API\nThe toolchain API is exported by the `@u27n/core` package and can be used to implement alternatives to the command line interface such as the [@u27n/webpack](https://www.npmjs.com/package/@u27n/webpack) package.\n\n## Configuration\n```ts\nimport { Config } from \"@u27n/core\";\n\n// Read and validate a config file:\nconst config = await Config.read(\"./u27n.json\");\n\n// Create a validated config object programmatically:\nconst config = await Config.fromJson({\n  include: [\n    \"./src/**/*\"\n  ],\n  locales: [\n    \"en\",\n    \"de\"\n  ],\n}, process.cwd());\n```\n\n## Plugins\n\n### Sources\nPlugins can be used to implement how specific source files are handled. Sources can be implemented manually or\nby extending the [SourceBase](./src/source-base.ts) or [TextSource](./src/text-source.ts) classes.\n```ts\nimport { Plugin } from \"@u27n/core\";\n\nexport default class ExamplePlugin implements Plugin {\n  createSource(context: Plugin.CreateSourceContext) {\n    if (context.filename.endsWith(\".txt\")) {\n      // It is usually required to read the text content of the source file from disk:\n      const content = await context.getTextContent();\n\n      // The result must be an object implementing the \"Source\" interface or\n      // undefined, to indicate that this plugin can't handle the specified file:\n      return new ExampleSource(content);\n    }\n  }\n}\n```\n\n### Data Adapters\nData adapters provide the interface between a project and the translation data storage. The [default data adapter](./src/data-adapter-default.ts) stores all translation data in a single git friendly file usually called `u27n-data.json`. Plugins have the ability to overwrite the project's data adapter during initialization.\n```ts\nimport { Plugin } from \"@u27n/core\";\n\nexport default class ExamplePlugin implements Plugin {\n  setup(context: Plugin.Context) {\n    // \"customAdapter\" must be an object implementing the \"DataAdapter\" interface.\n    context.setDataAdapter(customAdapter);\n  }\n}\n```\n\n## Projects\nProjects provide a high level API for compiling locales, updating translation data and watching for changes.\n```ts\nimport { Project } from \"@u27n/core\";\n\n// Create a project instance:\nconst project = await Project.create({\n  config,\n  dataAdapter,\n});\n\n// Run once:\n// (Diagnostics can be accessed via the result object)\nconst result = await project.run({\n  // If true, output files are generated:\n  output: true,\n  // If true, sources and translation data may be modified:\n  modify: false,\n  // If true, additional diagnostics for data fragments are collected:\n  fragmentDiagnostics: true,\n});\n\n// Run and watch for changes:\nconst stop = await project.watch({\n  // Same options as in project.run(..) with the addition of:\n\n  // Passed to the file system abstraction as a delay when watching for changes:\n  delay: 100,\n\n  // Called for every critical error that occurs:\n  onError(error: unknown) {\n    // ...\n  },\n\n  // Called when a set of changes has been processed.\n  // This function may return a promise that is\n  // awaited before processing further change sets.\n  async onFinish(result: WatchResult) {\n    // ...\n  },\n});\n\n// Stop watching for changes:\nawait stop();\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmxjp%2Fu27n-core","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmxjp%2Fu27n-core","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmxjp%2Fu27n-core/lists"}