Skip to content

Latest commit

 

History

History
105 lines (81 loc) · 4.9 KB

i18n.md

File metadata and controls

105 lines (81 loc) · 4.9 KB

Internationalisation

The app follows the device language. Three locales ship:

Locale Resource directory Base
English values/ —
Simplified Chinese values-b+zh+Hans/ —
Traditional Chinese values-b+zh+Hant/ Taiwan usage

Match by script, not by region

Use the BCP 47 qualifier (b+zh+Hans), not the legacy region form (values-zh-rCN). Region matching only covers the regions it names: zh-rCN serves mainland China and leaves a Simplified-reading user in Singapore or Malaysia on English. Script matching covers everyone who reads that script, wherever they are. Available since API 21, so it is free at minSdk 26.

Traditional Chinese uses Taiwan vocabulary. zh-Hant is not Simplified with converted characters — the words differ too: 軟體/軟件, 網路/網絡, 連線/連接. Taiwan usage is the base because it is the larger reading population; a separate values-b+zh+Hant+HK can be added later if Hong Kong usage justifies it. Never generate one from the other by character conversion: it produces text that is technically legible and obviously machine-made.

Per-app language

Android 13 (API 33) lets a user set a language for one app in system settings. Below that, the same thing is available through AndroidX. Support it either way with AppCompatDelegate.setApplicationLocales() plus a locales_config.xml listing the three locales — the backport handles pre-33 and the platform picks it up on 33+.

Worth having because a person's device language and their preferred reading language are not always the same, and this is exactly the kind of app someone runs on a phone set to a language they did not choose.

Machine identifiers stay English

This is the rule that matters most here. Translate the sentence, never the identifier.

Never translated — these are values, not prose:

  • The five SetupResult names: READY, INVALID_REQUEST, FLOW_LIMIT, DIAL_FAILED, INTERNAL_ERROR
  • Configuration parameters: net, morph, sni, pin, rate, etar, and the nowhere:// scheme
  • Carrier, transport and protocol names: TLS / TCP, QUIC, MORPH, PIN, nw3
  • Flow ids, hex fingerprints, host names, timestamps

Always translated — the sentence that explains the identifier:

FLOW_LIMIT This Portal is at its flow limit. Nothing is wrong with your setup — it is busy.

FLOW_LIMIT 这个 Portal 的连接数已满。你的配置没有问题 — 它只是忙。

The reason is searchability, not laziness. A user reporting a problem should be able to paste DIAL_FAILED and have it match the specification, the source, and someone else's log from a different locale. Translating it to 拨号失败 breaks every one of those links, and the person is no better off — the sentence beneath it already told them what happened in their own language.

Writing strings

  • No sentence assembly. Never build a message by concatenating fragments; word order differs between languages. One <string> per complete sentence, with positional arguments: <string name="quota_counted">%1$s of %2$s counted</string>.
  • Use plurals, even where English happens not to need them: <plurals name="active_flows">. Chinese has one form and English two; a hardcoded "$n flows" is wrong in both eventually.
  • Byte sizes, rates and durations are formatted, not concatenated. Use android.text.format.Formatter and NumberFormat so grouping separators and units follow the locale.
  • Keep the identifier out of the translatable string. Put DIAL_FAILED in the layout beside the message, not inside it, so a translator cannot accidentally localise it.
  • Mark what must not be translated: <string name="protocol_baseline" translatable="false">Nowhere v3.0.1</string>.

Layout under translation

Chinese runs roughly 30–40% shorter than English, which sounds harmless and is not: a layout tuned to Chinese leaves English clipping, and one tuned to English leaves Chinese looking sparse. Two habits cover it:

  • Nothing gets a fixed width sized to its current text. Rows are flex with gap; labels wrap rather than ellipsise where the sentence carries meaning.
  • The longest strings in the app are the rejection and certificate explanations. If they fit in both English and Chinese without the card growing, everything else will.

Chinese also needs a face with the glyphs — the app's Latin faces do not have them. Fall back to the system CJK face rather than bundling one; a bundled CJK font adds several megabytes for something every Android device already has.

Not doing yet

  • RTL. None of the three shipping locales is right-to-left, so it is not implemented. Use start/end rather than left/right in layouts anyway — it costs nothing now and is the whole job later.
  • Locale-specific number formatting in logs. Log lines stay machine-formatted: a timestamp in a log is a value to compare, not prose.