feat(app): support locale plural rules (#40370)
Co-authored-by: Luke Parker <10430890+Hona@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
c7aadc83e1
commit
b1553c2b5c
@@ -0,0 +1,24 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { pluralCategory } from "./i18n"
|
||||
|
||||
describe("pluralCategory", () => {
|
||||
test.each([
|
||||
["en", 0, "other"],
|
||||
["en", 1, "one"],
|
||||
["fr", 0, "one"],
|
||||
["fr", 1_000_000, "many"],
|
||||
["ru", 1, "one"],
|
||||
["ru", 2, "few"],
|
||||
["ru", 5, "many"],
|
||||
["ru", 21, "one"],
|
||||
["ar", 0, "zero"],
|
||||
["ar", 1, "one"],
|
||||
["ar", 2, "two"],
|
||||
["ar", 3, "few"],
|
||||
["ar", 11, "many"],
|
||||
["ar", 100, "other"],
|
||||
["ja", 1, "other"],
|
||||
] as const)("selects %s for %d as %s", (locale, count, expected) => {
|
||||
expect(pluralCategory(locale, count)).toBe(expected)
|
||||
})
|
||||
})
|
||||
@@ -3,11 +3,37 @@ import { dict as en } from "../i18n/en"
|
||||
|
||||
export type UiI18nKey = keyof typeof en
|
||||
|
||||
export const UI_PLURAL_KEYS = [
|
||||
"ui.sessionTurn.diffs.changed",
|
||||
"ui.messagePart.context.read",
|
||||
"ui.messagePart.context.search",
|
||||
"ui.messagePart.context.list",
|
||||
] as const
|
||||
export type UiI18nPluralKey = (typeof UI_PLURAL_KEYS)[number]
|
||||
export type UiPluralCategory = "zero" | "one" | "two" | "few" | "many" | "other"
|
||||
export type UiI18nPluralLookupKey = `${UiI18nPluralKey}.${UiPluralCategory}`
|
||||
|
||||
export type UiI18nParams = Record<string, string | number | boolean>
|
||||
|
||||
export type UiI18n = {
|
||||
locale: Accessor<string>
|
||||
t: (key: UiI18nKey, params?: UiI18nParams) => string
|
||||
plural: (key: UiI18nPluralKey, count: number, params?: UiI18nParams) => string
|
||||
}
|
||||
|
||||
const rules = new Map<string, Intl.PluralRules>()
|
||||
|
||||
export function pluralCategory(locale: string, count: number): UiPluralCategory {
|
||||
const cached = rules.get(locale)
|
||||
if (cached) return cached.select(count)
|
||||
const next = new Intl.PluralRules(locale)
|
||||
if (rules.size >= 32) rules.delete(rules.keys().next().value!)
|
||||
rules.set(locale, next)
|
||||
return next.select(count)
|
||||
}
|
||||
|
||||
export function pluralKey(key: UiI18nPluralKey, category: UiPluralCategory) {
|
||||
return `${key}.${category}` as UiI18nPluralLookupKey
|
||||
}
|
||||
|
||||
function resolveTemplate(text: string, params?: UiI18nParams) {
|
||||
@@ -25,6 +51,8 @@ const fallback: UiI18n = {
|
||||
const value = en[key] ?? String(key)
|
||||
return resolveTemplate(value, params)
|
||||
},
|
||||
plural: (key, count, params) =>
|
||||
fallback.t(pluralKey(key, pluralCategory(fallback.locale(), count)), { ...params, count }),
|
||||
}
|
||||
|
||||
const Context = createContext<UiI18n>(fallback)
|
||||
|
||||
@@ -57,6 +57,10 @@ export const dict = {
|
||||
"ui.sessionTurn.summary.response": "استجابة",
|
||||
"ui.sessionTurn.diff.showMore": "إظهار المزيد من التغييرات ({{count}})",
|
||||
"ui.sessionTurn.diffs.changed.one": "ملف معدل: {{count}}",
|
||||
"ui.sessionTurn.diffs.changed.zero": "الملفات المعدلة: {{count}}",
|
||||
"ui.sessionTurn.diffs.changed.two": "عدد الملفات المعدلة: {{count}}",
|
||||
"ui.sessionTurn.diffs.changed.few": "الملفات المعدلة: {{count}}",
|
||||
"ui.sessionTurn.diffs.changed.many": "الملفات المعدلة: {{count}}",
|
||||
"ui.sessionTurn.diffs.changed.other": "الملفات المعدلة: {{count}}",
|
||||
"ui.sessionTurn.diffs.showAll": "إظهار الكل",
|
||||
"ui.sessionTurn.diffs.showLess": "إظهار عدد أقل",
|
||||
@@ -95,10 +99,22 @@ export const dict = {
|
||||
"ui.messagePart.questions.dismissed": "تم إهمال الأسئلة",
|
||||
"ui.messagePart.compaction": "تم اختصار الجلسة",
|
||||
"ui.messagePart.context.read.one": "{{count}} قراءة",
|
||||
"ui.messagePart.context.read.zero": "{{count}} قراءة",
|
||||
"ui.messagePart.context.read.two": "عدد القراءات: {{count}}",
|
||||
"ui.messagePart.context.read.few": "{{count}} قراءات",
|
||||
"ui.messagePart.context.read.many": "{{count}} قراءةً",
|
||||
"ui.messagePart.context.read.other": "{{count}} قراءات",
|
||||
"ui.messagePart.context.search.one": "{{count}} بحث",
|
||||
"ui.messagePart.context.search.zero": "{{count}} عملية بحث",
|
||||
"ui.messagePart.context.search.two": "عدد عمليات البحث: {{count}}",
|
||||
"ui.messagePart.context.search.few": "{{count}} عمليات بحث",
|
||||
"ui.messagePart.context.search.many": "{{count}} عملية بحث",
|
||||
"ui.messagePart.context.search.other": "{{count}} عمليات بحث",
|
||||
"ui.messagePart.context.list.one": "{{count}} عملية سرد",
|
||||
"ui.messagePart.context.list.zero": "{{count}} عملية سرد",
|
||||
"ui.messagePart.context.list.two": "عدد عمليات السرد: {{count}}",
|
||||
"ui.messagePart.context.list.few": "{{count}} عمليات سرد",
|
||||
"ui.messagePart.context.list.many": "{{count}} عملية سرد",
|
||||
"ui.messagePart.context.list.other": "{{count}} عمليات سرد",
|
||||
"ui.messagePart.diagnostic.error": "خطأ",
|
||||
"ui.messagePart.title.edit": "تحرير",
|
||||
|
||||
@@ -57,6 +57,7 @@ export const dict = {
|
||||
"ui.sessionTurn.summary.response": "Resposta",
|
||||
"ui.sessionTurn.diff.showMore": "Mostrar mais alterações ({{count}})",
|
||||
"ui.sessionTurn.diffs.changed.one": "Arquivo alterado: {{count}}",
|
||||
"ui.sessionTurn.diffs.changed.many": "Arquivos alterados: {{count}}",
|
||||
"ui.sessionTurn.diffs.changed.other": "Arquivos alterados: {{count}}",
|
||||
"ui.sessionTurn.diffs.showAll": "Mostrar tudo",
|
||||
"ui.sessionTurn.diffs.showLess": "Mostrar menos",
|
||||
@@ -95,10 +96,13 @@ export const dict = {
|
||||
"ui.messagePart.questions.dismissed": "Perguntas descartadas",
|
||||
"ui.messagePart.compaction": "Sessão compactada",
|
||||
"ui.messagePart.context.read.one": "{{count}} leitura",
|
||||
"ui.messagePart.context.read.many": "{{count}} de leituras",
|
||||
"ui.messagePart.context.read.other": "{{count}} leituras",
|
||||
"ui.messagePart.context.search.one": "{{count}} pesquisa",
|
||||
"ui.messagePart.context.search.many": "{{count}} de pesquisas",
|
||||
"ui.messagePart.context.search.other": "{{count}} pesquisas",
|
||||
"ui.messagePart.context.list.one": "{{count}} lista",
|
||||
"ui.messagePart.context.list.many": "{{count}} de listas",
|
||||
"ui.messagePart.context.list.other": "{{count}} listas",
|
||||
"ui.messagePart.diagnostic.error": "Erro",
|
||||
"ui.messagePart.title.edit": "Editar",
|
||||
|
||||
@@ -61,6 +61,7 @@ export const dict = {
|
||||
"ui.sessionTurn.summary.response": "Odgovor",
|
||||
"ui.sessionTurn.diff.showMore": "Prikaži još izmjena ({{count}})",
|
||||
"ui.sessionTurn.diffs.changed.one": "{{count}} izmijenjena datoteka",
|
||||
"ui.sessionTurn.diffs.changed.few": "{{count}} izmijenjene datoteke",
|
||||
"ui.sessionTurn.diffs.changed.other": "{{count}} izmijenjene datoteke",
|
||||
"ui.sessionTurn.diffs.showAll": "Prikaži sve",
|
||||
"ui.sessionTurn.diffs.showLess": "Prikaži manje",
|
||||
@@ -99,10 +100,13 @@ export const dict = {
|
||||
"ui.messagePart.questions.dismissed": "Pitanja odbačena",
|
||||
"ui.messagePart.compaction": "Sesija sažeta",
|
||||
"ui.messagePart.context.read.one": "{{count}} čitanje",
|
||||
"ui.messagePart.context.read.few": "{{count}} čitanja",
|
||||
"ui.messagePart.context.read.other": "{{count}} čitanja",
|
||||
"ui.messagePart.context.search.one": "{{count}} pretraga",
|
||||
"ui.messagePart.context.search.few": "{{count}} pretrage",
|
||||
"ui.messagePart.context.search.other": "{{count}} pretrage",
|
||||
"ui.messagePart.context.list.one": "{{count}} listanje",
|
||||
"ui.messagePart.context.list.few": "{{count}} listanja",
|
||||
"ui.messagePart.context.list.other": "{{count}} listanja",
|
||||
"ui.messagePart.diagnostic.error": "Greška",
|
||||
"ui.messagePart.title.edit": "Uredi",
|
||||
|
||||
@@ -57,6 +57,7 @@ export const dict = {
|
||||
"ui.sessionTurn.summary.response": "Respuesta",
|
||||
"ui.sessionTurn.diff.showMore": "Mostrar más cambios ({{count}})",
|
||||
"ui.sessionTurn.diffs.changed.one": "{{count}} archivo modificado",
|
||||
"ui.sessionTurn.diffs.changed.many": "{{count}} de archivos modificados",
|
||||
"ui.sessionTurn.diffs.changed.other": "{{count}} archivos modificados",
|
||||
"ui.sessionTurn.diffs.showAll": "Mostrar todos",
|
||||
"ui.sessionTurn.diffs.showLess": "Mostrar menos",
|
||||
@@ -95,10 +96,13 @@ export const dict = {
|
||||
"ui.messagePart.questions.dismissed": "Preguntas descartadas",
|
||||
"ui.messagePart.compaction": "Sesión compactada",
|
||||
"ui.messagePart.context.read.one": "{{count}} lectura",
|
||||
"ui.messagePart.context.read.many": "{{count}} de lecturas",
|
||||
"ui.messagePart.context.read.other": "{{count}} lecturas",
|
||||
"ui.messagePart.context.search.one": "{{count}} búsqueda",
|
||||
"ui.messagePart.context.search.many": "{{count}} de búsquedas",
|
||||
"ui.messagePart.context.search.other": "{{count}} búsquedas",
|
||||
"ui.messagePart.context.list.one": "{{count}} listado",
|
||||
"ui.messagePart.context.list.many": "{{count}} de listados",
|
||||
"ui.messagePart.context.list.other": "{{count}} listados",
|
||||
"ui.messagePart.diagnostic.error": "Error",
|
||||
"ui.messagePart.title.edit": "Editar",
|
||||
|
||||
@@ -58,6 +58,7 @@ export const dict = {
|
||||
"ui.sessionTurn.summary.response": "Réponse",
|
||||
"ui.sessionTurn.diff.showMore": "Afficher plus de modifications ({{count}})",
|
||||
"ui.sessionTurn.diffs.changed.one": "Fichier modifié : {{count}}",
|
||||
"ui.sessionTurn.diffs.changed.many": "Fichiers modifiés : {{count}}",
|
||||
"ui.sessionTurn.diffs.changed.other": "Fichiers modifiés : {{count}}",
|
||||
"ui.sessionTurn.diffs.showAll": "Tout afficher",
|
||||
"ui.sessionTurn.diffs.showLess": "Afficher moins",
|
||||
@@ -96,10 +97,13 @@ export const dict = {
|
||||
"ui.messagePart.questions.dismissed": "Questions ignorées",
|
||||
"ui.messagePart.compaction": "Session compactée",
|
||||
"ui.messagePart.context.read.one": "{{count}} lecture",
|
||||
"ui.messagePart.context.read.many": "{{count}} de lectures",
|
||||
"ui.messagePart.context.read.other": "{{count}} lectures",
|
||||
"ui.messagePart.context.search.one": "{{count}} recherche",
|
||||
"ui.messagePart.context.search.many": "{{count}} de recherches",
|
||||
"ui.messagePart.context.search.other": "{{count}} recherches",
|
||||
"ui.messagePart.context.list.one": "{{count}} liste",
|
||||
"ui.messagePart.context.list.many": "{{count}} de listes",
|
||||
"ui.messagePart.context.list.other": "{{count}} listes",
|
||||
"ui.messagePart.diagnostic.error": "Erreur",
|
||||
"ui.messagePart.title.edit": "Modifier",
|
||||
|
||||
@@ -56,6 +56,7 @@ export const dict: Record<string, string> = {
|
||||
"ui.sessionTurn.summary.response": "Risposta",
|
||||
"ui.sessionTurn.diff.showMore": "Mostra più modifiche ({{count}})",
|
||||
"ui.sessionTurn.diffs.changed.one": "{{count}} file modificato",
|
||||
"ui.sessionTurn.diffs.changed.many": "{{count}} di file modificati",
|
||||
"ui.sessionTurn.diffs.changed.other": "{{count}} file modificati",
|
||||
"ui.sessionTurn.diffs.showAll": "Mostra tutto",
|
||||
"ui.sessionTurn.diffs.showLess": "Mostra meno",
|
||||
@@ -95,10 +96,13 @@ export const dict: Record<string, string> = {
|
||||
"ui.messagePart.questions.dismissed": "Domande ignorate",
|
||||
"ui.messagePart.compaction": "Sessione compattata",
|
||||
"ui.messagePart.context.read.one": "{{count}} lettura",
|
||||
"ui.messagePart.context.read.many": "{{count}} di letture",
|
||||
"ui.messagePart.context.read.other": "{{count}} letture",
|
||||
"ui.messagePart.context.search.one": "{{count}} ricerca",
|
||||
"ui.messagePart.context.search.many": "{{count}} di ricerche",
|
||||
"ui.messagePart.context.search.other": "{{count}} ricerche",
|
||||
"ui.messagePart.context.list.one": "{{count}} elenco",
|
||||
"ui.messagePart.context.list.many": "{{count}} di elenchi",
|
||||
"ui.messagePart.context.list.other": "{{count}} elenchi",
|
||||
"ui.list.loading": "Caricamento",
|
||||
"ui.list.empty": "Nessun risultato",
|
||||
|
||||
@@ -56,6 +56,8 @@ export const dict = {
|
||||
"ui.sessionTurn.summary.response": "Odpowiedź",
|
||||
"ui.sessionTurn.diff.showMore": "Pokaż więcej zmian ({{count}})",
|
||||
"ui.sessionTurn.diffs.changed.one": "{{count}} zmieniony plik",
|
||||
"ui.sessionTurn.diffs.changed.few": "{{count}} zmienione pliki",
|
||||
"ui.sessionTurn.diffs.changed.many": "{{count}} zmienionych plików",
|
||||
"ui.sessionTurn.diffs.changed.other": "Liczba zmienionych plików: {{count}}",
|
||||
"ui.sessionTurn.diffs.showAll": "Pokaż wszystkie",
|
||||
"ui.sessionTurn.diffs.showLess": "Pokaż mniej",
|
||||
@@ -94,10 +96,16 @@ export const dict = {
|
||||
"ui.messagePart.questions.dismissed": "Pytania odrzucone",
|
||||
"ui.messagePart.compaction": "Sesja skompaktowana",
|
||||
"ui.messagePart.context.read.one": "Liczba odczytów: {{count}}",
|
||||
"ui.messagePart.context.read.few": "Liczba odczytów: {{count}}",
|
||||
"ui.messagePart.context.read.many": "Liczba odczytów: {{count}}",
|
||||
"ui.messagePart.context.read.other": "Liczba odczytów: {{count}}",
|
||||
"ui.messagePart.context.search.one": "Liczba wyszukiwań: {{count}}",
|
||||
"ui.messagePart.context.search.few": "Liczba wyszukiwań: {{count}}",
|
||||
"ui.messagePart.context.search.many": "Liczba wyszukiwań: {{count}}",
|
||||
"ui.messagePart.context.search.other": "Liczba wyszukiwań: {{count}}",
|
||||
"ui.messagePart.context.list.one": "Liczba list: {{count}}",
|
||||
"ui.messagePart.context.list.few": "Liczba list: {{count}}",
|
||||
"ui.messagePart.context.list.many": "Liczba list: {{count}}",
|
||||
"ui.messagePart.context.list.other": "Liczba list: {{count}}",
|
||||
"ui.messagePart.diagnostic.error": "Błąd",
|
||||
"ui.messagePart.title.edit": "Edycja",
|
||||
|
||||
@@ -56,6 +56,8 @@ export const dict = {
|
||||
"ui.sessionTurn.summary.response": "Ответ",
|
||||
"ui.sessionTurn.diff.showMore": "Показать ещё изменений ({{count}})",
|
||||
"ui.sessionTurn.diffs.changed.one": "Изменённый файл: {{count}}",
|
||||
"ui.sessionTurn.diffs.changed.few": "{{count}} изменённых файла",
|
||||
"ui.sessionTurn.diffs.changed.many": "{{count}} изменённых файлов",
|
||||
"ui.sessionTurn.diffs.changed.other": "Изменённые файлы: {{count}}",
|
||||
"ui.sessionTurn.diffs.showAll": "Показать все",
|
||||
"ui.sessionTurn.diffs.showLess": "Показать меньше",
|
||||
@@ -94,10 +96,16 @@ export const dict = {
|
||||
"ui.messagePart.questions.dismissed": "Вопросы отклонены",
|
||||
"ui.messagePart.compaction": "Сессия сжата",
|
||||
"ui.messagePart.context.read.one": "{{count}} чтение",
|
||||
"ui.messagePart.context.read.few": "{{count}} чтения",
|
||||
"ui.messagePart.context.read.many": "{{count}} чтений",
|
||||
"ui.messagePart.context.read.other": "Операций чтения: {{count}}",
|
||||
"ui.messagePart.context.search.one": "{{count}} поиск",
|
||||
"ui.messagePart.context.search.few": "{{count}} поиска",
|
||||
"ui.messagePart.context.search.many": "{{count}} поисков",
|
||||
"ui.messagePart.context.search.other": "Операций поиска: {{count}}",
|
||||
"ui.messagePart.context.list.one": "{{count}} список",
|
||||
"ui.messagePart.context.list.few": "{{count}} списка",
|
||||
"ui.messagePart.context.list.many": "{{count}} списков",
|
||||
"ui.messagePart.context.list.other": "Получено списков: {{count}}",
|
||||
"ui.messagePart.diagnostic.error": "Ошибка",
|
||||
"ui.messagePart.title.edit": "Редактировать",
|
||||
|
||||
@@ -59,6 +59,8 @@ export const dict: Record<string, string> = {
|
||||
"ui.sessionTurn.summary.response": "Відповідь",
|
||||
"ui.sessionTurn.diff.showMore": "Показати більше змін ({{count}})",
|
||||
"ui.sessionTurn.diffs.changed.one": "Змінений файл: {{count}}",
|
||||
"ui.sessionTurn.diffs.changed.few": "Змінені файли: {{count}}",
|
||||
"ui.sessionTurn.diffs.changed.many": "Змінених файлів: {{count}}",
|
||||
"ui.sessionTurn.diffs.changed.other": "Змінені файли: {{count}}",
|
||||
"ui.sessionTurn.diffs.showAll": "Показати всі",
|
||||
"ui.sessionTurn.diffs.showLess": "Показати менше",
|
||||
@@ -102,10 +104,16 @@ export const dict: Record<string, string> = {
|
||||
"ui.messagePart.questions.dismissed": "Запитання відхилено",
|
||||
"ui.messagePart.compaction": "Сесію стиснуто",
|
||||
"ui.messagePart.context.read.one": "{{count}} читання",
|
||||
"ui.messagePart.context.read.few": "{{count}} читання",
|
||||
"ui.messagePart.context.read.many": "{{count}} читань",
|
||||
"ui.messagePart.context.read.other": "{{count}} читань",
|
||||
"ui.messagePart.context.search.one": "{{count}} пошук",
|
||||
"ui.messagePart.context.search.few": "{{count}} пошуки",
|
||||
"ui.messagePart.context.search.many": "{{count}} пошуків",
|
||||
"ui.messagePart.context.search.other": "{{count}} пошуків",
|
||||
"ui.messagePart.context.list.one": "{{count}} список",
|
||||
"ui.messagePart.context.list.few": "{{count}} списки",
|
||||
"ui.messagePart.context.list.many": "{{count}} списків",
|
||||
"ui.messagePart.context.list.other": "{{count}} списків",
|
||||
|
||||
"ui.list.loading": "Завантаження",
|
||||
|
||||
Reference in New Issue
Block a user