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/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). 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 a4f33e0697..5f2c6fe9c3 100644 --- a/amethyst/src/main/res/values/strings.xml +++ b/amethyst/src/main/res/values/strings.xml @@ -1950,7 +1950,10 @@ Title Description A title is required. - %1$d events + + %1$d event + %1$d events + No events in this calendar yet. Going @@ -1970,7 +1973,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.