OneScript
Foundation

i18n

Show your script's text in the device language.

onescript/i18n picks one of your languages based on system.locale.languages (the device's language preferences), and provides message lookup, interpolation, plurals, and date and number formatting.

Quick start

Add a JSON file for each language to your project:

src/
  app.tsx
  locales/
    en.json
    zh-Hans.json

locales/en.json:

{
  "greeting": "Hello, {name}!",
  "cart": {
    "items": { "one": "{count} item", "other": "{count} items" }
  }
}

locales/zh-Hans.json:

{
  "greeting": "你好,{name}!",
  "cart": {
    "items": { "other": "{count} 件商品" }
  }
}

src/app.tsx:

import { createI18n } from "onescript/i18n"
import en from "./locales/en.json"
import zhHans from "./locales/zh-Hans.json"

const i18n = createI18n({
  fallback: "en",
  messages: { en, "zh-Hans": zhHans },
})

// When the device language is English:
i18n.locale                           // "en"
i18n.t("greeting", { name: "Ada" })   // "Hello, Ada!"
i18n.t("cart.items", { count: 1 })    // "1 item"
i18n.t("cart.items", { count: 3 })    // "3 items"

// When the device language is Simplified Chinese:
i18n.locale                           // "zh-Hans"
i18n.t("cart.items", { count: 3 })    // "3 件商品"

Messages

  • Messages are nested objects, and keys are dot paths such as "cart.items". Keys are type-checked against the fallback language, so a typo is reported in the editor.
  • {name} is replaced with the value you pass. Numbers are formatted for the message language, for example 1234 becomes 1,234 in English. A missing value throws an error.
  • An object with an other key is a plural message and needs a count. The branch is chosen with Intl.PluralRules (zero, one, two, few, many, other); if that branch is missing, other is used.
  • Other languages may leave out some messages. A missing message uses the fallback language and logs a warning once per key.

Language matching

createI18n checks system.locale.languages in order and uses the first language that matches. If none matches, it uses fallback.

  • Regions don't need to match: en-GB matches en, and pt-BR matches pt.
  • Chinese is matched by script: zh-CN matches zh-Hans, and zh-TW and zh-HK match zh-Hant.
  • Different scripts never match: if only zh-Hans is available, a Traditional Chinese user moves on to their next preferred language instead of seeing Simplified Chinese.

With en, zh-Hans, and zh-Hant messages and fallback: "en":

system.locale.languagesi18n.locale
["zh-CN"]"zh-Hans"
["zh-HK"]"zh-Hant"
["en-GB"]"en"
["ja-JP", "zh-TW"]"zh-Hant" (no Japanese, so the next preference is used)
["ja-JP"]"en" (nothing matches, so fallback is used)

To ignore the device language, set locale:

const i18n = createI18n({
  fallback: "en",
  locale: "zh-Hans",
  messages: { en, "zh-Hans": zhHans },
})

i18n.locale // always "zh-Hans"

Dates and numbers

i18n.date(value, options) and i18n.number(value, options) format with the same language as your messages. options are the same as for Intl.DateTimeFormat and Intl.NumberFormat.

const date = new Date(2026, 9, 7)

// When i18n.locale is "en":
i18n.date(date, { dateStyle: "long" })                    // "October 7, 2026"
i18n.number(1234.5)                                       // "1,234.5"
i18n.number(9.5, { style: "currency", currency: "USD" })  // "$9.50"
i18n.number(0.25, { style: "percent" })                   // "25%"
i18n.number(12500, { notation: "compact" })               // "13K"

// When i18n.locale is "zh-Hans":
i18n.date(date, { dateStyle: "long" })                    // "2026年10月7日"
i18n.number(12500, { notation: "compact" })               // "1.3万"

API

createI18n(options):

OptionTypeDescription
fallbackstringThe language with the most complete messages. Used when another language is missing a message; keys are typed from it.
messagesRecord<string, object>Messages for each language, keyed by language tag such as "en" or "zh-Hans".
localestringOptional. Use this language instead of the device language.

Return value:

MemberDescription
localeThe matched language.
t(key, values?)Returns a message. values are used for interpolation and plurals.
date(value, options?)Formats a date for locale.
number(value, options?)Formats a number for locale.

matchLocale(requested, available, fallback) is also exported to apply the same matching rules on its own. For example, matchLocale(["zh-TW"], ["en", "zh-Hant"], "en") returns "zh-Hant".

Using other libraries

For ICU MessageFormat and other advanced features, install a library such as i18next and choose the language with system.locale.languages:

import i18next from "i18next"
import { system } from "onescript"

await i18next.init({
  lng: system.locale.languages[0],
  fallbackLng: "en",
  resources: {
    en: { translation: { greeting: "Hello, {{name}}!" } },
  },
})

How is this guide?

Last updated on

On this page