Merge pull request #3020 from davotoula/feat/calendar-plurals-i18n

i18n: convert calendar count + reminder strings to <plurals>
This commit is contained in:
Vitor Pamplona
2026-05-20 17:40:21 -04:00
committed by GitHub
9 changed files with 89 additions and 14 deletions
@@ -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 =
@@ -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),
@@ -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,
)
@@ -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),
+52
View File
@@ -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 `<plurals>` resource, not a `<string>`.** 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:
- `<string name="…">%1$d items</string>` where the noun is countable → should be `<plurals>`.
- `<item quantity="one">1 reply</item>` → 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 24, 2224, 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 `<plurals>` 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 `<plurals>` with at least `other`. Crowdin will fan out to all CLDR categories for that locale.
3. If you're **converting** an existing `<string>` to `<plurals>`, you **must** convert it in every locale that already had the `<string>` — otherwise aapt2 will fail with a resource-type mismatch. Use `<plurals>` with `other` only to preserve existing translation (Crowdin fills the rest).
4. Reference: [Android `<plurals>` 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).
@@ -1729,7 +1729,9 @@
<string name="calendar_collection_title">Cím</string>
<string name="calendar_collection_description">Leírás</string>
<string name="calendar_collection_invalid">A cím megadása kötelező.</string>
<string name="calendar_collection_count">%1$d esemény</string>
<plurals name="calendar_collection_count">
<item quantity="other">%1$d esemény</item>
</plurals>
<string name="calendar_collection_empty_members">Ebben a naptárban még nincsenek események.</string>
<string name="calendar_rsvp_going">Ott leszek</string>
<string name="calendar_rsvp_maybe">Talán</string>
@@ -1748,7 +1750,9 @@
<string name="calendar_reminder_channel_name">Naptár-emlékeztetők</string>
<string name="calendar_reminder_channel_description">Értesítés, ha egy esemény, amin Ön részt vesz, hamarosan elkezdődik.</string>
<string name="calendar_reminder_default_title">Naptáresemény</string>
<string name="calendar_reminder_body">%1$d perc múlva kezdődik</string>
<plurals name="calendar_reminder_body">
<item quantity="other">%1$d perc múlva kezdődik</item>
</plurals>
<string name="calendar_add_to_calendar_action">Vegye fel az egyik naptárba</string>
<string name="calendar_add_to_calendar_title">Hozzáadás egy naptárhoz</string>
<string name="calendar_add_to_calendar_none">Ön még nem hozott létre egyetlen naptárat sem.</string>
@@ -1741,7 +1741,9 @@ Zaplanowane posty z innych kont nie zostaną opublikowane, dopóki to konto jest
<string name="calendar_collection_title">Tytuł</string>
<string name="calendar_collection_description">Opis</string>
<string name="calendar_collection_invalid">Tytuł jest wymagany.</string>
<string name="calendar_collection_count">Wydarzeń: %1$d</string>
<plurals name="calendar_collection_count">
<item quantity="other">Wydarzeń: %1$d</item>
</plurals>
<string name="calendar_collection_empty_members">Brak wydarzeń w tym kalendarzu.</string>
<string name="calendar_rsvp_going">Będę</string>
<string name="calendar_rsvp_maybe">Może</string>
@@ -1760,7 +1762,9 @@ Zaplanowane posty z innych kont nie zostaną opublikowane, dopóki to konto jest
<string name="calendar_reminder_channel_name">Przypomnienia w kalendarzu</string>
<string name="calendar_reminder_channel_description">Powiadomienie, gdy wydarzenie, w którym bierzesz udział, ma się właśnie rozpocząć.</string>
<string name="calendar_reminder_default_title">Wydarzenie w kalendarzu</string>
<string name="calendar_reminder_body">Rozpoczęcie za %1$d min.</string>
<plurals name="calendar_reminder_body">
<item quantity="other">Rozpoczęcie za %1$d min.</item>
</plurals>
<string name="calendar_add_to_calendar_action">Dodaj do jednego z kalendarzy</string>
<string name="calendar_add_to_calendar_title">Dodaj do kalendarza</string>
<string name="calendar_add_to_calendar_none">Nie utworzyłeś jeszcze żadnych kalendarzy.</string>
@@ -1721,7 +1721,9 @@
<string name="calendar_collection_title">标题</string>
<string name="calendar_collection_description">描述</string>
<string name="calendar_collection_invalid">标题是必需的。</string>
<string name="calendar_collection_count">%1$d 个活动</string>
<plurals name="calendar_collection_count">
<item quantity="other">%1$d 个活动</item>
</plurals>
<string name="calendar_collection_empty_members">此日历中尚无活动。</string>
<string name="calendar_rsvp_going">参加</string>
<string name="calendar_rsvp_maybe">可能</string>
@@ -1740,7 +1742,9 @@
<string name="calendar_reminder_channel_name">日历提醒</string>
<string name="calendar_reminder_channel_description">您正参加的活动即将开始时进行提醒。</string>
<string name="calendar_reminder_default_title">日历活动</string>
<string name="calendar_reminder_body">在 %1$d 分钟内开始</string>
<plurals name="calendar_reminder_body">
<item quantity="other">在 %1$d 分钟内开始</item>
</plurals>
<string name="calendar_add_to_calendar_action">添加到您日历中的一个</string>
<string name="calendar_add_to_calendar_title">添加到日历</string>
<string name="calendar_add_to_calendar_none">您尚未创建任何日历。</string>
+8 -2
View File
@@ -1950,7 +1950,10 @@
<string name="calendar_collection_title">Title</string>
<string name="calendar_collection_description">Description</string>
<string name="calendar_collection_invalid">A title is required.</string>
<string name="calendar_collection_count">%1$d events</string>
<plurals name="calendar_collection_count">
<item quantity="one">%1$d event</item>
<item quantity="other">%1$d events</item>
</plurals>
<string name="calendar_collection_empty_members">No events in this calendar yet.</string>
<string name="calendar_rsvp_going">Going</string>
@@ -1970,7 +1973,10 @@
<string name="calendar_reminder_channel_name">Calendar reminders</string>
<string name="calendar_reminder_channel_description">Heads-up when an event you\'re attending is about to start.</string>
<string name="calendar_reminder_default_title">Calendar event</string>
<string name="calendar_reminder_body">Starts in %1$d minutes</string>
<plurals name="calendar_reminder_body">
<item quantity="one">Starts in %1$d minute</item>
<item quantity="other">Starts in %1$d minutes</item>
</plurals>
<string name="calendar_add_to_calendar_action">Add to one of your calendars</string>
<string name="calendar_add_to_calendar_title">Add to a calendar</string>
<string name="calendar_add_to_calendar_none">You haven\'t created any calendars yet.</string>