Merge pull request #3020 from davotoula/feat/calendar-plurals-i18n
i18n: convert calendar count + reminder strings to <plurals>
This commit is contained in:
+4
-2
@@ -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 =
|
||||
|
||||
+3
-2
@@ -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),
|
||||
|
||||
+2
-1
@@ -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,
|
||||
)
|
||||
|
||||
+2
-1
@@ -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),
|
||||
|
||||
@@ -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 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 `<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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user