mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-13 14:32:03 +00:00
Four locales received English on surfaces whose translations were already complete, because the resolving code was a de/en binary. - Medication reminder push and the Telegram inline-keyboard labels admitted only de/en, so es/fr/it/pl got the English template on every channel. Route the resolver through a shared coerceLocale(). - The Coach's trailing "reply now in X" clause said English for any non-German locale, overriding the correct system-prompt directive. Read the language name from a shared map instead. - Coach refusal copy was a de/en constant pair, answering four locales in English at the one turn the Coach is declining to help. Move the copy into the bundles; the EN/DE constants now derive from them. - The sleep-stage chart formatted axis ticks as en-US for four locales; the cycle calendar and the device-score tile followed the *browser* locale rather than the app locale. - Three components carried a byte-identical duration formatter that branched on German. Lift it to one key-driven helper. Polish needs a third plural tier: 2-4 takes a "few" form a two-form scheme cannot express, so "2 godzin temu" read as "2 of-hours ago". Add a pluralKey() helper driven by Intl.PluralRules and a Few tier to the relative-time and stale-hint keys. The five locales whose rules have no "few" category resolve exactly as before by construction, and their Few values mirror Other. Teach the reverse-coverage guard to expand a pluralKey() base into its three tiers.
68 lines
2.6 KiB
TypeScript
68 lines
2.6 KiB
TypeScript
/**
|
||
* Plural-tier selection for the count-bearing message keys.
|
||
*
|
||
* The bundle originally carried a two-form scheme per countable string —
|
||
* `<base>One` for 1 and `<base>Other` for everything else. That expresses
|
||
* English, German, Spanish, French and Italian correctly, but not Polish:
|
||
* Polish splits the non-singular range into a "few" form (2–4, 22–24, 32–34,
|
||
* …) and a "many" form (0, 5–21, …), so a two-form scheme rendered
|
||
* "2 godzin temu" where the language wants "2 godziny temu".
|
||
*
|
||
* Rather than hand-roll the modulo arithmetic, the tier comes from
|
||
* `Intl.PluralRules`, which carries the CLDR rules for every locale we ship.
|
||
* The three tiers map as:
|
||
*
|
||
* CLDR "one" → `One`
|
||
* CLDR "few" → `Few`
|
||
* CLDR "many" / "other" → `Other`
|
||
*
|
||
* For de / en / es / fr / it the "few" category never fires on an integer, so
|
||
* those locales resolve exactly as they did before this module existed — the
|
||
* change is inert for them by construction, not by a special case. For Polish
|
||
* the surviving `Other` tier carries the "many" form, which is what the key
|
||
* already held, so only the new `Few` tier needed fresh strings.
|
||
*
|
||
* `Other` is the floor: an unknown category, an unsupported locale, or an
|
||
* `Intl.PluralRules` that throws all resolve to it, which is the pre-existing
|
||
* behaviour. A caller can therefore add the `Few` key for Polish alone and
|
||
* every other bundle keeps working.
|
||
*/
|
||
import type { Locale } from "./config";
|
||
import { resolveIntlLocale } from "@/lib/format-locale";
|
||
|
||
export type PluralTier = "One" | "Few" | "Other";
|
||
|
||
/**
|
||
* Select the plural tier for `count` under `locale`'s CLDR rules.
|
||
*
|
||
* Counts are compared as integers — every call site floors its value before
|
||
* reaching here (minutes, hours, days, weeks, months), so the CLDR fractional
|
||
* categories never come into play.
|
||
*/
|
||
export function pluralTier(count: number, locale: Locale): PluralTier {
|
||
let category: Intl.LDMLPluralRule;
|
||
try {
|
||
category = new Intl.PluralRules(resolveIntlLocale(locale)).select(count);
|
||
} catch {
|
||
// A runtime without the locale data still gets the two-form behaviour
|
||
// rather than a thrown render.
|
||
return count === 1 ? "One" : "Other";
|
||
}
|
||
if (category === "one") return "One";
|
||
if (category === "few") return "Few";
|
||
return "Other";
|
||
}
|
||
|
||
/**
|
||
* Build the message key for `count` from a base key.
|
||
*
|
||
* `pluralKey("insights.relativeHoursAgo", 3, "pl")` → `insights.relativeHoursAgoFew`.
|
||
*/
|
||
export function pluralKey(
|
||
baseKey: string,
|
||
count: number,
|
||
locale: Locale,
|
||
): string {
|
||
return `${baseKey}${pluralTier(count, locale)}`;
|
||
}
|