diff --git a/src/locales/ar.json b/src/locales/ar.json index 907920f12d2..7370a51f63e 100644 --- a/src/locales/ar.json +++ b/src/locales/ar.json @@ -197,7 +197,7 @@ "timeslider.month.october": "أكتوبر", "timeslider.month.november": "نوفمبر", "timeslider.month.december": "ديسمبر", - "timeslider.unnamedauthors": "بدون اسم {{num}} {[plural(num) واحد: كاتب، آخر: مؤلف]}", + "timeslider.unnamedauthors": "بدون اسم {{num}} {[plural(num) one: كاتب, other: مؤلف ]}", "pad.savedrevs.marked": "هذا التنقيح محدد الآن كمراجعة محفوظة", "pad.savedrevs.timeslider": "يمكنك عرض المراجعات المحفوظة بزيارة متصفح التاريخ", "pad.userlist.entername": "أدخل اسمك", diff --git a/src/locales/diq.json b/src/locales/diq.json index 0886872f8cb..49bd7e2ee2a 100644 --- a/src/locales/diq.json +++ b/src/locales/diq.json @@ -159,7 +159,7 @@ "timeslider.month.october": "Tışrino Verên", "timeslider.month.november": "Tışrino Peyên", "timeslider.month.december": "Kanun", - "timeslider.unnamedauthors": "{{num}} unnamed {[plural(num) zu: nuştoğ, zewbi: nustoği ]}", + "timeslider.unnamedauthors": "{{num}} unnamed {[plural(num) one: nuştoğ, other: nustoği ]}", "pad.savedrevs.marked": "Eno vurriyayış henda qeyd bıyaye yew vurriyayış deyne nışan bıyo", "pad.savedrevs.timeslider": "Xızberê zemani ziyer kerdış ra şıma şenê revizyonanê qeyd bıyayan bıvinê", "pad.userlist.entername": "Namey xo cıkewe", diff --git a/src/locales/ff.json b/src/locales/ff.json index a1164036d37..e2fd32a1ffd 100644 --- a/src/locales/ff.json +++ b/src/locales/ff.json @@ -147,7 +147,7 @@ "timeslider.month.october": "Yarkomaa", "timeslider.month.november": "Jolal", "timeslider.month.december": "Bowte", - "timeslider.unnamedauthors": "{{num}} innitaaka {[plural(num) goo: ballifo, goɗɗo: wallifɓe]}", + "timeslider.unnamedauthors": "{{num}} innitaaka {[plural(num) one: ballifo, other: wallifɓe ]}", "pad.savedrevs.marked": "Ndee yamre maantaama jooni ko baylital danndangal", "pad.savedrevs.timeslider": "Aɗa waawi yiyde baylitte danndaaɗe so yillaade daasorde tuma ndee", "pad.userlist.entername": " Naatnu innde maa", diff --git a/src/locales/hr.json b/src/locales/hr.json index 3111f7e3a5e..4aad1692995 100644 --- a/src/locales/hr.json +++ b/src/locales/hr.json @@ -115,7 +115,7 @@ "timeslider.month.october": "listopada", "timeslider.month.november": "studenoga", "timeslider.month.december": "prosinca", - "timeslider.unnamedauthors": "{{num}} {[plural(num) one: neimenovani autor, plural(num) two: neimenovana autora, plural(num) other: neimenovanih autora ]}", + "timeslider.unnamedauthors": "{{num}} {[plural(num) one: neimenovani autor, two: neimenovana autora, other: neimenovanih autora ]}", "pad.savedrevs.marked": "Ova inačica označena je sada kao spremljena inačica", "pad.savedrevs.timeslider": "Možete vidjeti spremljene inačice rabeći vremensku lentu (timeslider)", "pad.userlist.entername": "Unesite Vaše suradničko ime", diff --git a/src/locales/hu.json b/src/locales/hu.json index 89de739c95d..484ec77a1ab 100644 --- a/src/locales/hu.json +++ b/src/locales/hu.json @@ -158,7 +158,7 @@ "timeslider.month.october": "október", "timeslider.month.november": "november", "timeslider.month.december": "december", - "timeslider.unnamedauthors": "{{num}} névtelen {[plural(num), one: szerző, other: szerző]}", + "timeslider.unnamedauthors": "{{num}} névtelen {[plural(num) one: szerző, other: szerző ]}", "pad.savedrevs.marked": "Ez a revízió mostantól mentettként jelölve", "pad.savedrevs.timeslider": "A mentett revíziókat az időcsúszkán tudod megnézni", "pad.userlist.entername": "Add meg a nevedet", diff --git a/src/locales/it.json b/src/locales/it.json index 13dd100f0ba..37586bc8c0e 100644 --- a/src/locales/it.json +++ b/src/locales/it.json @@ -395,7 +395,7 @@ "pad.savedrevs.timeslider": "Puoi vedere le versioni salvate visitando la cronologia", "pad.userlist.entername": "Inserisci il tuo nome", "pad.userlist.unnamed": "senza nome", - "pad.userlist.onlineCount": "{[ plurale(count) uno: {{count}} utente connesso, altro: {{count}} utenti connessi ]}", + "pad.userlist.onlineCount": "{[ plural(count) one: {{count}} utente connesso, other: {{count}} utenti connessi ]}", "pad.editbar.clearcolors": "Eliminare i colori degli autori sull'intero documento? Questa azione non può essere annullata", "pad.impexp.importbutton": "Importa ora", "pad.impexp.importing": "Importazione in corso...", diff --git a/src/locales/mnw.json b/src/locales/mnw.json index 09b987756b2..e8b9b6b89c7 100644 --- a/src/locales/mnw.json +++ b/src/locales/mnw.json @@ -51,7 +51,7 @@ "timeslider.month.october": "\nအံက်တဝ်ပါ", "timeslider.month.november": "\nနဝ်ဝေမ်ပါ", "timeslider.month.december": "ဒဳဇြေန်ပါ", - "timeslider.unnamedauthors": "{{num}} ဟွံကဵုယၟု {[ဂမၠိုၚ်(num) မွဲ: ကဝိ, တၞဟ်: ကဝိဂမၠိုၚ် ]}", + "timeslider.unnamedauthors": "{{num}} ဟွံကဵုယၟု {[plural(num) one: ကဝိ, other: ကဝိဂမၠိုၚ် ]}", "pad.userlist.entername": "စုတ် ယၟုညးလွပ်", "pad.userlist.unnamed": "ဟွံကဵုလဝ်ယၟု", "pad.impexp.importbutton": " မပၠောပ်စုတ် လၟုဟ်", diff --git a/src/locales/my.json b/src/locales/my.json index 522ded52a46..bf5b03c9da5 100644 --- a/src/locales/my.json +++ b/src/locales/my.json @@ -149,7 +149,7 @@ "timeslider.month.october": "အောက်တိုဘာ", "timeslider.month.november": "နို​ဝင်​ဘာ​", "timeslider.month.december": "ဒီဇင်ဘာ", - "timeslider.unnamedauthors": "{{num}} အမည်မဖော်လိုသူ {[အများကိန်း (num) one: author၊ အခြား: author]}", + "timeslider.unnamedauthors": "{{num}} အမည်မဖော်လိုသူ {[plural(num) one: author, other: author ]}", "pad.savedrevs.marked": "ယခုပြန်လည်တည်းဖြတ်မှုအားသိမ်းဆည်းထားသောတည်းဖြတ်မှုတစ်ခုအဖြစ်အမှတ်အသားပြုထားသည်", "pad.savedrevs.timeslider": "timeslider ကိုသွားခြင်းဖြင့်သိမ်းဆည်းထားသောပြန်လည်တည်းဖြတ်ချက်များကိုသင်မြင်နိုင်သည်", "pad.userlist.entername": "မင်းနာမည်ထည့်ပါ", diff --git a/src/locales/ps.json b/src/locales/ps.json index d48ec2c79e8..b7a91fff569 100644 --- a/src/locales/ps.json +++ b/src/locales/ps.json @@ -215,7 +215,7 @@ "timeslider.month.october": "اکتوبر", "timeslider.month.november": "نومبر", "timeslider.month.december": "ډېسمبر", - "timeslider.unnamedauthors": "{{num}} بې نومه {[ډېرگړي(num) يو: ليکوال، نور: ليکوالان ]}", + "timeslider.unnamedauthors": "{{num}} بې نومه {[plural(num) one: ليکوال, other: ليکوالان ]}", "pad.savedrevs.marked": "اوس دا بياليدنه د يوې خوندي شوې بياليدنې په توگه په نښه شوه", "pad.userlist.entername": "نوم مو ورکړئ", "pad.userlist.unnamed": "بې نومه", diff --git a/src/locales/ro.json b/src/locales/ro.json index 128187e5dab..79d31cd22d0 100644 --- a/src/locales/ro.json +++ b/src/locales/ro.json @@ -126,7 +126,7 @@ "timeslider.month.october": "octombrie", "timeslider.month.november": "noiembrie", "timeslider.month.december": "decembrie", - "timeslider.unnamedauthors": "{{num}} anonim {[plural (num) unul: autor, altul: autori ]}", + "timeslider.unnamedauthors": "{{num}} anonim {[plural(num) one: autor, other: autori ]}", "pad.savedrevs.marked": "Această revizie este marcată acum ca o revizuire salvată", "pad.savedrevs.timeslider": "Puteți vedea reviziile salvate accesând cursorul de timp", "pad.userlist.entername": "Introduceți numele dumneavoastră", diff --git a/src/locales/sh-latn.json b/src/locales/sh-latn.json index eb26375efac..ea2bf86197a 100644 --- a/src/locales/sh-latn.json +++ b/src/locales/sh-latn.json @@ -121,7 +121,7 @@ "timeslider.month.october": "oktobra", "timeslider.month.november": "novembra", "timeslider.month.december": "decembra", - "timeslider.unnamedauthors": "{{num}} {[plural(num) one: neimenovani autor, plural(num) two: neimenovana autora, plural(num) other: neimenovanih autora ]}", + "timeslider.unnamedauthors": "{{num}} {[plural(num) one: neimenovani autor, two: neimenovana autora, other: neimenovanih autora ]}", "pad.savedrevs.marked": "Ova inačica označena je sada kao spremljena", "pad.savedrevs.timeslider": "Možete pogledati spremljene inačice rabeći vremesledni klizač", "pad.userlist.entername": "Upišite svoje ime", diff --git a/src/locales/shn.json b/src/locales/shn.json index dd4d5b84a6e..c957638379a 100644 --- a/src/locales/shn.json +++ b/src/locales/shn.json @@ -106,7 +106,7 @@ "timeslider.month.october": "ဢွၵ်ႇထူဝ်ႇပႃႇ", "timeslider.month.november": "ၼူဝ်ႇဝႅမ်ႇပႃႇ", "timeslider.month.december": "တီႇသႅမ်ႇပႃႇ", - "timeslider.unnamedauthors": "{{num}} ဢမ်ႇသႂ်ႇၸိုဝ်ႈ {[plural(num) ၼိုင်ႈ: ၽူႈတႅမ်ႈလိၵ်ႈ, တၢင်ႇၸိူဝ်း: ၽူႈတႅမ်ႈလိၵ်ႈၶဝ် ]}", + "timeslider.unnamedauthors": "{{num}} ဢမ်ႇသႂ်ႇၸိုဝ်ႈ {[plural(num) one: ၽူႈတႅမ်ႈလိၵ်ႈ, other: ၽူႈတႅမ်ႈလိၵ်ႈၶဝ် ]}", "pad.savedrevs.marked": "ဢၼ်မႄးဝႆႉၼႆႉ ယၢမ်းလဵဝ် ၶိုၼ်းမႄး ၵဵပ်းသိမ်းဝႆႉ ၸိူင်ႉၼင်ႇဢၼ်ၼိုင်ႈ", "pad.savedrevs.timeslider": "ၸဝ်ႈၵဝ်ႇတေတူၺ်းလႆႈ ဢၼ်ၶိုၼ်းမႄးသိမ်းဝႆႉၼၼ်ႉယူႇ ပေႃးၶဝ်ႈတူၺ်းတီႈ ၶၢဝ်းယၢမ်းထေႃပူၼ်ႉ", "pad.userlist.entername": "ပေႃႇသႂ်ႇပၼ် ၸိုဝ်ႈၽူႈၸႂ်ႉတိုဝ်း ၸဝ်ႈၵဝ်ႇ", diff --git a/src/locales/sl.json b/src/locales/sl.json index 4319253c91b..69bb74ac1da 100644 --- a/src/locales/sl.json +++ b/src/locales/sl.json @@ -155,7 +155,7 @@ "timeslider.month.october": "oktobra", "timeslider.month.november": "novembra", "timeslider.month.december": "decembra", - "timeslider.unnamedauthors": "{{num}} {[plural(num) one: neimenovan avtor, plural(num) two: neimenovana avtorja, plural(num) few: neimenovani avtorji, other: neimenovanih avtorjev ]}", + "timeslider.unnamedauthors": "{{num}} {[plural(num) one: neimenovan avtor, two: neimenovana avtorja, few: neimenovani avtorji, other: neimenovanih avtorjev ]}", "pad.savedrevs.marked": "Ta redakcija je zdaj označena kot shranjena redakcija", "pad.savedrevs.timeslider": "Shranjene redakcije si lahko ogledate z odprtjem časovnega traku", "pad.userlist.entername": "Vnesite svoje ime", diff --git a/src/locales/sq.json b/src/locales/sq.json index 988c88b3446..1c0f1dad23c 100644 --- a/src/locales/sq.json +++ b/src/locales/sq.json @@ -152,7 +152,7 @@ "timeslider.month.october": "Tetor", "timeslider.month.november": "Nëntor", "timeslider.month.december": "Dhjetor", - "timeslider.unnamedauthors": "{{num}} i paemër {[plural(num) një: autor, tjetër:{{num}} autorë ]}", + "timeslider.unnamedauthors": "{{num}} i paemër {[plural(num) one: autor, other: autorë ]}", "pad.savedrevs.marked": "Ky rishikim tani është shënuar si rishikim i ruajtur", "pad.savedrevs.timeslider": "Rishikimet e ruajtura mund t’i shihni duke vizituar rrjedhën kohore", "pad.userlist.entername": "Jepni emrin tuaj", diff --git a/src/locales/sw.json b/src/locales/sw.json index 425c28cd1f7..e488cf5e99d 100644 --- a/src/locales/sw.json +++ b/src/locales/sw.json @@ -151,7 +151,7 @@ "timeslider.month.october": "Oktoba", "timeslider.month.november": "Novemba", "timeslider.month.december": "Desemba", - "timeslider.unnamedauthors": "{{num}} haijatajwa jina {[wingi (num) moja: mwandishi, mwingine: waandishi]}", + "timeslider.unnamedauthors": "{{num}} haijatajwa jina {[plural(num) one: mwandishi, other: waandishi ]}", "pad.savedrevs.marked": "Marekebisho haya sasa yamewekwa alama kama marekebisho yaliyohifadhiwa", "pad.savedrevs.timeslider": "Unaweza kuona marekebisho yaliyohifadhiwa kwa kutembelea mpangilio wa nyakati", "pad.userlist.entername": "Ingiza jina lako", diff --git a/src/locales/vi.json b/src/locales/vi.json index 95f52079c5d..9a517cffe0f 100644 --- a/src/locales/vi.json +++ b/src/locales/vi.json @@ -101,7 +101,7 @@ "timeslider.month.october": "Tháng Mười", "timeslider.month.november": "Tháng Mười Một", "timeslider.month.december": "Tháng Mười Hai", - "timeslider.unnamedauthors": "Không tên {{in a}} {[plural(num) một: tác giả, khác: tác giả]}", + "timeslider.unnamedauthors": "Không tên {{in a}} {[plural(num) one: tác giả, other: tác giả ]}", "pad.savedrevs.marked": "Phiên bản này đã được đánh dấu là một phiên bản đã lưu", "pad.userlist.entername": "Nhập tên của bạn", "pad.userlist.unnamed": "Không tên", diff --git a/src/tests/backend-new/specs/locale-files.test.ts b/src/tests/backend-new/specs/locale-files.test.ts new file mode 100644 index 00000000000..6cf0d73635a --- /dev/null +++ b/src/tests/backend-new/specs/locale-files.test.ts @@ -0,0 +1,101 @@ +'use strict'; + +import {readFileSync, readdirSync} from 'fs'; +import {join} from 'path'; +import {describe, it, expect} from 'vitest'; + +const repoRoot = join(__dirname, '..', '..', '..', '..'); +const localesDir = join(repoRoot, 'src', 'locales'); +const localeFiles = readdirSync(localesDir) + .filter((f) => f.endsWith('.json')) + .map((f) => join(localesDir, f)); + +const macroCandidateRe = /\{\[[^]*?]}/g; + +const parseMacro = (candidate: string): {macroName: string, options: string} | null => { + if (!candidate.startsWith('{[') || !candidate.endsWith(']}')) return null; + const body = candidate.slice(2, -2).trim(); + const openParen = body.indexOf('('); + const closeParen = body.indexOf(')', openParen + 1); + if (openParen <= 0 || closeParen <= openParen + 1) return null; + const macroName = body.slice(0, openParen).trim(); + const variableName = body.slice(openParen + 1, closeParen).trim(); + if (!/^[a-zA-Z]+$/.test(macroName) || !/^[a-zA-Z]+$/.test(variableName)) return null; + return {macroName, options: body.slice(closeParen + 1).trim()}; +}; + +const parsePluralOptionNames = (options: string): string[] | null => { + if (options === '') return []; + const names: string[] = []; + let i = 0; + while (i < options.length) { + while (i < options.length && /\s/.test(options[i])) i++; + const nameStart = i; + while (i < options.length && /[a-zA-Z]/.test(options[i])) i++; + if (nameStart === i || options[i] !== ':') return null; + names.push(options.slice(nameStart, i)); + i++; + while (i < options.length) { + if (options[i] === ',') { + i++; + break; + } + i++; + } + } + return names; +}; +const allowedMacros = new Set(['plural']); +const allowedPluralOptions = new Set(['zero', 'one', 'two', 'few', 'many', 'other']); + +describe('locale files', () => { + it('current locale corpus contains no unsupported macros', () => { + const failures: string[] = []; + for (const file of localeFiles) { + const rel = file.replace(repoRoot + '/', ''); + const parsed = JSON.parse(readFileSync(file, 'utf8')) as Record; + for (const [key, value] of Object.entries(parsed)) { + if (key === '@metadata') continue; + for (const candidate of value.match(macroCandidateRe) ?? []) { + const macro = parseMacro(candidate); + if (macro == null) { + failures.push(`${rel}:${key}:malformed:${candidate}`); + continue; + } + const {macroName, options} = macro; + if (!allowedMacros.has(macroName)) failures.push(`${rel}:${key}:${macroName}`); + if (macroName !== 'plural') continue; + const optionNames = parsePluralOptionNames(options); + if (optionNames == null) { + failures.push(`${rel}:${key}:malformed:${candidate}`); + continue; + } + for (const name of optionNames) { + if (!allowedPluralOptions.has(name)) failures.push(`${rel}:${key}:${name}`); + } + } + } + } + expect(failures).toEqual([]); + }); + + for (const file of localeFiles) { + const rel = file.replace(repoRoot + '/', ''); + + it(`${rel} contains a JSON object of string translations`, () => { + const parsed = JSON.parse(readFileSync(file, 'utf8')); + expect(parsed, `${rel} must contain a JSON object`).toBeTruthy(); + expect(Array.isArray(parsed), `${rel} must not contain a JSON array`).toBe(false); + expect(typeof parsed, `${rel} must contain a JSON object`).toBe('object'); + for (const [key, value] of Object.entries(parsed as Record)) { + if (key === '@metadata') { + expect(value != null && typeof value === 'object' && !Array.isArray(value), + `${rel} key @metadata must map to an object`).toBe(true); + continue; + } + expect(typeof value, `${rel} key ${key} must map to a string`).toBe('string'); + } + }); + + } +});