Localise a string or add a language
Primico ships in English and German, and no user-visible string lives in Kotlin. This guide covers the four jobs that come up: adding a string, using it correctly, adding a quick-add keyword, and adding a language.
Prerequisites
Section titled “Prerequisites”- A working build (see the quickstart).
- For quick-add changes: a phone or emulator for the one instrumented test.
Add or change a string
Section titled “Add or change a string”Every string the screens show lives in :ui’s Compose Multiplatform resources:
- English:
ui/src/commonMain/composeResources/values/strings.xml - German:
ui/src/commonMain/composeResources/values-de/strings.xml
-
Add the entry to both files under the same name. The English file is the fallback, so a key missing from German silently shows English, and nothing fails. The PR checklist asks for both.
values/strings.xml <string name="snackbar_task_title_empty">A task needs a title</string><!-- values-de/strings.xml --><string name="snackbar_task_title_empty">Eine Aufgabe braucht einen Titel</string>Arguments are positional (
%1$s,%1$d), as in Android resources. German quotation marks are the typographic„…“, as in the existing entries. -
Put counts in
<plurals>, even where English and German happen to agree:<plurals name="task_count"><item quantity="one">%1$d task</item><item quantity="other">%1$d tasks</item></plurals> -
Build once. The
Resaccessors are generated (generateResClass = Alwaysinui/build.gradle.kts), one top-level property per string, in the packagede.andi1984.cadence.ui.resources. Screens import them with a star import rather than one line per key:import de.andi1984.cadence.ui.resources.*import org.jetbrains.compose.resources.pluralStringResourceimport org.jetbrains.compose.resources.stringResourceText(stringResource(Res.string.tags_title))Text(pluralStringResource(Res.plurals.projects_overdue_count, overdue, overdue))These are
org.jetbrains.compose.resourcesfunctions, notandroidx.compose.ui.res. The same call has to work on the desktop. -
Outside a composable, pass the resource, not the text. The ViewModel can’t resolve strings, so it hands over a
StringResource, for exampleshowSnackbar(Res.string.snackbar_error, listOf(result.message)). The snackbar composable resolves it.SnackbarMessage.Counteddoes the same for plurals.
Format dates, times and labels correctly
Section titled “Format dates, times and labels correctly”- Use
currentLocale()fromui/format/DateLabels.kt, neverLocale.getDefault(). The app’s language can differ from the system’s (Android 13+ has a per-app picker), andcurrentLocale()readsLocalAppLocale, which the shell provides throughCadenceTheme. - Formatters live in
ui/format/and are@Composable, because both the wording and the date patterns (date_pattern_*) come from resources. - The domain layer produces no prose.
RecurrenceEngine.summarize()returns a structuredRecurrenceSummarythatui/format/RecurrenceLabels.ktturns into words.Prioritycarries onlyshortLabel(P2); its spoken name lives inPriorityLabels.kt.
Add a quick-add keyword
Section titled “Add a quick-add keyword”Quick add’s vocabulary is data, not grammar. It lives in
QuickAddLexicon,
and the parser builds its patterns from whichever lexicon it’s handed.
- Find the right field on
englishWordsorgermanWords:tomorrow,every,units,afterCompletion,clockand so on. Each field is documented in the class. - Add the word in lower case, with every inflected form spelled out (
tag,tage,tagen), plus an umlaut-free spelling if people type one (taeglichnext totäglich). - Don’t list weekday or month names. They come from
java.timefor the locale. - Add a case to
QuickAddParserTest, parsing withQuickAddLexicon.GermanorQuickAddLexicon.Englishas the existing cases do. - Run the instrumented test on a device (see below).
Lexicons compose, and forLocale always folds English in, so every 2 weeks keeps working in a
German install.
Add a whole language
Section titled “Add a whole language”Say you’re adding French (fr).
-
Screen strings: copy
values/strings.xmltoui/src/commonMain/composeResources/values-fr/strings.xmland translate every entry,<plurals>included. French has different plural rules, so give each plural the quantities French needs. -
Android-only strings: add
app-android/src/main/res/values-fr/strings.xmlfor the launcher, notification and widget strings. -
Per-app language picker: add the locale to
app-android/src/main/res/xml/locales_config.xml:<locale android:name="fr" /> -
Quick-add lexicon: in
QuickAddLexicon’s companion, add afrenchWordslexicon (the structure words only:demain,chaque,jours, …) and a publicval French: QuickAddLexicon = English + frenchWords + namesOf(Locale.FRENCH). Then extendforLocale’swhen (locale.language)with aLocale.FRENCH.language -> Frenchbranch. Without a lexicon, an unlisted language still reads its own weekday and month names (fromjava.time) plus every English keyword. Adding a language means adding a lexicon, never touchingQuickAddParserorQuickAddPatterns. -
Tests: parser cases for the new words in
QuickAddParserTest, and make sure the locale is ineveryShippedLexiconCompilesUnderIcuinQuickAddPatternsDeviceTest. French is already in that list, and any other language needs adding.
The regex trap: ICU on the device, java.util.regex in the tests
Section titled “The regex trap: ICU on the device, java.util.regex in the tests”Android compiles regexes with ICU, and the unit tests use java.util.regex. They disagree
in two ways that pass every JVM test and then crash or misbehave on a phone:
- ICU rejects the
(?u)and(?U)inline flags outright, so the quick-add sheet throws on the first keystroke.QuickAddPatternstherefore spells word boundaries as\p{L}lookarounds (START/END), not\b. IGNORE_CASEfolds only ASCII on the JVM, soÜbermorgenwould parse on the phone but not in a test. Every non-ASCII letter in a keyword is written as a two-case class. Theliteral()helper does that for every lexicon word, so words you add to a lexicon are safe automatically. The rule bites when you hand-write a pattern.
Don’t put \b or an inline flag back into QuickAddPatterns. After any change to it or to a
lexicon, run the one test that uses the device’s engine:
./gradlew connectedDebugAndroidTestVerify
Section titled “Verify”- The CI test command passes (see run the tests).
- Switch the phone’s app language (Android 13+: system settings → Apps → Primico → Language) and check the new strings, plurals and dates on screen.
- Type a line using the new keywords into quick add on the device and check the chips.
Related
Section titled “Related”- Quick-add reference: every token the parser understands.
- Tasks, projects and tags: why
@tagcreates and#projectonly selects. - CONTRIBUTING.md: the PR checklist item for German strings.