This document explains how developers manage our English source strings and add/remove languages in the Thunderbird for Android project.
Note
Translators: If you want to contribute translations, see Translations. This document is developer-focused.
- We use Android’s resource system for localizing strings in Android-only modules.
- We use Compose Multiplatform Resources for localizing strings in Kotlin Multiplatform (KMP) modules.
- Source language is English (American English, represented as
en). - Source strings are modified only in this repository (via pull requests).
- Translations are managed exclusively in Weblate and merged into the repository by the Thunderbird team.
- Languages are added/removed when they reach 70% translation or fall below 60%.
Source strings are always stored in English (en). They must be managed carefully to avoid breaking
existing translations.
- Do not edit translation files directly in Git.
- Translations should always be updated in Weblate.
Stored in res/values/strings.xml (and plurals.xml if applicable).
Stored in src/commonMain/composeResources/values/strings.xml.
To use Compose Multiplatform Resources in a module, follow these steps in your build.gradle.kts:
-
Apply the plugin: Use
ThunderbirdPlugins.Library.kmpComposefor a KMP library module with Compose support.plugins { id(ThunderbirdPlugins.Library.kmpCompose) } -
Configure
compose.resourcesblock: SetpublicResClasstofalseand provide a uniquepackageOfResClass.compose.resources { publicResClass = false packageOfResClass = "net.thunderbird.feature.yourfeature.resources" } -
Ensure
androidnamespace is set: The Android target requires a namespace in thekotlinblock.kotlin { android { namespace = "net.thunderbird.feature.yourfeature.api" } }
If a mechanical or global change to translations is required (for example, renaming placeholders or fixing formatting across all languages):
- Lock components in Weblate (maintenance page).
- Commit all outstanding changes.
- Push Weblate changes (creates a PR).
- Merge the Weblate PR.
- Apply your mechanical change in a separate PR.
- Wait for Weblate sync to propagate your merged PR.
- Unlock components in Weblate.
This ensures translators do not work on outdated strings and avoids merge conflicts.
- Add the new string in the appropriate source file:
- Android:
res/values/strings.xml - Compose:
src/commonMain/composeResources/values/strings.xml
- Android:
- Do not add translations.
- After merge, Weblate will pull the new string.
- Translators can then add translations in Weblate.
There are two kinds of changes to source strings:
Correcting minor errors (spelling, capitalization, punctuation, grammar) in the English source is allowed:
- Keep the same key — translations will remain valid.
Example:
- Changing "Recieve" to "Receive" or "email" to "Email".
Caution
Never reuse an existing key for a changed meaning — this would cause translators’ work to become misleading or incorrect.
If the meaning of the string changes (new wording, different context, updated functionality):
- Add a new key with the new string.
- Update all references in the source code to use the new key.
- Delete the old key from the source file.
- Delete the old key’s translations from all translation files (e.g.
values-*/strings.xml). - Build the project to ensure there are no references to the old key remaining.
This ensures there are no stale or misleading translations left behind.
Example:
- Old: "Check mail now" (
action_check_mail) - New: "Sync mail" (
action_sync_mail)
- Delete the key from the source file.
- Delete the key’s translations from all translation files.
- Build the project to ensure there are no references to the removed key remaining.
Used in Android-only modules or Android-specific source sets.
// In a Context-aware class
val title = context.getString(R.string.my_string_key)
// With arguments
val message = context.getString(R.string.welcome_message, userName)Used in KMP modules, primarily in commonMain.
import org.jetbrains.compose.resources.getString
import org.jetbrains.compose.resources.getPluralString
import your.module.package.resources.Res
import your.module.package.resources.my_string_key
// In a Composable function (using the @Composable stringResource)
val title = stringResource(Res.string.my_string_key)
// In a non-Composable (suspend) function
val title = getString(Res.string.my_string_key)
// With arguments
val message = getString(Res.string.welcome_message, userName)
// Plurals
val messagesCount = getPluralString(Res.plurals.new_messages, count, count)Note
The Res class is generated by the Compose Multiplatform Gradle plugin. You may need to build the project to
generate it after adding new strings.
When merging Weblate-generated PRs:
- Check plural forms for cs, lt, sk locales. Weblate does not handle these correctly (issue).
- Ensure both
manyandotherforms are present.- If unsure, reusing values from
manyorotheris acceptable.
- If unsure, reusing values from
We use Gradle’s androidResources.localeFilters to control which languages are bundled.
This must stay in sync with the string array supported_languages so the in-app picker shows only available locales.
Before adding a language, we require that it is at least 70% translated in Weblate.
We provide a Translation CLI script to check translation coverage:
./scripts/translation --token <weblate-token>
# Specify the low 60% threshold
./scripts/translation --token <weblate-token> --threshold 60- Requires a Weblate API token
- Default threshold is 70% (can be changed with
--threshold <N>)
For example code integration, run with --print-all:
./scripts/translation --token <weblate-token> --print-allThis output can be used to update:
resourceConfigurationsinapp-k9mail/build.gradle.ktsandapp-thunderbird/build.gradle.ktssupported_languagesinlegacy/core/src/res/values/arrays_general_settings_values.xml
- Remove language code from
androidResources.localeFiltersin:app-thunderbird/build.gradle.ktsapp-k9mail/build.gradle.kts
- Remove entry from
supported_languagesin:app/core/src/main/res/values/arrays_general_settings_values.xml
- Add the code to
androidResources.localeFiltersin both app modules. - Add entry to
supported_languagesin:app/core/src/main/res/values/arrays_general_settings_values.xml
- Add corresponding display name in:
app/ui/legacy/src/main/res/values/arrays_general_settings_strings.xml(sorted by Unicode default collation order).
- Ensure indexes match between
language_entriesandlanguage_values.
Important
The order of entries in language_entries and language_values must match exactly. Incorrect ordering will cause mismatches in the language picker.
When a new module contains translatable strings, a new Weblate component must be created.
Steps:
- Go to Add Component.
- Choose From existing component.
- Name your component (e.g.,
feature:notification:api). - For Component, select Thunderbird for Android / K-9 Mail/ui-legacy.
- Continue → Select Specify configuration manually.
- Set file format to Android String Resource.
- File mask:
- Android:
path/to/module/src/main/res/values-*/strings.xml - Compose:
path/to/module/src/commonMain/composeResources/values-*/strings.xml
- Android:
- Base file:
- Android:
path/to/module/src/main/res/values/strings.xml - Compose:
path/to/module/src/commonMain/composeResources/values/strings.xml
- Android:
- Uncheck Edit base file.
- License: Apache License 2.0.
- Save.
Android sometimes uses codes that differ from Weblate (e.g. Hebrew = iw in Android but he in Weblate).
Automation tools must map between systems. See LanguageCodeLoader.kt for an example.
You could find a more complete list of differences in the Android documentation and Unicode and internationalization support