ドキュメント
I18n
An NSH app speaks its users’ languages in two ways: translations, the app’s own text in each language, and formatting, numbers, currencies and dates written as each language and region writes them. Both follow the language the app shows, which is the user’s own until the app picks another.
Translations use rust-i18n’s files and its API, in TSX and in Rust alike:
put the files in locales/, call t("hello"), and the text shows in the user’s language.
import { Text } from "nsh";
import { t } from "nsh/i18n";
export default function App() {
return <Text>{t("messages.hello", { name: "Jason" })}</Text>;
}use nsh_core::prelude::*;
fn greeting() -> impl IntoElement {
text(move || t!("messages.hello", name = "Jason"))
}#Translation files
Translations live in a locales/ directory beside the app’s nsh.json (beside Cargo.toml in a Rust app). Nothing
needs configuring: the build reads every file there and the app carries them. The files are rust-i18n’s, so the
same files serve an app’s Rust crates, its server or its other tools.
my-app/
nsh.json
App.tsx
locales/
app.yml
One file can hold every language (_version: 2), each key followed by its text per language:
# locales/app.yml
_version: 2
hello:
en: Hello
zh-CN: 你好
ja: こんにちは
messages.hello:
en: Hello, %{name}!
zh-CN: 你好,%{name}!
ja: こんにちは、%{name}!
Or one file per language (_version: 1, the default), its name ending with the language (en.yml, app.zh-CN.yml)
and keys nested as objects:
# locales/zh-CN.yml
hello: 你好
messages:
hello: 你好,%{name}!
Files may be YAML (.yml, .yaml), JSON (.json) or TOML (.toml), and sit in subdirectories of locales/. Nested
keys join with dots: messages.hello above. A large app splits them by area (locales/settings.yml,
locales/checkout.yml); keys of one language from several files merge.
During nsh run and nsh dev, saving a file in locales/ rebuilds the app like saving its code: the running app
shows the new text.
#t
t(key, options?) is the text of key in the language the app shows.
import { t } from "nsh/i18n";
t("hello"); // "Hello", "你好", ...
t("messages.hello", { name: "Jason" }); // "Hello, Jason!": %{name} filled
t("hello", { locale: "ja" }); // "こんにちは": another language
t!("hello");
t!("messages.hello", name = "Jason");
t!("hello", locale = "ja");
- Arguments fill the
%{name}patterns of the text. Values are written as text (String(value)in TSX,Displayin Rust); a pattern without an argument stays as it is. localeasks for the text in another language than the app shows.- Fallback, as rust-i18n’s: a key a language lacks comes from the language without its last subtag (
zh-Hant-TW, thenzh-Hant, thenzh), then from English (en). Where no language has the key,treturns<locale>.<key>("fr.messages.hello"), so a missing translation shows on screen rather than an empty space. - Reactive:
treads the language, so a text, a style or a computed value that calls it updates when the language changes, and when the translations change during development. Call it where the text is shown (<Text>{t("hello")}</Text>,text(move || t!("hello"))), not once into a constant.
Translations are ready before any of the app’s modules runs, so t works at the top level of a module too (its
result is then computed once).
#The language the app shows
By default the app’s text shows in the user’s language: the first of the user’s preferred languages
(localeSettings().languages, most preferred first, as the system lists them) that the app has translations for,
itself or a shorter tag of it. A user who prefers Swiss German, then Chinese, then English sees an app’s zh text
when it has no German, and its de text when it has. Where the app has none of them, the text shows in locale()’s
language (the device’s region format), which falls back to English as described above.
setLocale shows the app in a language the user picks inside it, whatever the device’s:
import { Button } from "nsh/base";
import { Text, View } from "nsh";
import { availableLocales, setLocale, t } from "nsh/i18n";
export function LanguagePicker() {
return (
<View>
<Each of={availableLocales()}>
{(locale) => (
<Button onPress={() => setLocale(locale())}>
<Text>{t("language.name", { locale: locale() })}</Text>
</Button>
)}
</Each>
</View>
);
}use nsh_core::{platform::Locale, prelude::*};
fn use_chinese() {
set_locale(Locale::parse("zh-CN"));
}
fn follow_the_device() {
set_locale(None);
}setLocale("zh-CN")changes everything that follows the language at once:t,locale(), the first oflocaleSettings().languagesandIntl’s default locale. A tag that is not BCP 47 throws aRangeError.setLocale(null)(Rust:set_locale(None)) follows the device again.availableLocales()(Rust:available_locales()) lists the languages of the app’s translations, sorted.
The choice lasts while the app runs; an app that remembers it saves it (Storage) and
calls setLocale when it starts. Each surface has its own language: an app embedded twice can show two.
#Numbers and dates
Intl and toLocaleString format for the language the app shows by default. Pass locale() as their locale, so
that what writes a number reads the language and updates when setLocale changes it:
import { locale } from "nsh";
const price = (amount: number) =>
amount.toLocaleString(locale(), { style: "currency", currency: "EUR" });
// en-US: "€1,234.50" de: "1.234,50 €" zh-CN: "€1,234.50"
Native Shell’s runtime formats English (en-US) and its regional variants itself. Every other language’s data (its
separators, compact units, date patterns, month and weekday names) comes with the app: the languages of its
translations by default, or the ones nsh.json’s locales lists.
{
"id": "com.example.shop",
"locales": ["de", "fr", "ja", "zh-CN"]
}
- Without
locales, the app formats in the languages it has translations for (locales/above), and in English. - With
locales, it formats in exactly the languages listed, and English: list the languages your users format in when they differ from those you translate (an English-only app that writes German prices), or to leave some out. - A language not carried formats as English (
en-US), as a locale without data always has. The build warns about a listed language Native Shell has no data for.
Each language adds a few kilobytes to the bundle, and only the app that lists it carries it: an app formats in the languages its users speak, never in every language Native Shell knows.
The languages with data: German (de), Spanish (es), French (fr), Hindi (hi), Indonesian (id), Italian
(it), Japanese (ja), Korean (ko), Dutch (nl), Polish (pl), Portuguese (pt), Russian (ru), Swedish
(sv), Thai (th), Turkish (tr), Vietnamese (vi) and Chinese (zh, simplified and traditional), with their
regional variants (de-CH, es-MX, pt-PT, zh-TW, …). A new language is one data file in Native Shell’s
nsh-i18n crate (crates/nsh-i18n/data/), contributed like any other change.
On the web, the browser’s own Intl formats every language, whatever locales lists.
nsh package also declares these languages to iOS and macOS (the bundle’s CFBundleLocalizations), so the system’s
own UI in the app, such as the edit menu of selected text, shows in the language the app does: a user whose system
is in Chinese sees 全选 in an app that has Chinese, and English in one that does not.
See Number and date formatting for what Intl formats, and
Device and environment for the user’s other settings: the time zone,
12- or 24-hour time, the first day of the week and the units.
#Rust apps
A Rust app’s locales/ sits beside its Cargo.toml; nsh run, nsh test and nsh package read it and hand
the translations to the root (Root::translations), so t! works without any setup. A Rust app built by Cargo
alone (a host that embeds the crate) passes its own:
use nsh_core::{element::Root, i18n::Translations};
let translations = Translations::from_json(include_str!("../translations.json")).unwrap();
let root = Root::of::<App>().translations(translations);
t! here is Native Shell’s, not rust-i18n’s: it reads the language of the surface it runs on (each surface has its
own), and a view that shows it follows set_locale. Its forms are rust-i18n’s: t!("key"),
t!("key", name = value), t!("key", locale = "ja").
A TSX app’s Rust components translate with the same t!: they share the app’s locales/.
#Testing
nsh test opens an app in en-US unless open’s locale says otherwise; a test of another language opens it in
that one, or calls setLocale as the app does:
import { open, test } from "nsh/test";
test("the greeting is translated", async () => {
const app = await open("./App.tsx", { locale: "zh-CN" });
await app.waitFor("text:你好");
});
#Checklist
- Every string the user reads comes from
t(labels, placeholders, accessibility labels, alerts’ buttons), never a literal in a component. - Keys name what the text is for (
checkout.pay,errors.network), not the English text, so English can change without renaming keys. - Whole sentences go in one key with
%{name}arguments, never pieced together from several: word order differs between languages. - Numbers, prices and dates go through
Intl/toLocaleString, never written with+andtoFixed. - Layouts leave room: German and Finnish text runs much longer than English, Chinese and Japanese shorter.
nsh.json’slocaleslists the languages users format in when they differ from the translated ones.