From c7226d3293471aeab3b94416288d840ef2d8dae9 Mon Sep 17 00:00:00 2001 From: davotoula Date: Wed, 20 May 2026 23:25:14 +0200 Subject: [PATCH 1/2] i18n: convert calendar count + reminder strings to MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The NIP-52 calendar feature shipped two count-bearing strings as resources ("%1$d events" and "Starts in %1$d minutes"), which forces the wrong noun form in Slavic languages where the declension depends on the threshold integer. Convert both to in the default locale and in zh-rCN/pl-rPL/hu-rHU (the locales that already have translations), and update the 4 callsites — 3 Composables use pluralStringResource and the CalendarReminderWorker uses the pluralStringRes helper. pl-rPL and hu-rHU keep only the "other" quantity, preserving existing text without regression; proper CLDR fan-out (one/few/many for Polish, one for Hungarian) will come from Crowdin or a native translator. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../service/calendar/CalendarReminderWorker.kt | 6 ++++-- .../amethyst/ui/note/types/CalendarCollectionRender.kt | 5 +++-- .../loggedIn/calendars/CalendarCollectionsView.kt | 3 ++- .../calendars/detail/CalendarEventDetailScreen.kt | 3 ++- amethyst/src/main/res/values-hu-rHU/strings.xml | 8 ++++++-- amethyst/src/main/res/values-pl-rPL/strings.xml | 8 ++++++-- amethyst/src/main/res/values-zh-rCN/strings.xml | 8 ++++++-- amethyst/src/main/res/values/strings.xml | 10 ++++++++-- 8 files changed, 37 insertions(+), 14 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/calendar/CalendarReminderWorker.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/calendar/CalendarReminderWorker.kt index 4f1f30f7e1..0cb0b6f217 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/calendar/CalendarReminderWorker.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/calendar/CalendarReminderWorker.kt @@ -29,6 +29,7 @@ import androidx.work.WorkerParameters import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.model.nip52Calendar.appointmentView import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.ui.pluralStringRes import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.quartz.nip52Calendar.appt.day.CalendarDateSlotEvent import com.vitorpamplona.quartz.nip52Calendar.appt.tags.RSVPStatusTag @@ -92,9 +93,10 @@ class CalendarReminderWorker( val title = view.title ?: stringRes(applicationContext, R.string.calendar_reminder_default_title) val minutesAway = ((start - now).coerceAtLeast(0L) / 60L).toInt() val body = - stringRes( + pluralStringRes( applicationContext, - R.string.calendar_reminder_body, + R.plurals.calendar_reminder_body, + minutesAway, minutesAway, ) val deepLink = diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/CalendarCollectionRender.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/CalendarCollectionRender.kt index a25cc86232..f88c033c74 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/CalendarCollectionRender.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/CalendarCollectionRender.kt @@ -31,6 +31,7 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp @@ -40,7 +41,6 @@ import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer import com.vitorpamplona.amethyst.ui.theme.replyModifier import com.vitorpamplona.quartz.nip52Calendar.calendar.CalendarEvent @@ -87,8 +87,9 @@ fun RenderCalendarCollectionEvent( } Spacer(modifier = StdVertSpacer) + val eventCount = event.calendarEventAddresses().size Text( - text = stringRes(R.string.calendar_collection_count, event.calendarEventAddresses().size), + text = pluralStringResource(R.plurals.calendar_collection_count, eventCount, eventCount), style = MaterialTheme.typography.labelMedium, color = MaterialTheme.colorScheme.primary, modifier = Modifier.padding(start = 10.dp, end = 10.dp, bottom = 12.dp), diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/CalendarCollectionsView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/CalendarCollectionsView.kt index 4da24d43e8..359d2b60e4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/CalendarCollectionsView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/CalendarCollectionsView.kt @@ -43,6 +43,7 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp @@ -190,7 +191,7 @@ fun CalendarCollectionCard( ) } Text( - text = stringRes(R.string.calendar_collection_count, count), + text = pluralStringResource(R.plurals.calendar_collection_count, count, count), style = MaterialTheme.typography.labelMedium, color = MaterialTheme.colorScheme.primary, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/detail/CalendarEventDetailScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/detail/CalendarEventDetailScreen.kt index b304da4efd..e7de2dd865 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/detail/CalendarEventDetailScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/detail/CalendarEventDetailScreen.kt @@ -55,6 +55,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp @@ -460,7 +461,7 @@ private fun CollectionMembersSection( nav: INav, ) { Column(modifier = Modifier.padding(horizontal = 16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) { - SectionTitle(stringRes(R.string.calendar_collection_count, memberAddresses.size)) + SectionTitle(pluralStringResource(R.plurals.calendar_collection_count, memberAddresses.size, memberAddresses.size)) if (memberAddresses.isEmpty()) { Text( text = stringRes(R.string.calendar_collection_empty_members), diff --git a/amethyst/src/main/res/values-hu-rHU/strings.xml b/amethyst/src/main/res/values-hu-rHU/strings.xml index 301b09836c..50f121cc3f 100644 --- a/amethyst/src/main/res/values-hu-rHU/strings.xml +++ b/amethyst/src/main/res/values-hu-rHU/strings.xml @@ -1729,7 +1729,9 @@ Cím Leírás A cím megadása kötelező. - %1$d esemény + + %1$d esemény + Ebben a naptárban még nincsenek események. Ott leszek Talán @@ -1748,7 +1750,9 @@ Naptár-emlékeztetők Értesítés, ha egy esemény, amin Ön részt vesz, hamarosan elkezdődik. Naptáresemény - %1$d perc múlva kezdődik + + %1$d perc múlva kezdődik + Vegye fel az egyik naptárba Hozzáadás egy naptárhoz Ön még nem hozott létre egyetlen naptárat sem. diff --git a/amethyst/src/main/res/values-pl-rPL/strings.xml b/amethyst/src/main/res/values-pl-rPL/strings.xml index 99ad57294b..5e504ddfdd 100644 --- a/amethyst/src/main/res/values-pl-rPL/strings.xml +++ b/amethyst/src/main/res/values-pl-rPL/strings.xml @@ -1741,7 +1741,9 @@ Zaplanowane posty z innych kont nie zostaną opublikowane, dopóki to konto jest Tytuł Opis Tytuł jest wymagany. - Wydarzeń: %1$d + + Wydarzeń: %1$d + Brak wydarzeń w tym kalendarzu. Będę Może @@ -1760,7 +1762,9 @@ Zaplanowane posty z innych kont nie zostaną opublikowane, dopóki to konto jest Przypomnienia w kalendarzu Powiadomienie, gdy wydarzenie, w którym bierzesz udział, ma się właśnie rozpocząć. Wydarzenie w kalendarzu - Rozpoczęcie za %1$d min. + + Rozpoczęcie za %1$d min. + Dodaj do jednego z kalendarzy Dodaj do kalendarza Nie utworzyłeś jeszcze żadnych kalendarzy. diff --git a/amethyst/src/main/res/values-zh-rCN/strings.xml b/amethyst/src/main/res/values-zh-rCN/strings.xml index 363484afde..a900073a34 100644 --- a/amethyst/src/main/res/values-zh-rCN/strings.xml +++ b/amethyst/src/main/res/values-zh-rCN/strings.xml @@ -1721,7 +1721,9 @@ 标题 描述 标题是必需的。 - %1$d 个活动 + + %1$d 个活动 + 此日历中尚无活动。 参加 可能 @@ -1740,7 +1742,9 @@ 日历提醒 您正参加的活动即将开始时进行提醒。 日历活动 - 在 %1$d 分钟内开始 + + 在 %1$d 分钟内开始 + 添加到您日历中的一个 添加到日历 您尚未创建任何日历。 diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml index bcd67af071..bbd3bed188 100644 --- a/amethyst/src/main/res/values/strings.xml +++ b/amethyst/src/main/res/values/strings.xml @@ -1949,7 +1949,10 @@ Title Description A title is required. - %1$d events + + %1$d event + %1$d events + No events in this calendar yet. Going @@ -1969,7 +1972,10 @@ Calendar reminders Heads-up when an event you\'re attending is about to start. Calendar event - Starts in %1$d minutes + + Starts in %1$d minute + Starts in %1$d minutes + Add to one of your calendars Add to a calendar You haven\'t created any calendars yet. From 63e6283354111d11759bd877e931a24356441e67 Mon Sep 17 00:00:00 2001 From: davotoula Date: Wed, 20 May 2026 23:32:24 +0200 Subject: [PATCH 2/2] docs(claude): add res/CLAUDE.md note on for Slavic locales Captures the rule we just hit twice in a row (hashtag-limit + calendar count): any count-bearing string must be , every locale must include the CLDR categories it actually uses, and don't hardcode "1" in quantity="one" items. Loads automatically whenever a file under res/ is edited. Co-Authored-By: Claude Opus 4.7 (1M context) --- amethyst/src/main/res/CLAUDE.md | 52 +++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 amethyst/src/main/res/CLAUDE.md diff --git a/amethyst/src/main/res/CLAUDE.md b/amethyst/src/main/res/CLAUDE.md new file mode 100644 index 0000000000..f3da508b02 --- /dev/null +++ b/amethyst/src/main/res/CLAUDE.md @@ -0,0 +1,52 @@ +# String resources — plural handling + +Always consider Slavic / Baltic / Semitic / Celtic languages when a string contains a count. The CLDR plural categories `one` / `other` that English uses are **not enough** — these language families decline the noun on `few`, `many`, `two`, `zero`, etc. + +## Rules + +1. **Any string whose noun changes form with the count must be a `` resource, not a ``.** If the English reads naturally as "1 X" vs "N X" with a different noun form, it's a plural. +2. **The same applies to thresholds** ("more than %1$d hashtags"). The count IS the threshold, and the noun form depends on it in some languages. +3. **Never hardcode `"1"` in the English text** of a `quantity="one"` item — always use the `%1$d` placeholder. Hardcoding breaks every language whose `one` category covers numbers other than 1 (e.g. some Slavic languages). +4. **Each locale must include every CLDR category it uses**, not just the categories present in English. Quick reference: + - English / German / Swedish / Brazilian Portuguese / Hungarian: `one`, `other` + - Czech / Polish / Russian / Ukrainian / Croatian: `one`, `few`, `many`, `other` + - Arabic: `zero`, `one`, `two`, `few`, `many`, `other` + - Chinese / Japanese: `other` only + +## Anti-patterns to flag + +When adding or reviewing strings, flag these: + +- `%1$d items` where the noun is countable → should be ``. +- `1 reply` → hardcoded `1`, should be `%1$d reply`. +- A locale `strings.xml` providing only `one`/`other` for Polish / Czech / Russian → missing `few`/`many`, will silently fall through to `other` for counts 2–4, 22–24, etc. + +## Call-site patterns + +In a `@Composable`: + +```kotlin +import androidx.compose.ui.res.pluralStringResource +// ... +val n = items.size +Text(pluralStringResource(R.plurals.foo, n, n)) +``` + +Pass the count **twice**: once as the CLDR selector, once as the `%1$d` format arg. Hoist the count to a local `val` if computing it is non-trivial (lists, mapNotNull, etc.) — the API forces two reads and you don't want two allocations. + +Outside Compose (Workers, callbacks, services), use the project helper: + +```kotlin +import com.vitorpamplona.amethyst.ui.pluralStringRes +// ... +pluralStringRes(ctx, R.plurals.foo, count, count) +``` + +Defined in `amethyst/src/main/java/com/vitorpamplona/amethyst/ui/StringResourceCache.kt` — wraps `ctx.resources.getQuantityString(id, count, *formatArgs)`. + +## Workflow for new count strings + +1. Add the new `` to default `values/strings.xml` with `one` + `other`. +2. If you're also adding a locale-specific translation (e.g. zh-rCN, pl-rPL), add it as `` with at least `other`. Crowdin will fan out to all CLDR categories for that locale. +3. If you're **converting** an existing `` to ``, you **must** convert it in every locale that already had the `` — otherwise aapt2 will fail with a resource-type mismatch. Use `` with `other` only to preserve existing translation (Crowdin fills the rest). +4. Reference: [Android `` docs](https://developer.android.com/guide/topics/resources/string-resource#Plurals) and [CLDR plural rules](https://unicode-org.github.io/cldr-staging/charts/latest/supplemental/language_plural_rules.html).