CallShield ships in English, Simplified Chinese, Brazilian Portuguese and Spanish. New languages are welcome, and partial ones help. Android falls back to English per string, so a translation that covers the screens people actually look at is better than none.
The Portuguese and Spanish text hasn't had a native speaker's review yet. If something reads wrong, a PR that fixes a few lines is as useful as a whole new language.
The general translation thread is issue #7. Comment there to claim a language so two people don't translate the same 1,800 strings.
The source of truth is app/src/main/res/values/strings.xml (about 1,800 strings
and 50 <plurals>). Create app/src/main/res/values-<locale>/ and put your
translated <string> and <plurals> elements there. Either layout works,
everything in one strings.xml or split into strings.xml + plurals.xml.
Resource filenames carry no meaning to the Android build.
Locale directory names use Android's qualifier form, not BCP-47:
| Language | Directory |
|---|---|
| Simplified Chinese | values-zh-rCN |
| German | values-de |
| Brazilian Portuguese | values-pt-rBR |
| Spanish | values-es |
Priority order, if you are not translating everything:
- Call screening and blocking. The dashboard, the blocked log, the after-call notification. This is what users see when the app does its job.
- Onboarding and permissions. The recovery paths. A user who cannot read why call screening is off cannot fix it.
- Blocklist and rules. Day-to-day management.
- Settings, statistics, changelog. These can come later.
Add your language to app/src/main/res/xml/locales_config.xml:
<locale android:name="zh-CN" />Note the BCP-47 form here (zh-CN), which differs from the resource directory
(values-zh-rCN). The manifest sets android:localeConfig, so this file is what
populates the per-app language picker in Android's system settings. Skip it and
your translation only appears for users whose entire device is set to that
language. It is the single easiest thing to forget.
Then add it to the app's own picker in ui/AppLanguage.kt, with its name in its
own language as a translatable="false" string. ShippedLocalesTest fails if
the two lists disagree.
python scripts/check_translations.py # every locale in the repo
python scripts/check_translations.py --locale zh-rCN # just yours
python scripts/check_translations.py --dir path/to/values-xx # before it's in the repoIt reports coverage and fails on the things that break at runtime rather than at build time:
- Format specifiers that don't match the English.
%1$s,%dand friends must survive translation with the same count and types. Get this wrong andString.formatthrows the moment the string is displayed. Which in this app can be the call-screening notification, on a device in a language the maintainer cannot read. Reordering is fine and expected. That's what the positional%1$sform is for. - Missing plural quantities. Chinese needs only
other, while Russian and Polish needone/few/many/other. The checker knows which. - Stale keys that no longer exist upstream (a warning, not an error).
- Locales missing from
locales_config.xml.
Missing strings are reported as coverage, never as a failure, with one
exception. Each shipped locale has a recorded coverage floor in
scripts/translation_floors.json, and the checker fails if coverage drops
below it. A partial translation is fine and expected. A translation quietly
rotting as English strings are added faster than they are translated is not.
If you improve a locale, raise its floor in the same change:
python scripts/check_translations.py --update-floors--update-floors only ever raises a floor, and it still fails when a locale
is below the floor it had. Lowering a floor means editing
scripts/translation_floors.json by hand, on purpose, with the reason in the
commit message. A floors file that can't be read fails the check rather than
switching it off. A new locale starts with no floor and only warns until
someone records one.
- Phone numbers are wrapped in Unicode bidi isolates before display, so you do
not need to do anything special for RTL languages. But do keep
%1$splaceholders intact rather than substituting a literal example number. - "Trusted" is used consistently for allowlisted callers. Keep one term for it in your language rather than alternating synonyms.
- Carrier authentication (STIR/SHAKEN) copy deliberately avoids implying the caller is safe. It says the caller ID is authenticated. Please preserve that distinction, it is a correctness matter and not a stylistic one.
- Keep
CallShielduntranslated as the app name.
Open a PR with just the resource files and the locales_config.xml line. Please
don't include build outputs, APKs, or version bumps. Those come from the release
process and only make the diff hard to review. Your commits keep your authorship.