feat(app): support locale plural rules (#40370)

Co-authored-by: Luke Parker <10430890+Hona@users.noreply.github.com>
This commit is contained in:
opencode-agent[bot]
2026-08-04 04:36:57 +00:00
committed by GitHub
parent c7aadc83e1
commit b1553c2b5c
33 changed files with 289 additions and 60 deletions
+1 -1
View File
@@ -233,7 +233,7 @@ function ResolvedDraftRoute(props: { draft: DraftTab }) {
function UiI18nBridge(props: ParentProps) { function UiI18nBridge(props: ParentProps) {
const language = useLanguage() const language = useLanguage()
return <I18nProvider value={{ locale: language.intl, t: language.t }}>{props.children}</I18nProvider> return <I18nProvider value={{ locale: language.intl, t: language.t, plural: language.plural }}>{props.children}</I18nProvider>
} }
function LayoutCompatibility(props: ParentProps) { function LayoutCompatibility(props: ParentProps) {
+15 -1
View File
@@ -1,7 +1,7 @@
import * as i18n from "@solid-primitives/i18n" import * as i18n from "@solid-primitives/i18n"
import { createEffect, createMemo, createResource } from "solid-js" import { createEffect, createMemo, createResource } from "solid-js"
import { createStore } from "solid-js/store" import { createStore } from "solid-js/store"
import { createSimpleContext } from "@opencode-ai/ui/context" import { createSimpleContext, pluralCategory, type UiI18nPluralKey } from "@opencode-ai/ui/context"
import { Persist, persisted } from "@/utils/persist" import { Persist, persisted } from "@/utils/persist"
import { dict as en } from "@/i18n/en" import { dict as en } from "@/i18n/en"
import { dict as uiEn } from "@opencode-ai/ui/i18n/en" import { dict as uiEn } from "@opencode-ai/ui/i18n/en"
@@ -38,6 +38,11 @@ export type Locale =
type RawDictionary = typeof en & typeof uiEn type RawDictionary = typeof en & typeof uiEn
type Dictionary = i18n.Flatten<RawDictionary> type Dictionary = i18n.Flatten<RawDictionary>
type PluralKey =
| UiI18nPluralKey
| "session.question.pending"
| "session.followupDock.summary"
| "session.revertDock.summary"
type Source = { dict: Record<string, string> } type Source = { dict: Record<string, string> }
function cookie(locale: Locale) { function cookie(locale: Locale) {
@@ -294,6 +299,14 @@ export const { use: useLanguage, provider: LanguageProvider } = createSimpleCont
params?: Record<string, string | number | boolean>, params?: Record<string, string | number | boolean>,
) => string ) => string
const plural = (key: PluralKey, count: number, params?: Record<string, string | number | boolean>) => {
const category = pluralCategory(intl(), count)
const current = (dict.loading ? base : (dict() ?? base)) as Record<string, string>
const candidate = `${key}.${category}`
const fallback = `${key}.other`
return i18n.resolveTemplate(current[candidate] ?? current[fallback] ?? fallback, { ...params, count })
}
const label = (value: Locale) => { const label = (value: Locale) => {
const key = LABEL_KEY[value] const key = LABEL_KEY[value]
if (key) return t(key) if (key) return t(key)
@@ -313,6 +326,7 @@ export const { use: useLanguage, provider: LanguageProvider } = createSimpleCont
locales: LOCALES, locales: LOCALES,
label, label,
t, t,
plural,
setLocale(next: Locale) { setLocale(next: Locale) {
setStore("locale", normalizeLocale(next)) setStore("locale", normalizeLocale(next))
}, },
+12
View File
@@ -593,14 +593,26 @@ export const dict = {
"session.question.minimize": "تصغير السؤال", "session.question.minimize": "تصغير السؤال",
"session.question.restore": "استعادة السؤال", "session.question.restore": "استعادة السؤال",
"session.question.pending.one": "{{count}} سؤال معلق", "session.question.pending.one": "{{count}} سؤال معلق",
"session.question.pending.zero": "عدد الأسئلة المعلقة: {{count}}",
"session.question.pending.two": "عدد الأسئلة المعلقة: {{count}}",
"session.question.pending.few": "{{count}} أسئلة معلقة",
"session.question.pending.many": "{{count}} سؤالًا معلقًا",
"session.question.pending.other": "الأسئلة المعلقة: {{count}}", "session.question.pending.other": "الأسئلة المعلقة: {{count}}",
"session.followupDock.summary.one": "{{count}} رسالة في قائمة الانتظار", "session.followupDock.summary.one": "{{count}} رسالة في قائمة الانتظار",
"session.followupDock.summary.zero": "عدد الرسائل في قائمة الانتظار: {{count}}",
"session.followupDock.summary.two": "عدد الرسائل في قائمة الانتظار: {{count}}",
"session.followupDock.summary.few": "{{count}} رسائل في قائمة الانتظار",
"session.followupDock.summary.many": "{{count}} رسالةً في قائمة الانتظار",
"session.followupDock.summary.other": "{{count}} رسائل في قائمة الانتظار", "session.followupDock.summary.other": "{{count}} رسائل في قائمة الانتظار",
"session.followupDock.sendNow": "إرسال الآن", "session.followupDock.sendNow": "إرسال الآن",
"session.followupDock.edit": "تحرير", "session.followupDock.edit": "تحرير",
"session.followupDock.collapse": "طي الرسائل المنتظرة", "session.followupDock.collapse": "طي الرسائل المنتظرة",
"session.followupDock.expand": "توسيع الرسائل المنتظرة", "session.followupDock.expand": "توسيع الرسائل المنتظرة",
"session.revertDock.summary.one": "{{count}} رسالة تم التراجع عنها", "session.revertDock.summary.one": "{{count}} رسالة تم التراجع عنها",
"session.revertDock.summary.zero": "عدد الرسائل التي تم التراجع عنها: {{count}}",
"session.revertDock.summary.two": "عدد الرسائل التي تم التراجع عنها: {{count}}",
"session.revertDock.summary.few": "{{count}} رسائل تم التراجع عنها",
"session.revertDock.summary.many": "{{count}} رسالةً تم التراجع عنها",
"session.revertDock.summary.other": "{{count}} رسائل تم التراجع عنها", "session.revertDock.summary.other": "{{count}} رسائل تم التراجع عنها",
"session.revertDock.collapse": "طي الرسائل التي تم التراجع عنها", "session.revertDock.collapse": "طي الرسائل التي تم التراجع عنها",
"session.revertDock.expand": "توسيع الرسائل التي تم التراجع عنها", "session.revertDock.expand": "توسيع الرسائل التي تم التراجع عنها",
+3
View File
@@ -599,14 +599,17 @@ export const dict = {
"session.question.minimize": "Minimizar pergunta", "session.question.minimize": "Minimizar pergunta",
"session.question.restore": "Restaurar pergunta", "session.question.restore": "Restaurar pergunta",
"session.question.pending.one": "{{count}} pergunta pendente", "session.question.pending.one": "{{count}} pergunta pendente",
"session.question.pending.many": "{{count}} de perguntas pendentes",
"session.question.pending.other": "{{count}} perguntas pendentes", "session.question.pending.other": "{{count}} perguntas pendentes",
"session.followupDock.summary.one": "{{count}} mensagem na fila", "session.followupDock.summary.one": "{{count}} mensagem na fila",
"session.followupDock.summary.many": "{{count}} de mensagens na fila",
"session.followupDock.summary.other": "{{count}} mensagens na fila", "session.followupDock.summary.other": "{{count}} mensagens na fila",
"session.followupDock.sendNow": "Enviar agora", "session.followupDock.sendNow": "Enviar agora",
"session.followupDock.edit": "Editar", "session.followupDock.edit": "Editar",
"session.followupDock.collapse": "Recolher mensagens na fila", "session.followupDock.collapse": "Recolher mensagens na fila",
"session.followupDock.expand": "Expandir mensagens na fila", "session.followupDock.expand": "Expandir mensagens na fila",
"session.revertDock.summary.one": "{{count}} mensagem revertida", "session.revertDock.summary.one": "{{count}} mensagem revertida",
"session.revertDock.summary.many": "{{count}} de mensagens revertidas",
"session.revertDock.summary.other": "{{count}} mensagens revertidas", "session.revertDock.summary.other": "{{count}} mensagens revertidas",
"session.revertDock.collapse": "Recolher mensagens revertidas", "session.revertDock.collapse": "Recolher mensagens revertidas",
"session.revertDock.expand": "Expandir mensagens revertidas", "session.revertDock.expand": "Expandir mensagens revertidas",
+3
View File
@@ -654,14 +654,17 @@ export const dict = {
"session.question.minimize": "Minimiziraj pitanje", "session.question.minimize": "Minimiziraj pitanje",
"session.question.restore": "Vrati pitanje", "session.question.restore": "Vrati pitanje",
"session.question.pending.one": "{{count}} pitanje na čekanju", "session.question.pending.one": "{{count}} pitanje na čekanju",
"session.question.pending.few": "{{count}} pitanja na čekanju",
"session.question.pending.other": "{{count}} pitanja na čekanju", "session.question.pending.other": "{{count}} pitanja na čekanju",
"session.followupDock.summary.one": "{{count}} poruka na čekanju", "session.followupDock.summary.one": "{{count}} poruka na čekanju",
"session.followupDock.summary.few": "{{count}} poruke na čekanju",
"session.followupDock.summary.other": "{{count}} poruka na čekanju", "session.followupDock.summary.other": "{{count}} poruka na čekanju",
"session.followupDock.sendNow": "Pošalji sada", "session.followupDock.sendNow": "Pošalji sada",
"session.followupDock.edit": "Uredi", "session.followupDock.edit": "Uredi",
"session.followupDock.collapse": "Sažmi poruke na čekanju", "session.followupDock.collapse": "Sažmi poruke na čekanju",
"session.followupDock.expand": "Proširi poruke na čekanju", "session.followupDock.expand": "Proširi poruke na čekanju",
"session.revertDock.summary.one": "{{count}} vraćena poruka", "session.revertDock.summary.one": "{{count}} vraćena poruka",
"session.revertDock.summary.few": "{{count}} vraćene poruke",
"session.revertDock.summary.other": "{{count}} vraćenih poruka", "session.revertDock.summary.other": "{{count}} vraćenih poruka",
"session.revertDock.collapse": "Sažmi vraćene poruke", "session.revertDock.collapse": "Sažmi vraćene poruke",
"session.revertDock.expand": "Proširi vraćene poruke", "session.revertDock.expand": "Proširi vraćene poruke",
+3
View File
@@ -657,14 +657,17 @@ export const dict = {
"session.question.minimize": "Minimizar pregunta", "session.question.minimize": "Minimizar pregunta",
"session.question.restore": "Restaurar pregunta", "session.question.restore": "Restaurar pregunta",
"session.question.pending.one": "{{count}} pregunta pendiente", "session.question.pending.one": "{{count}} pregunta pendiente",
"session.question.pending.many": "{{count}} de preguntas pendientes",
"session.question.pending.other": "{{count}} preguntas pendientes", "session.question.pending.other": "{{count}} preguntas pendientes",
"session.followupDock.summary.one": "{{count}} mensaje en cola", "session.followupDock.summary.one": "{{count}} mensaje en cola",
"session.followupDock.summary.many": "{{count}} de mensajes en cola",
"session.followupDock.summary.other": "{{count}} mensajes en cola", "session.followupDock.summary.other": "{{count}} mensajes en cola",
"session.followupDock.sendNow": "Enviar ahora", "session.followupDock.sendNow": "Enviar ahora",
"session.followupDock.edit": "Editar", "session.followupDock.edit": "Editar",
"session.followupDock.collapse": "Contraer mensajes en cola", "session.followupDock.collapse": "Contraer mensajes en cola",
"session.followupDock.expand": "Expandir mensajes en cola", "session.followupDock.expand": "Expandir mensajes en cola",
"session.revertDock.summary.one": "{{count}} mensaje revertido", "session.revertDock.summary.one": "{{count}} mensaje revertido",
"session.revertDock.summary.many": "{{count}} de mensajes revertidos",
"session.revertDock.summary.other": "{{count}} mensajes revertidos", "session.revertDock.summary.other": "{{count}} mensajes revertidos",
"session.revertDock.collapse": "Contraer mensajes revertidos", "session.revertDock.collapse": "Contraer mensajes revertidos",
"session.revertDock.expand": "Expandir mensajes revertidos", "session.revertDock.expand": "Expandir mensajes revertidos",
+3
View File
@@ -608,14 +608,17 @@ export const dict = {
"session.question.minimize": "Réduire la question", "session.question.minimize": "Réduire la question",
"session.question.restore": "Restaurer la question", "session.question.restore": "Restaurer la question",
"session.question.pending.one": "{{count}} question en attente", "session.question.pending.one": "{{count}} question en attente",
"session.question.pending.many": "{{count}} de questions en attente",
"session.question.pending.other": "{{count}} questions en attente", "session.question.pending.other": "{{count}} questions en attente",
"session.followupDock.summary.one": "{{count}} message en file d'attente", "session.followupDock.summary.one": "{{count}} message en file d'attente",
"session.followupDock.summary.many": "{{count}} de messages en file d'attente",
"session.followupDock.summary.other": "{{count}} messages en file d'attente", "session.followupDock.summary.other": "{{count}} messages en file d'attente",
"session.followupDock.sendNow": "Envoyer maintenant", "session.followupDock.sendNow": "Envoyer maintenant",
"session.followupDock.edit": "Modifier", "session.followupDock.edit": "Modifier",
"session.followupDock.collapse": "Réduire les messages en file d'attente", "session.followupDock.collapse": "Réduire les messages en file d'attente",
"session.followupDock.expand": "Développer les messages en file d'attente", "session.followupDock.expand": "Développer les messages en file d'attente",
"session.revertDock.summary.one": "{{count}} message annulé", "session.revertDock.summary.one": "{{count}} message annulé",
"session.revertDock.summary.many": "{{count}} de messages annulés",
"session.revertDock.summary.other": "{{count}} messages annulés", "session.revertDock.summary.other": "{{count}} messages annulés",
"session.revertDock.collapse": "Réduire les messages annulés", "session.revertDock.collapse": "Réduire les messages annulés",
"session.revertDock.expand": "Développer les messages annulés", "session.revertDock.expand": "Développer les messages annulés",
+3
View File
@@ -627,14 +627,17 @@ export const dict = {
"session.question.minimize": "Riduci la domanda", "session.question.minimize": "Riduci la domanda",
"session.question.restore": "Ripristina la domanda", "session.question.restore": "Ripristina la domanda",
"session.question.pending.one": "{{count}} domanda in sospeso", "session.question.pending.one": "{{count}} domanda in sospeso",
"session.question.pending.many": "{{count}} di domande in sospeso",
"session.question.pending.other": "{{count}} domande in sospeso", "session.question.pending.other": "{{count}} domande in sospeso",
"session.followupDock.summary.one": "{{count}} messaggio in coda", "session.followupDock.summary.one": "{{count}} messaggio in coda",
"session.followupDock.summary.many": "{{count}} di messaggi in coda",
"session.followupDock.summary.other": "{{count}} messaggi in coda", "session.followupDock.summary.other": "{{count}} messaggi in coda",
"session.followupDock.sendNow": "Invia ora", "session.followupDock.sendNow": "Invia ora",
"session.followupDock.edit": "Modifica", "session.followupDock.edit": "Modifica",
"session.followupDock.collapse": "Comprimi i messaggi in coda", "session.followupDock.collapse": "Comprimi i messaggi in coda",
"session.followupDock.expand": "Espandi i messaggi in coda", "session.followupDock.expand": "Espandi i messaggi in coda",
"session.revertDock.summary.one": "{{count}} messaggio annullato", "session.revertDock.summary.one": "{{count}} messaggio annullato",
"session.revertDock.summary.many": "{{count}} di messaggi annullati",
"session.revertDock.summary.other": "{{count}} messaggi annullati", "session.revertDock.summary.other": "{{count}} messaggi annullati",
"session.revertDock.collapse": "Comprimi i messaggi annullati", "session.revertDock.collapse": "Comprimi i messaggi annullati",
"session.revertDock.expand": "Espandi i messaggi annullati", "session.revertDock.expand": "Espandi i messaggi annullati",
+73 -4
View File
@@ -30,6 +30,17 @@ const appLocales = [
"sv", "sv",
] as const ] as const
const desktopLocales = appLocales const desktopLocales = appLocales
const pluralCategories: Partial<Record<(typeof appLocales)[number], readonly string[]>> = {
ar: ["zero", "two", "few", "many"],
br: ["many"],
bs: ["few"],
es: ["many"],
fr: ["many"],
it: ["many"],
pl: ["few", "many"],
ru: ["few", "many"],
uk: ["few", "many"],
}
const domains = [ const domains = [
{ {
@@ -53,18 +64,23 @@ const domains = [
] as const ] as const
describe.skipIf(!!process.env.CI)("i18n parity", () => { describe.skipIf(!!process.env.CI)("i18n parity", () => {
test("non-English locales have every English key", async () => { test("non-English locales have every English key and required plural variants", async () => {
for (const domain of domains) { for (const domain of domains) {
const source = await dictionary(domain.source) const source = await dictionary(domain.source)
for (const locale of domain.locales) { for (const locale of domain.locales) {
const target = await dictionary(domain.target(locale)) const target = await dictionary(domain.target(locale))
const missing = Object.keys(source).filter((key) => !Object.hasOwn(target, key)) const missing = Object.keys(source).filter((key) => !Object.hasOwn(target, key))
const extra = Object.keys(target).filter((key) => !Object.hasOwn(source, key)) const extra = Object.keys(target)
.filter((key) => !Object.hasOwn(source, key))
.sort()
const expected = pluralFamilies(source)
.flatMap((key) => (pluralCategories[locale] ?? []).map((category) => `${key}.${category}`))
.sort()
expect({ domain: domain.name, locale, missing, extra }).toEqual({ expect({ domain: domain.name, locale, missing, extra }).toEqual({
domain: domain.name, domain: domain.name,
locale, locale,
missing: [], missing: [],
extra: [], extra: expected,
}) })
} }
} }
@@ -78,7 +94,17 @@ describe.skipIf(!!process.env.CI)("i18n parity", () => {
const mismatched = Object.keys(source).filter( const mismatched = Object.keys(source).filter(
(key) => Object.hasOwn(target, key) && placeholders(source[key]).join() !== placeholders(target[key]).join(), (key) => Object.hasOwn(target, key) && placeholders(source[key]).join() !== placeholders(target[key]).join(),
) )
expect({ domain: domain.name, locale, mismatched }).toEqual({ domain: domain.name, locale, mismatched: [] }) const pluralMismatched = pluralFamilies(source).flatMap((key) =>
(pluralCategories[locale] ?? [])
.map((category) => `${key}.${category}`)
.filter((variant) => placeholders(source[`${key}.other`]).join() !== placeholders(target[variant]).join()),
)
expect({ domain: domain.name, locale, mismatched, pluralMismatched }).toEqual({
domain: domain.name,
locale,
mismatched: [],
pluralMismatched: [],
})
} }
} }
}) })
@@ -110,6 +136,38 @@ describe.skipIf(!!process.env.CI)("i18n parity", () => {
}) })
}) })
describe("i18n plural parity", () => {
test("locale-specific categories exist and preserve count placeholders", async () => {
for (const domain of domains.slice(0, 2)) {
const source = await dictionary(domain.source)
const families = pluralFamilies(source)
for (const locale of domain.locales) {
const target = await dictionary(domain.target(locale))
const missing = families.flatMap((key) =>
(pluralCategories[locale] ?? [])
.map((category) => `${key}.${category}`)
.filter((variant) => !Object.hasOwn(target, variant)),
)
const mismatched = families.flatMap((key) =>
(pluralCategories[locale] ?? [])
.map((category) => `${key}.${category}`)
.filter(
(variant) =>
Object.hasOwn(target, variant) &&
placeholders(source[`${key}.other`]).join() !== placeholders(target[variant]).join(),
),
)
expect({ domain: domain.name, locale, missing, mismatched }).toEqual({
domain: domain.name,
locale,
missing: [],
mismatched: [],
})
}
}
})
})
async function dictionary(file: string) { async function dictionary(file: string) {
const module: unknown = await import(file) const module: unknown = await import(file)
if (typeof module !== "object" || module === null || !("dict" in module) || !isDictionary(module.dict)) { if (typeof module !== "object" || module === null || !("dict" in module) || !isDictionary(module.dict)) {
@@ -126,3 +184,14 @@ function isDictionary(value: unknown): value is Record<string, string> {
function placeholders(value: string) { function placeholders(value: string) {
return Array.from(value.matchAll(/{{\s*([^}]+?)\s*}}/g), (match) => match[1]).sort() return Array.from(value.matchAll(/{{\s*([^}]+?)\s*}}/g), (match) => match[1]).sort()
} }
function pluralFamilies(dictionary: Record<string, string>) {
return Object.keys(dictionary)
.filter(
(key) =>
key.endsWith(".one") &&
dictionary[key].includes("{{count}}") &&
dictionary[`${key.slice(0, -4)}.other`]?.includes("{{count}}"),
)
.map((key) => key.slice(0, -4))
}
+6
View File
@@ -599,14 +599,20 @@ export const dict = {
"session.question.minimize": "Zminimalizuj pytanie", "session.question.minimize": "Zminimalizuj pytanie",
"session.question.restore": "Przywróć pytanie", "session.question.restore": "Przywróć pytanie",
"session.question.pending.one": "{{count}} oczekujące pytanie", "session.question.pending.one": "{{count}} oczekujące pytanie",
"session.question.pending.few": "{{count}} oczekujące pytania",
"session.question.pending.many": "{{count}} oczekujących pytań",
"session.question.pending.other": "Pytania oczekujące: {{count}}", "session.question.pending.other": "Pytania oczekujące: {{count}}",
"session.followupDock.summary.one": "{{count}} wiadomość w kolejce", "session.followupDock.summary.one": "{{count}} wiadomość w kolejce",
"session.followupDock.summary.few": "{{count}} wiadomości w kolejce",
"session.followupDock.summary.many": "{{count}} wiadomości w kolejce",
"session.followupDock.summary.other": "{{count}} wiadomości w kolejce", "session.followupDock.summary.other": "{{count}} wiadomości w kolejce",
"session.followupDock.sendNow": "Wyślij teraz", "session.followupDock.sendNow": "Wyślij teraz",
"session.followupDock.edit": "Edytuj", "session.followupDock.edit": "Edytuj",
"session.followupDock.collapse": "Zwiń wiadomości w kolejce", "session.followupDock.collapse": "Zwiń wiadomości w kolejce",
"session.followupDock.expand": "Rozwiń wiadomości w kolejce", "session.followupDock.expand": "Rozwiń wiadomości w kolejce",
"session.revertDock.summary.one": "{{count}} cofnięta wiadomość", "session.revertDock.summary.one": "{{count}} cofnięta wiadomość",
"session.revertDock.summary.few": "{{count}} cofnięte wiadomości",
"session.revertDock.summary.many": "{{count}} cofniętych wiadomości",
"session.revertDock.summary.other": "{{count}} cofnięte wiadomości", "session.revertDock.summary.other": "{{count}} cofnięte wiadomości",
"session.revertDock.collapse": "Zwiń cofnięte wiadomości", "session.revertDock.collapse": "Zwiń cofnięte wiadomości",
"session.revertDock.expand": "Rozwiń cofnięte wiadomości", "session.revertDock.expand": "Rozwiń cofnięte wiadomości",
+6
View File
@@ -652,14 +652,20 @@ export const dict = {
"session.question.minimize": "Свернуть вопрос", "session.question.minimize": "Свернуть вопрос",
"session.question.restore": "Восстановить вопрос", "session.question.restore": "Восстановить вопрос",
"session.question.pending.one": "{{count}} вопрос без ответа", "session.question.pending.one": "{{count}} вопрос без ответа",
"session.question.pending.few": "{{count}} вопроса без ответа",
"session.question.pending.many": "{{count}} вопросов без ответа",
"session.question.pending.other": "Вопросы без ответа: {{count}}", "session.question.pending.other": "Вопросы без ответа: {{count}}",
"session.followupDock.summary.one": "{{count}} сообщение в очереди", "session.followupDock.summary.one": "{{count}} сообщение в очереди",
"session.followupDock.summary.few": "{{count}} сообщения в очереди",
"session.followupDock.summary.many": "{{count}} сообщений в очереди",
"session.followupDock.summary.other": "Сообщений в очереди: {{count}}", "session.followupDock.summary.other": "Сообщений в очереди: {{count}}",
"session.followupDock.sendNow": "Отправить сейчас", "session.followupDock.sendNow": "Отправить сейчас",
"session.followupDock.edit": "Редактировать", "session.followupDock.edit": "Редактировать",
"session.followupDock.collapse": "Свернуть сообщения в очереди", "session.followupDock.collapse": "Свернуть сообщения в очереди",
"session.followupDock.expand": "Развернуть сообщения в очереди", "session.followupDock.expand": "Развернуть сообщения в очереди",
"session.revertDock.summary.one": "{{count}} сообщение возвращено", "session.revertDock.summary.one": "{{count}} сообщение возвращено",
"session.revertDock.summary.few": "{{count}} сообщения возвращены",
"session.revertDock.summary.many": "{{count}} сообщений возвращено",
"session.revertDock.summary.other": "Возвращено сообщений: {{count}}", "session.revertDock.summary.other": "Возвращено сообщений: {{count}}",
"session.revertDock.collapse": "Свернуть возвращённые сообщения", "session.revertDock.collapse": "Свернуть возвращённые сообщения",
"session.revertDock.expand": "Развернуть возвращённые сообщения", "session.revertDock.expand": "Развернуть возвращённые сообщения",
+6
View File
@@ -682,14 +682,20 @@ export const dict = {
"session.question.minimize": "Згорнути запитання", "session.question.minimize": "Згорнути запитання",
"session.question.restore": "Відновити запитання", "session.question.restore": "Відновити запитання",
"session.question.pending.one": "{{count}} запитання очікує відповіді", "session.question.pending.one": "{{count}} запитання очікує відповіді",
"session.question.pending.few": "{{count}} запитання очікують відповіді",
"session.question.pending.many": "{{count}} запитань очікують відповіді",
"session.question.pending.other": "Запитання, що очікують відповіді: {{count}}", "session.question.pending.other": "Запитання, що очікують відповіді: {{count}}",
"session.followupDock.summary.one": "{{count}} повідомлення в черзі", "session.followupDock.summary.one": "{{count}} повідомлення в черзі",
"session.followupDock.summary.few": "{{count}} повідомлення в черзі",
"session.followupDock.summary.many": "{{count}} повідомлень у черзі",
"session.followupDock.summary.other": "{{count}} повідомлень у черзі", "session.followupDock.summary.other": "{{count}} повідомлень у черзі",
"session.followupDock.sendNow": "Надіслати зараз", "session.followupDock.sendNow": "Надіслати зараз",
"session.followupDock.edit": "Редагувати", "session.followupDock.edit": "Редагувати",
"session.followupDock.collapse": "Згорнути повідомлення в черзі", "session.followupDock.collapse": "Згорнути повідомлення в черзі",
"session.followupDock.expand": "Розгорнути повідомлення в черзі", "session.followupDock.expand": "Розгорнути повідомлення в черзі",
"session.revertDock.summary.one": "{{count}} скасоване повідомлення", "session.revertDock.summary.one": "{{count}} скасоване повідомлення",
"session.revertDock.summary.few": "{{count}} скасовані повідомлення",
"session.revertDock.summary.many": "{{count}} скасованих повідомлень",
"session.revertDock.summary.other": "{{count}} скасованих повідомлень", "session.revertDock.summary.other": "{{count}} скасованих повідомлень",
"session.revertDock.collapse": "Згорнути скасовані повідомлення", "session.revertDock.collapse": "Згорнути скасовані повідомлення",
"session.revertDock.expand": "Розгорнути скасовані повідомлення", "session.revertDock.expand": "Розгорнути скасовані повідомлення",
@@ -18,11 +18,7 @@ export function SessionFollowupDock(props: {
const toggle = () => setStore("collapsed", (value) => !value) const toggle = () => setStore("collapsed", (value) => !value)
const total = createMemo(() => props.items.length) const total = createMemo(() => props.items.length)
const label = createMemo(() => const label = createMemo(() => language.plural("session.followupDock.summary", total()))
language.t(total() === 1 ? "session.followupDock.summary.one" : "session.followupDock.summary.other", {
count: total(),
}),
)
const preview = createMemo(() => props.items[0]?.text ?? "") const preview = createMemo(() => props.items[0]?.text ?? "")
return ( return (
@@ -29,11 +29,7 @@ export function SessionRevertDock(props: {
const toggle = () => setStore("collapsed", (value) => !value) const toggle = () => setStore("collapsed", (value) => !value)
const total = createMemo(() => props.items.length) const total = createMemo(() => props.items.length)
const label = createMemo(() => const label = createMemo(() => language.plural("session.revertDock.summary", total()))
language.t(total() === 1 ? "session.revertDock.summary.one" : "session.revertDock.summary.other", {
count: total(),
}),
)
const preview = createMemo(() => props.items[0]?.text ?? "") const preview = createMemo(() => props.items[0]?.text ?? "")
const onHeaderKeyDown = (event: KeyboardEvent) => { const onHeaderKeyDown = (event: KeyboardEvent) => {
@@ -161,10 +161,7 @@ function TimelineDiffSummaryRow(props: { diffs: SummaryDiff[] }) {
> >
<div data-slot="session-turn-diffs-header"> <div data-slot="session-turn-diffs-header">
<span data-slot="session-turn-diffs-label"> <span data-slot="session-turn-diffs-label">
{language.t( {language.plural("ui.sessionTurn.diffs.changed", props.diffs.length)}
props.diffs.length === 1 ? "ui.sessionTurn.diffs.changed.one" : "ui.sessionTurn.diffs.changed.other",
{ count: String(props.diffs.length) },
)}
</span> </span>
<DiffChanges changes={props.diffs} /> <DiffChanges changes={props.diffs} />
<Show when={overflow() > 0}> <Show when={overflow() > 0}>
+10 -2
View File
@@ -4,7 +4,13 @@ import { Font } from "@opencode-ai/ui/font"
import { MetaProvider } from "@solidjs/meta" import { MetaProvider } from "@solidjs/meta"
import { MarkedProvider } from "@opencode-ai/ui/context/marked" import { MarkedProvider } from "@opencode-ai/ui/context/marked"
import { DialogProvider } from "@opencode-ai/ui/context/dialog" import { DialogProvider } from "@opencode-ai/ui/context/dialog"
import { I18nProvider, type UiI18nParams } from "@opencode-ai/ui/context" import {
I18nProvider,
pluralCategory,
pluralKey,
type UiI18nParams,
type UiI18nPluralKey,
} from "@opencode-ai/ui/context"
import { dict as uiEn } from "@opencode-ai/ui/i18n/en" import { dict as uiEn } from "@opencode-ai/ui/i18n/en"
import { dict as uiZh } from "@opencode-ai/ui/i18n/zh" import { dict as uiZh } from "@opencode-ai/ui/i18n/zh"
import { createEffect, createMemo, Suspense, type ParentProps } from "solid-js" import { createEffect, createMemo, Suspense, type ParentProps } from "solid-js"
@@ -62,13 +68,15 @@ function UiI18nBridge(props: ParentProps) {
const text = value ?? String(key) const text = value ?? String(key)
return resolveTemplate(text, params) return resolveTemplate(text, params)
} }
const plural = (key: UiI18nPluralKey, count: number, params?: UiI18nParams) =>
t(pluralKey(key, pluralCategory(locale(), count)), { ...params, count })
createEffect(() => { createEffect(() => {
if (typeof document !== "object") return if (typeof document !== "object") return
document.documentElement.lang = locale() document.documentElement.lang = locale()
}) })
return <I18nProvider value={{ locale, t }}>{props.children}</I18nProvider> return <I18nProvider value={{ locale, t, plural }}>{props.children}</I18nProvider>
} }
export default function App() { export default function App() {
@@ -1097,22 +1097,16 @@ export function ContextToolGroup(props: {
<AnimatedCountList <AnimatedCountList
items={[ items={[
{ {
key: "read", key: "ui.messagePart.context.read",
count: summary().read, count: summary().read,
one: i18n.t("ui.messagePart.context.read.one"),
other: i18n.t("ui.messagePart.context.read.other"),
}, },
{ {
key: "search", key: "ui.messagePart.context.search",
count: summary().search, count: summary().search,
one: i18n.t("ui.messagePart.context.search.one"),
other: i18n.t("ui.messagePart.context.search.other"),
}, },
{ {
key: "list", key: "ui.messagePart.context.list",
count: summary().list, count: summary().list,
one: i18n.t("ui.messagePart.context.list.one"),
other: i18n.t("ui.messagePart.context.list.other"),
}, },
]} ]}
fallback="" fallback=""
@@ -441,10 +441,7 @@ export function SessionTurn(
> >
<div data-slot="session-turn-diffs-header"> <div data-slot="session-turn-diffs-header">
<span data-slot="session-turn-diffs-label"> <span data-slot="session-turn-diffs-label">
{i18n.t( {i18n.plural("ui.sessionTurn.diffs.changed", edited())}
edited() === 1 ? "ui.sessionTurn.diffs.changed.one" : "ui.sessionTurn.diffs.changed.other",
{ count: String(edited()) },
)}
</span> </span>
<DiffChanges changes={diffs()} /> <DiffChanges changes={diffs()} />
<Show when={overflow() > 0}> <Show when={overflow() > 0}>
@@ -1,5 +1,6 @@
import { createMemo } from "solid-js" import { createMemo } from "solid-js"
import { AnimatedNumber } from "@opencode-ai/ui/animated-number" import { AnimatedNumber } from "@opencode-ai/ui/animated-number"
import { pluralCategory, pluralKey, useI18n, type UiI18nPluralKey } from "@opencode-ai/ui/context"
function split(text: string) { function split(text: string) {
const match = /{{\s*count\s*}}/.exec(text) const match = /{{\s*count\s*}}/.exec(text)
@@ -23,14 +24,16 @@ function common(one: string, other: string) {
} }
} }
export function AnimatedCountLabel(props: { count: number; one: string; other: string; class?: string }) { export function AnimatedCountLabel(props: { count: number; plural: UiI18nPluralKey; class?: string }) {
const one = createMemo(() => split(props.one)) const i18n = useI18n()
const other = createMemo(() => split(props.other)) const category = createMemo(() => pluralCategory(i18n.locale(), Math.round(props.count)))
const singular = createMemo(() => Math.round(props.count) === 1) const one = createMemo(() => split(i18n.t(pluralKey(props.plural, "one"))))
const active = createMemo(() => (singular() ? one() : other())) const other = createMemo(() => split(i18n.t(pluralKey(props.plural, "other"))))
const active = createMemo(() => split(i18n.t(pluralKey(props.plural, category()))))
const suffix = createMemo(() => common(one().after, other().after)) const suffix = createMemo(() => common(one().after, other().after))
const splitSuffix = createMemo( const splitSuffix = createMemo(
() => () =>
(category() === "one" || category() === "other") &&
one().before === other().before && one().before === other().before &&
(one().after.startsWith(other().after) || other().after.startsWith(one().after)), (one().after.startsWith(other().after) || other().after.startsWith(one().after)),
) )
@@ -38,7 +41,7 @@ export function AnimatedCountLabel(props: { count: number; one: string; other: s
const stem = createMemo(() => (splitSuffix() ? suffix().stem : active().after)) const stem = createMemo(() => (splitSuffix() ? suffix().stem : active().after))
const tail = createMemo(() => { const tail = createMemo(() => {
if (!splitSuffix()) return "" if (!splitSuffix()) return ""
if (singular()) return suffix().one if (category() === "one") return suffix().one
return suffix().other return suffix().other
}) })
const showTail = createMemo(() => splitSuffix() && tail().length > 0) const showTail = createMemo(() => splitSuffix() && tail().length > 0)
@@ -25,9 +25,6 @@ as it appears in the context tool group on the session page.`,
const TEXT = { const TEXT = {
active: "Exploring", active: "Exploring",
done: "Explored", done: "Explored",
read: { one: "{{count}} read", other: "{{count}} reads" },
search: { one: "{{count}} search", other: "{{count}} searches" },
list: { one: "{{count}} list", other: "{{count}} lists" },
} as const } as const
function rand(min: number, max: number) { function rand(min: number, max: number) {
@@ -118,9 +115,9 @@ export const Playground = {
} }
const items = (): CountItem[] => [ const items = (): CountItem[] => [
{ key: "read", count: reads(), one: TEXT.read.one, other: TEXT.read.other }, { key: "ui.messagePart.context.read", count: reads() },
{ key: "search", count: searches(), one: TEXT.search.one, other: TEXT.search.other }, { key: "ui.messagePart.context.search", count: searches() },
{ key: "list", count: lists(), one: TEXT.list.one, other: TEXT.list.other }, { key: "ui.messagePart.context.list", count: lists() },
] ]
return ( return (
@@ -210,8 +207,8 @@ export const Empty = {
<ToolStatusTitle active activeText="Exploring" doneText="Explored" split={false} /> <ToolStatusTitle active activeText="Exploring" doneText="Explored" split={false} />
<AnimatedCountList <AnimatedCountList
items={[ items={[
{ key: "read", count: 0, one: "{{count}} read", other: "{{count}} reads" }, { key: "ui.messagePart.context.read", count: 0 },
{ key: "search", count: 0, one: "{{count}} search", other: "{{count}} searches" }, { key: "ui.messagePart.context.search", count: 0 },
]} ]}
fallback="" fallback=""
/> />
@@ -226,9 +223,9 @@ export const Done = {
<span style={{ "font-weight": "400", color: "var(--text-base, #ccc)" }}> <span style={{ "font-weight": "400", color: "var(--text-base, #ccc)" }}>
<AnimatedCountList <AnimatedCountList
items={[ items={[
{ key: "read", count: 5, one: "{{count}} read", other: "{{count}} reads" }, { key: "ui.messagePart.context.read", count: 5 },
{ key: "search", count: 3, one: "{{count}} search", other: "{{count}} searches" }, { key: "ui.messagePart.context.search", count: 3 },
{ key: "list", count: 1, one: "{{count}} list", other: "{{count}} lists" }, { key: "ui.messagePart.context.list", count: 1 },
]} ]}
fallback="" fallback=""
/> />
@@ -1,11 +1,10 @@
import { Index, createMemo } from "solid-js" import { Index, createMemo } from "solid-js"
import type { UiI18nPluralKey } from "@opencode-ai/ui/context"
import { AnimatedCountLabel } from "./tool-count-label" import { AnimatedCountLabel } from "./tool-count-label"
export type CountItem = { export type CountItem = {
key: string key: UiI18nPluralKey
count: number count: number
one: string
other: string
} }
export function AnimatedCountList(props: { items: CountItem[]; fallback?: string; class?: string }) { export function AnimatedCountList(props: { items: CountItem[]; fallback?: string; class?: string }) {
@@ -36,11 +35,7 @@ export function AnimatedCountList(props: { items: CountItem[]; fallback?: string
</span> </span>
<span data-slot="tool-count-summary-item" data-active={active() ? "true" : "false"}> <span data-slot="tool-count-summary-item" data-active={active() ? "true" : "false"}>
<span data-slot="tool-count-summary-item-inner"> <span data-slot="tool-count-summary-item-inner">
<AnimatedCountLabel <AnimatedCountLabel plural={item().key} count={Math.max(0, Math.round(item().count))} />
one={item().one}
other={item().other}
count={Math.max(0, Math.round(item().count))}
/>
</span> </span>
</span> </span>
</> </>
@@ -123,6 +123,8 @@ const dict: Record<string, string> = {
"prompt.example.25": "What should we test next?", "prompt.example.25": "What should we test next?",
} }
const plurals = new Intl.PluralRules("en-US")
function render(template: string, params?: Record<string, unknown>) { function render(template: string, params?: Record<string, unknown>) {
if (!params) return template if (!params) return template
return template.replace(/\{\{([^}]+)\}\}/g, (_, key: string) => { return template.replace(/\{\{([^}]+)\}\}/g, (_, key: string) => {
@@ -140,5 +142,9 @@ export function useLanguage() {
t(key: string, params?: Record<string, unknown>) { t(key: string, params?: Record<string, unknown>) {
return render(dict[key] ?? key, params) return render(dict[key] ?? key, params)
}, },
plural(key: string, count: number, params?: Record<string, unknown>) {
const value = dict[`${key}.${plurals.select(count)}`] ?? dict[`${key}.other`] ?? key
return render(value, { ...params, count })
},
} }
} }
+24
View File
@@ -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)
})
})
+28
View File
@@ -3,11 +3,37 @@ import { dict as en } from "../i18n/en"
export type UiI18nKey = keyof typeof 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 UiI18nParams = Record<string, string | number | boolean>
export type UiI18n = { export type UiI18n = {
locale: Accessor<string> locale: Accessor<string>
t: (key: UiI18nKey, params?: UiI18nParams) => 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) { function resolveTemplate(text: string, params?: UiI18nParams) {
@@ -25,6 +51,8 @@ const fallback: UiI18n = {
const value = en[key] ?? String(key) const value = en[key] ?? String(key)
return resolveTemplate(value, params) return resolveTemplate(value, params)
}, },
plural: (key, count, params) =>
fallback.t(pluralKey(key, pluralCategory(fallback.locale(), count)), { ...params, count }),
} }
const Context = createContext<UiI18n>(fallback) const Context = createContext<UiI18n>(fallback)
+16
View File
@@ -57,6 +57,10 @@ export const dict = {
"ui.sessionTurn.summary.response": "استجابة", "ui.sessionTurn.summary.response": "استجابة",
"ui.sessionTurn.diff.showMore": "إظهار المزيد من التغييرات ({{count}})", "ui.sessionTurn.diff.showMore": "إظهار المزيد من التغييرات ({{count}})",
"ui.sessionTurn.diffs.changed.one": "ملف معدل: {{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.changed.other": "الملفات المعدلة: {{count}}",
"ui.sessionTurn.diffs.showAll": "إظهار الكل", "ui.sessionTurn.diffs.showAll": "إظهار الكل",
"ui.sessionTurn.diffs.showLess": "إظهار عدد أقل", "ui.sessionTurn.diffs.showLess": "إظهار عدد أقل",
@@ -95,10 +99,22 @@ export const dict = {
"ui.messagePart.questions.dismissed": "تم إهمال الأسئلة", "ui.messagePart.questions.dismissed": "تم إهمال الأسئلة",
"ui.messagePart.compaction": "تم اختصار الجلسة", "ui.messagePart.compaction": "تم اختصار الجلسة",
"ui.messagePart.context.read.one": "{{count}} قراءة", "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.read.other": "{{count}} قراءات",
"ui.messagePart.context.search.one": "{{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.search.other": "{{count}} عمليات بحث",
"ui.messagePart.context.list.one": "{{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.context.list.other": "{{count}} عمليات سرد",
"ui.messagePart.diagnostic.error": "خطأ", "ui.messagePart.diagnostic.error": "خطأ",
"ui.messagePart.title.edit": "تحرير", "ui.messagePart.title.edit": "تحرير",
+4
View File
@@ -57,6 +57,7 @@ export const dict = {
"ui.sessionTurn.summary.response": "Resposta", "ui.sessionTurn.summary.response": "Resposta",
"ui.sessionTurn.diff.showMore": "Mostrar mais alterações ({{count}})", "ui.sessionTurn.diff.showMore": "Mostrar mais alterações ({{count}})",
"ui.sessionTurn.diffs.changed.one": "Arquivo alterado: {{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.changed.other": "Arquivos alterados: {{count}}",
"ui.sessionTurn.diffs.showAll": "Mostrar tudo", "ui.sessionTurn.diffs.showAll": "Mostrar tudo",
"ui.sessionTurn.diffs.showLess": "Mostrar menos", "ui.sessionTurn.diffs.showLess": "Mostrar menos",
@@ -95,10 +96,13 @@ export const dict = {
"ui.messagePart.questions.dismissed": "Perguntas descartadas", "ui.messagePart.questions.dismissed": "Perguntas descartadas",
"ui.messagePart.compaction": "Sessão compactada", "ui.messagePart.compaction": "Sessão compactada",
"ui.messagePart.context.read.one": "{{count}} leitura", "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.read.other": "{{count}} leituras",
"ui.messagePart.context.search.one": "{{count}} pesquisa", "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.search.other": "{{count}} pesquisas",
"ui.messagePart.context.list.one": "{{count}} lista", "ui.messagePart.context.list.one": "{{count}} lista",
"ui.messagePart.context.list.many": "{{count}} de listas",
"ui.messagePart.context.list.other": "{{count}} listas", "ui.messagePart.context.list.other": "{{count}} listas",
"ui.messagePart.diagnostic.error": "Erro", "ui.messagePart.diagnostic.error": "Erro",
"ui.messagePart.title.edit": "Editar", "ui.messagePart.title.edit": "Editar",
+4
View File
@@ -61,6 +61,7 @@ export const dict = {
"ui.sessionTurn.summary.response": "Odgovor", "ui.sessionTurn.summary.response": "Odgovor",
"ui.sessionTurn.diff.showMore": "Prikaži još izmjena ({{count}})", "ui.sessionTurn.diff.showMore": "Prikaži još izmjena ({{count}})",
"ui.sessionTurn.diffs.changed.one": "{{count}} izmijenjena datoteka", "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.changed.other": "{{count}} izmijenjene datoteke",
"ui.sessionTurn.diffs.showAll": "Prikaži sve", "ui.sessionTurn.diffs.showAll": "Prikaži sve",
"ui.sessionTurn.diffs.showLess": "Prikaži manje", "ui.sessionTurn.diffs.showLess": "Prikaži manje",
@@ -99,10 +100,13 @@ export const dict = {
"ui.messagePart.questions.dismissed": "Pitanja odbačena", "ui.messagePart.questions.dismissed": "Pitanja odbačena",
"ui.messagePart.compaction": "Sesija sažeta", "ui.messagePart.compaction": "Sesija sažeta",
"ui.messagePart.context.read.one": "{{count}} čitanje", "ui.messagePart.context.read.one": "{{count}} čitanje",
"ui.messagePart.context.read.few": "{{count}} čitanja",
"ui.messagePart.context.read.other": "{{count}} čitanja", "ui.messagePart.context.read.other": "{{count}} čitanja",
"ui.messagePart.context.search.one": "{{count}} pretraga", "ui.messagePart.context.search.one": "{{count}} pretraga",
"ui.messagePart.context.search.few": "{{count}} pretrage",
"ui.messagePart.context.search.other": "{{count}} pretrage", "ui.messagePart.context.search.other": "{{count}} pretrage",
"ui.messagePart.context.list.one": "{{count}} listanje", "ui.messagePart.context.list.one": "{{count}} listanje",
"ui.messagePart.context.list.few": "{{count}} listanja",
"ui.messagePart.context.list.other": "{{count}} listanja", "ui.messagePart.context.list.other": "{{count}} listanja",
"ui.messagePart.diagnostic.error": "Greška", "ui.messagePart.diagnostic.error": "Greška",
"ui.messagePart.title.edit": "Uredi", "ui.messagePart.title.edit": "Uredi",
+4
View File
@@ -57,6 +57,7 @@ export const dict = {
"ui.sessionTurn.summary.response": "Respuesta", "ui.sessionTurn.summary.response": "Respuesta",
"ui.sessionTurn.diff.showMore": "Mostrar más cambios ({{count}})", "ui.sessionTurn.diff.showMore": "Mostrar más cambios ({{count}})",
"ui.sessionTurn.diffs.changed.one": "{{count}} archivo modificado", "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.changed.other": "{{count}} archivos modificados",
"ui.sessionTurn.diffs.showAll": "Mostrar todos", "ui.sessionTurn.diffs.showAll": "Mostrar todos",
"ui.sessionTurn.diffs.showLess": "Mostrar menos", "ui.sessionTurn.diffs.showLess": "Mostrar menos",
@@ -95,10 +96,13 @@ export const dict = {
"ui.messagePart.questions.dismissed": "Preguntas descartadas", "ui.messagePart.questions.dismissed": "Preguntas descartadas",
"ui.messagePart.compaction": "Sesión compactada", "ui.messagePart.compaction": "Sesión compactada",
"ui.messagePart.context.read.one": "{{count}} lectura", "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.read.other": "{{count}} lecturas",
"ui.messagePart.context.search.one": "{{count}} búsqueda", "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.search.other": "{{count}} búsquedas",
"ui.messagePart.context.list.one": "{{count}} listado", "ui.messagePart.context.list.one": "{{count}} listado",
"ui.messagePart.context.list.many": "{{count}} de listados",
"ui.messagePart.context.list.other": "{{count}} listados", "ui.messagePart.context.list.other": "{{count}} listados",
"ui.messagePart.diagnostic.error": "Error", "ui.messagePart.diagnostic.error": "Error",
"ui.messagePart.title.edit": "Editar", "ui.messagePart.title.edit": "Editar",
+4
View File
@@ -58,6 +58,7 @@ export const dict = {
"ui.sessionTurn.summary.response": "Réponse", "ui.sessionTurn.summary.response": "Réponse",
"ui.sessionTurn.diff.showMore": "Afficher plus de modifications ({{count}})", "ui.sessionTurn.diff.showMore": "Afficher plus de modifications ({{count}})",
"ui.sessionTurn.diffs.changed.one": "Fichier modifié : {{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.changed.other": "Fichiers modifiés : {{count}}",
"ui.sessionTurn.diffs.showAll": "Tout afficher", "ui.sessionTurn.diffs.showAll": "Tout afficher",
"ui.sessionTurn.diffs.showLess": "Afficher moins", "ui.sessionTurn.diffs.showLess": "Afficher moins",
@@ -96,10 +97,13 @@ export const dict = {
"ui.messagePart.questions.dismissed": "Questions ignorées", "ui.messagePart.questions.dismissed": "Questions ignorées",
"ui.messagePart.compaction": "Session compactée", "ui.messagePart.compaction": "Session compactée",
"ui.messagePart.context.read.one": "{{count}} lecture", "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.read.other": "{{count}} lectures",
"ui.messagePart.context.search.one": "{{count}} recherche", "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.search.other": "{{count}} recherches",
"ui.messagePart.context.list.one": "{{count}} liste", "ui.messagePart.context.list.one": "{{count}} liste",
"ui.messagePart.context.list.many": "{{count}} de listes",
"ui.messagePart.context.list.other": "{{count}} listes", "ui.messagePart.context.list.other": "{{count}} listes",
"ui.messagePart.diagnostic.error": "Erreur", "ui.messagePart.diagnostic.error": "Erreur",
"ui.messagePart.title.edit": "Modifier", "ui.messagePart.title.edit": "Modifier",
+4
View File
@@ -56,6 +56,7 @@ export const dict: Record<string, string> = {
"ui.sessionTurn.summary.response": "Risposta", "ui.sessionTurn.summary.response": "Risposta",
"ui.sessionTurn.diff.showMore": "Mostra più modifiche ({{count}})", "ui.sessionTurn.diff.showMore": "Mostra più modifiche ({{count}})",
"ui.sessionTurn.diffs.changed.one": "{{count}} file modificato", "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.changed.other": "{{count}} file modificati",
"ui.sessionTurn.diffs.showAll": "Mostra tutto", "ui.sessionTurn.diffs.showAll": "Mostra tutto",
"ui.sessionTurn.diffs.showLess": "Mostra meno", "ui.sessionTurn.diffs.showLess": "Mostra meno",
@@ -95,10 +96,13 @@ export const dict: Record<string, string> = {
"ui.messagePart.questions.dismissed": "Domande ignorate", "ui.messagePart.questions.dismissed": "Domande ignorate",
"ui.messagePart.compaction": "Sessione compattata", "ui.messagePart.compaction": "Sessione compattata",
"ui.messagePart.context.read.one": "{{count}} lettura", "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.read.other": "{{count}} letture",
"ui.messagePart.context.search.one": "{{count}} ricerca", "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.search.other": "{{count}} ricerche",
"ui.messagePart.context.list.one": "{{count}} elenco", "ui.messagePart.context.list.one": "{{count}} elenco",
"ui.messagePart.context.list.many": "{{count}} di elenchi",
"ui.messagePart.context.list.other": "{{count}} elenchi", "ui.messagePart.context.list.other": "{{count}} elenchi",
"ui.list.loading": "Caricamento", "ui.list.loading": "Caricamento",
"ui.list.empty": "Nessun risultato", "ui.list.empty": "Nessun risultato",
+8
View File
@@ -56,6 +56,8 @@ export const dict = {
"ui.sessionTurn.summary.response": "Odpowiedź", "ui.sessionTurn.summary.response": "Odpowiedź",
"ui.sessionTurn.diff.showMore": "Pokaż więcej zmian ({{count}})", "ui.sessionTurn.diff.showMore": "Pokaż więcej zmian ({{count}})",
"ui.sessionTurn.diffs.changed.one": "{{count}} zmieniony plik", "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.changed.other": "Liczba zmienionych plików: {{count}}",
"ui.sessionTurn.diffs.showAll": "Pokaż wszystkie", "ui.sessionTurn.diffs.showAll": "Pokaż wszystkie",
"ui.sessionTurn.diffs.showLess": "Pokaż mniej", "ui.sessionTurn.diffs.showLess": "Pokaż mniej",
@@ -94,10 +96,16 @@ export const dict = {
"ui.messagePart.questions.dismissed": "Pytania odrzucone", "ui.messagePart.questions.dismissed": "Pytania odrzucone",
"ui.messagePart.compaction": "Sesja skompaktowana", "ui.messagePart.compaction": "Sesja skompaktowana",
"ui.messagePart.context.read.one": "Liczba odczytów: {{count}}", "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.read.other": "Liczba odczytów: {{count}}",
"ui.messagePart.context.search.one": "Liczba wyszukiwań: {{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.search.other": "Liczba wyszukiwań: {{count}}",
"ui.messagePart.context.list.one": "Liczba list: {{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.context.list.other": "Liczba list: {{count}}",
"ui.messagePart.diagnostic.error": "Błąd", "ui.messagePart.diagnostic.error": "Błąd",
"ui.messagePart.title.edit": "Edycja", "ui.messagePart.title.edit": "Edycja",
+8
View File
@@ -56,6 +56,8 @@ export const dict = {
"ui.sessionTurn.summary.response": "Ответ", "ui.sessionTurn.summary.response": "Ответ",
"ui.sessionTurn.diff.showMore": "Показать ещё изменений ({{count}})", "ui.sessionTurn.diff.showMore": "Показать ещё изменений ({{count}})",
"ui.sessionTurn.diffs.changed.one": "Изменённый файл: {{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.changed.other": "Изменённые файлы: {{count}}",
"ui.sessionTurn.diffs.showAll": "Показать все", "ui.sessionTurn.diffs.showAll": "Показать все",
"ui.sessionTurn.diffs.showLess": "Показать меньше", "ui.sessionTurn.diffs.showLess": "Показать меньше",
@@ -94,10 +96,16 @@ export const dict = {
"ui.messagePart.questions.dismissed": "Вопросы отклонены", "ui.messagePart.questions.dismissed": "Вопросы отклонены",
"ui.messagePart.compaction": "Сессия сжата", "ui.messagePart.compaction": "Сессия сжата",
"ui.messagePart.context.read.one": "{{count}} чтение", "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.read.other": "Операций чтения: {{count}}",
"ui.messagePart.context.search.one": "{{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.search.other": "Операций поиска: {{count}}",
"ui.messagePart.context.list.one": "{{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.context.list.other": "Получено списков: {{count}}",
"ui.messagePart.diagnostic.error": "Ошибка", "ui.messagePart.diagnostic.error": "Ошибка",
"ui.messagePart.title.edit": "Редактировать", "ui.messagePart.title.edit": "Редактировать",
+8
View File
@@ -59,6 +59,8 @@ export const dict: Record<string, string> = {
"ui.sessionTurn.summary.response": "Відповідь", "ui.sessionTurn.summary.response": "Відповідь",
"ui.sessionTurn.diff.showMore": "Показати більше змін ({{count}})", "ui.sessionTurn.diff.showMore": "Показати більше змін ({{count}})",
"ui.sessionTurn.diffs.changed.one": "Змінений файл: {{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.changed.other": "Змінені файли: {{count}}",
"ui.sessionTurn.diffs.showAll": "Показати всі", "ui.sessionTurn.diffs.showAll": "Показати всі",
"ui.sessionTurn.diffs.showLess": "Показати менше", "ui.sessionTurn.diffs.showLess": "Показати менше",
@@ -102,10 +104,16 @@ export const dict: Record<string, string> = {
"ui.messagePart.questions.dismissed": "Запитання відхилено", "ui.messagePart.questions.dismissed": "Запитання відхилено",
"ui.messagePart.compaction": "Сесію стиснуто", "ui.messagePart.compaction": "Сесію стиснуто",
"ui.messagePart.context.read.one": "{{count}} читання", "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.read.other": "{{count}} читань",
"ui.messagePart.context.search.one": "{{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.search.other": "{{count}} пошуків",
"ui.messagePart.context.list.one": "{{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.context.list.other": "{{count}} списків",
"ui.list.loading": "Завантаження", "ui.list.loading": "Завантаження",