📝 Managing Strings & Languages
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.
📖 Approach
- 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 via the Translation - Update workflow.
- Languages are added/removed when they reach 70% translation or fall below 60%.
🔄 Changing Source Strings
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.
Android Resource Files
Stored in res/values/strings.xml (and plurals.xml if applicable).
Compose Multiplatform Resources
Stored in src/commonMain/composeResources/values/strings.xml.
🏗️ Gradle Setup
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" } }
🔧 Mechanical/Global Changes
If a mechanical or global change to translations is required (for example, renaming placeholders or fixing formatting across all languages), follow this workflow:
- Lock components in Weblate: Go to the maintenance page and lock all components to prevent new translations during the change.
- Commit outstanding changes: Ensure all pending translations in Weblate are committed to its internal Git repository.
- Pull latest translations:
Trigger the Translation - Update GitHub workflow manually using
workflow_dispatch. - Merge the pull request:
Review and merge the resulting PR to ensure your local
mainbranch is in sync with Weblate. - Apply your change: Apply your mechanical changes to the source and translation files in a new branch and merge it.
- Update Weblate configuration (if needed):
If your change involved moving files or changing directory structures, use the
weblate-cli updatecommand to ensure Weblate is correctly configured. - Unlock components: Once Weblate has pulled the changes from the repository, unlock the components.
See the Weblate CLI section for more details on the tooling.
➕ Adding a String
- 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.
✏️ Changing a String
There are two kinds of changes to source strings:
🔤 Typos or Grammar Fixes
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”.
🧭 Changing Meaning
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)
❌ Removing a String
- 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.
🛠️ Using Strings in Code
Android Resources
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)
Compose Multiplatform Resources
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
Resclass is generated by the Compose Multiplatform Gradle plugin. You may need to build the project to generate it after adding new strings.
🔀 Merging Translations
Translations are merged from Weblate via an automated GitHub workflow. This workflow:
- Fetches the latest changes from Weblate’s Git export.
- Creates a pull request with the updated translation files.
- Preserves contributor attribution via
Co-authored-bytrailers.
When reviewing and merging these 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
🌍 Managing Languages
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.
🛠️ Automation Tools
We provide several CLI tools to assist with translation management.
🔎 Translation CLI
Used to check translation coverage before adding or removing languages.
./scripts/translation --token <weblate-token>
# Specify the low 60% threshold and verbose logging
./scripts/translation --token <weblate-token> --threshold 60 --log-level BODY
- Requires a Weblate API token
- Default threshold is 70% (can be changed with
--threshold <N>) - Default log level is
NONE(can be changed with--log-level <LEVEL>)
For example code integration, run with --print-all:
./scripts/translation --token <weblate-token> --print-all
This 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
🔧 Weblate CLI
Used to manage component configurations and create missing components on Weblate.
# Update managed components with standard configuration
./scripts/weblate --token <weblate-token> update
# Create missing components based on local modules
./scripts/weblate --token <weblate-token> create
# Delete a component by slug
./scripts/weblate --token <weblate-token> delete --slug <slug>
➕ Adding a Component
- Ensure your module follows the standard directory structure for strings.
- Run the
createcommand to identify and create missing components. - The tool will scan for modules containing
strings.xmland prompt you to create components for those missing from Weblate. - After creation, add the new component slug to
cli/weblate-cli/managed-components.txtto keep it updated with future configuration changes.
For more details, see the Weblate CLI README.
➖ Removing a Language
- 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
➕ Adding a Language
- 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.
🧩 Adding a Component to Weblate
When a new module contains translatable strings, a new Weblate component must be created. We provide a Weblate CLI to automate this process. See the Weblate CLI section for more details.
⚠️ Language Code Differences
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