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 |
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.
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.
This is the rule that matters most here. Translate the sentence, never the identifier.
Never translated — these are values, not prose:
- The five
SetupResultnames:READY,INVALID_REQUEST,FLOW_LIMIT,DIAL_FAILED,INTERNAL_ERROR - Configuration parameters:
net,morph,sni,pin,rate,etar, and thenowhere://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_LIMITThis 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.
- 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.FormatterandNumberFormatso grouping separators and units follow the locale. - Keep the identifier out of the translatable string. Put
DIAL_FAILEDin 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>.
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.
- RTL. None of the three shipping locales is right-to-left, so it is not
implemented. Use
start/endrather thanleft/rightin 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.