Translator brief
Hand this to whoever translates the app — including yourself with a machine translator, which is a legitimate first pass and never a last one.
Sections 1 and 2 are yours to write for this app. Everything from section 3 on is craft that
transfers between apps unchanged, and is the reason this file is worth having rather than a one-line
“please translate strings.xml”.
Translate from English only, never from another translation: every interpretation the first translator made gets compounded, silently.
1. What the app is, in one paragraph
<Write this. A translator who does not know what the app is for will produce fluent sentences that are wrong about the thing. Say who uses it and in what situation — copy for someone worried at 2am reads differently from copy for a productivity tool.>
2. The rules that outrank fluency
<Write these. They are the promises the copy makes that a translator must not soften, sharpen, or “improve”. Number them, and say what each one protects — a translator will follow a rule they understand the reason for and will helpfully fix one they do not. Shapes that recur:
- Never state something the app does not know. Where a value is absent because nobody entered it, the copy says so — it does not say everything is fine.
- The app observes; it never advises. A verb that becomes an imperative in translation changes what the app claims to be, and in some domains that is a regulatory question rather than a stylistic one.
- A unit that is always shown one way stays that way, however unnatural it reads, because the whole point is that a small change stays visible.>
3. Register
Decide it once and write it down, because it is the thing a translator most needs told and most often guesses.
- Who the app addresses, and how formally. Many languages force a choice English does not:
duorSie,tyorPan/Pani,tuorvous. Pick one and hold it across every string. - Whether the app speaks as “we” or is invisible. “We couldn’t save that” and “That couldn’t be saved” are different products.
- Sentence case or title case in buttons and headings. English tolerates both; most languages do not use title case at all, so a translated title-cased heading reads as a mistake.
4. Do not translate
Three kinds of string, marked translatable="false" in strings.xml so lint and the gate leave
them alone. If you find yourself wanting to translate one, that is worth a conversation rather than
an edit.
- The app’s own name.
- Language names in the switcher. They are endonyms — each language named in its own
language, in every locale — so someone who has landed in a language they cannot read can find
their way out by a name they recognise.
settings_language_englishreads “English” in every file. - Brand names, and anything with a legal identity. A product name that is the same in every market stays the same; translating it invents a different product.
Things that look untranslatable and are not
- Unit symbols. SI symbols look invariant and are not: spacing, the decimal separator, and sometimes the symbol itself change by locale. Keep them in resources.
- Numbers inside sentences. Grouping and decimal separators are locale-specific. Never build a number into a string by hand.
- Dates and times. Format them through the platform, never by concatenating a resource.
- Sample and debug data. Not resources at all — they live in
src/debug/and never ship.
5. Vocabulary
Keep a table here of every English word that appears more than once, with the target word for each context. This is the single highest-value section of the file.
The trap it exists for: one English word is often several target words. English Normal covers six different nouns in six different genders; a translator working string-by-string will produce a different form each time, all of them defensible, and the app will look sloppy in a way no test catches. Likewise Name: a person’s given name and an organisation’s name are different words in most languages.
| English | Resource ids | Why they differ | Example |
|---|---|---|---|
| Loan | home_add_loan, add_loan_title, add_loan_discard_title, add_loan_failed, home_empty, counterparty_no_loans, settings_currency_not_set, settings_currency_refused, settings_restore_confirm_body |
One word for both Directions, inflected by case. It is feminine, which is why Lent, Borrowed and Settled are feminine adjectives. Never dług or kredyt | pożyczka: Dodaj pożyczkę, Nie ma jeszcze pożyczek |
| Lent | direction_lent, add_loan_direction_pot_hint |
An adjective agreeing with pożyczka, never a verb: pożyczyłeś and pożyczyłaś would give the owner a gender (§6.2) | Udzielona |
| Borrowed | direction_borrowed, add_loan_direction_pot_hint |
The same reason as Lent | Zaciągnięta |
| Settled | loan_settled |
Agrees with pożyczka. Not spłacona or zamknięta: a Loan is Settled, only a Repayment is paid | Rozliczona |
| Outstanding | loan_outstanding |
What is left on one Loan. Not saldo, which is the Balance across a Counterparty’s Loans | Do spłaty: %1$s |
| Amount | add_loan_amount, add_loan_amount_empty, add_loan_amount_zero, add_loan_amount_too_large, add_loan_no_currency, settings_currency_change_body, settings_currency_refused, settings_currency_refused_unknown |
Inflected by case. Not suma, which reads as a total | Kwota, Wpisz kwotę |
| Day | add_loan_day |
The calendar day a Loan was made, with no time of day. Not data, which also names a timestamp | Dzień |
| Currency | settings_currency, settings_currency_search, settings_currency_change_title, settings_currency_refused_title, settings_currency_refused, settings_currency_refused_unknown, add_loan_no_currency, add_loan_choose_currency |
Inflected by case | Waluta, Wybierz walutę, Szukaj waluty |
| Who? | add_loan_who |
The UI word for a Counterparty, which never appears on screen. A question, not a label | Kto? |
| Person | add_loan_kind_person |
The kind of Counterparty that is somebody. Not kontakt: the phone’s contacts are never read | Osoba |
| Pot | add_loan_kind_pot, counterparty_pot_icon, add_loan_direction_pot_hint |
The UI says shared money, and pot is its gloss. Not konto, portfel or fundusz | Wspólne pieniądze (pula), dołożone do puli |
Words that genuinely are one word everywhere — Settings, Close, Edit, Delete — belong in the same table with a note saying so, otherwise someone will vary them for variety.
6. Traps
6.1 A substituted fragment has to fit its sentence
English composes freely because it barely inflects. "in the last %1$s" with "30 days" works;
the same substitution in an inflecting language needs the fragment in the right case, and the case
depends on the host sentence.
Pre-inflect the fragment for its host, and say in a comment which host it belongs to. If one fragment is used by two hosts that need different cases, it needs to become two resources — that is a code change, and it is cheaper to find now than after the translation.
6.2 The app knows nobody’s gender
Not the user’s, not the subject’s, not any third party’s. English hides this behind they and you. Most other languages do not, and a translator forced to choose will pick one, producing copy that is wrong for half the users and reads as an apology to the rest.
Every string with a human doing something in it needs checking, and the fix is usually to rewrite the English into a form that does not need a gender — a noun phrase instead of a verb with a subject. That is a change to the base language, made once, rather than a workaround in eight files.
6.3 Names the app cannot touch
Anything the user typed — a name, a label, a note — is substituted verbatim into a sentence. It cannot be inflected, so the sentence has to be built so that it does not need to be. Watch for possessives and for any preposition that would govern a case.
6.4 Plurals
A count-dependent string is a <plurals>, never string concatenation, and never if (n == 1).
Which categories a language uses is CLDR’s decision, not the translator’s. English has one and
other; Polish has one, few, many and other; Arabic has six. Supplying only the English pair
in a language that needs four is a silently wrong app — TranslationTest fails the build on it,
which is the whole reason that test exists.
%d is not always the right placeholder either: some categories in some languages read better
without the number, and quantity="one" in Polish means exactly one, not “the singular”.
6.5 Format arguments
- Positional (
%1$s), never bare (%s), whenever there is more than one. Word order changes; a translator must be able to reorder the arguments, and bare placeholders make that impossible. - The same arguments must appear in every locale. A missing one crashes at format time, on a
screen you did not test in that language.
TranslationTestchecks this continuously. - A literal
%must be escaped as%%.
7. What happens to your draft
- It goes into
res/values-<tag>/strings.xml, or — while it is still a draft — intotranslations/<tag>/one directory up, which is staged and reported but never shipped. TranslationTestchecks it mechanically: format arguments, plural categories, orphans, untranslatable resources, array lengths. This runs on every build.scripts/translation-gate.pychecks it is complete, and that nothing has gone stale — the English changed and the translation did not. A stale translation is invisible to every other check: it parses, it carries its arguments, and it says something the app no longer means.- The language is offered to users only when its tag is added to
res/xml/locales_config.xmlandAppLanguage. Until then it exists and reaches nobody, which is the correct state for a draft.
8. What a language ships on
An audit against §2, a look at it on a phone, and a route back — a native speaker’s read-through when you can get one, but not as the gate. The first app built from this template promised a native read-through per language and shipped nine languages; seven reviewers never materialised, and the promise had one outcome — mechanically green drafts sitting unshipped while they rotted against every English string added after them. A read-through was doing two jobs. Fluency needs a native speaker; the rules that outrank fluency (§2) are a bounded checklist answerable from this brief and the string. So the second half is the gate, and the first half gets a channel after shipping.
Before a language goes into locales_config.xml:
- Every string that §2 governs is read against §2, in that language. Nothing softened, sharpened or promised beyond what the app can know.
- The §5 table is checked for consistency, because a term translated two ways is the failure that makes an app look sloppy in a way no test catches.
- Every string that names another — a button, a section header — uses that string’s exact word.
Both halves read fluently on their own, so only the pairing is wrong. Once a pairing is found, add it
to
quotedPairsinTranslationTest, which holds it in every locale from then on. - It is compiled, installed and looked at. A staged draft has never met
aapt2: clipped labels and lint’s plural warnings are invisible until it does, and neither needs a native speaker to see.
After it ships, a way to report a bad string is how fluency findings arrive, as ordinary bug
reports. That route is already wired: Settings → Language → Something read wrong? opens a mail
tagged #language with the resolved locale already in it (ui/support/SupportHandoff.kt), which is
the one fact such a report cannot do without and the one nobody should have to type. It sits beside
the picker rather than on the Support screen because that is where somebody is standing the moment a
translation reads wrong. What a report must not do is talk
the app back across §2: the reporter has the fluency, and this file has the reasoning.
If this is not your policy, replace this section and say so in ADR-0004 — translation-gate.py
cites it as the thing no script stands in for.