|
|
@@ -30,6 +30,17 @@ const appLocales = [
|
|
|
"sv",
|
|
|
] as const
|
|
|
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 = [
|
|
|
{
|
|
|
@@ -53,18 +64,23 @@ const domains = [
|
|
|
] as const
|
|
|
|
|
|
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) {
|
|
|
const source = await dictionary(domain.source)
|
|
|
for (const locale of domain.locales) {
|
|
|
const target = await dictionary(domain.target(locale))
|
|
|
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({
|
|
|
domain: domain.name,
|
|
|
locale,
|
|
|
missing: [],
|
|
|
- extra: [],
|
|
|
+ extra: expected,
|
|
|
})
|
|
|
}
|
|
|
}
|
|
|
@@ -78,7 +94,17 @@ describe.skipIf(!!process.env.CI)("i18n parity", () => {
|
|
|
const mismatched = Object.keys(source).filter(
|
|
|
(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) {
|
|
|
const module: unknown = await import(file)
|
|
|
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) {
|
|
|
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))
|
|
|
+}
|