diff --git a/app/modules/i18n/translations.test.ts b/app/modules/i18n/translations.test.ts new file mode 100644 index 000000000..1c77f6711 --- /dev/null +++ b/app/modules/i18n/translations.test.ts @@ -0,0 +1,56 @@ +import fs from "node:fs"; +import path from "node:path"; +import { describe, expect, test } from "vitest"; +import { DEFAULT_LANGUAGE, languages } from "./config"; + +const LOCALES_DIR = path.join(process.cwd(), "locales"); + +const namespaceFiles = fs + .readdirSync(path.join(LOCALES_DIR, DEFAULT_LANGUAGE)) + .filter((file) => file.endsWith(".json")); + +const readNamespace = (language: string, file: string) => { + const filePath = path.join(LOCALES_DIR, language, file); + if (!fs.existsSync(filePath)) return null; + + return JSON.parse(fs.readFileSync(filePath, "utf8")) as Record< + string, + string + >; +}; + +const interpolatedVariables = (value: string) => + Array.from(value.matchAll(/\{\{\s*([a-zA-Z0-9_.]+)\s*(?:,[^}]*)?\}\}/g)) + .map((match) => match[1]) + .sort(); + +describe("translations", () => { + test.each(languages.filter(({ code }) => code !== DEFAULT_LANGUAGE))( + "$code interpolates the same variables as English", + ({ code }) => { + const mismatches: string[] = []; + + for (const file of namespaceFiles) { + const english = readNamespace(DEFAULT_LANGUAGE, file)!; + const translated = readNamespace(code, file); + if (!translated) continue; + + for (const [key, englishValue] of Object.entries(english)) { + const translatedValue = translated[key]; + if (!translatedValue) continue; + + const expected = interpolatedVariables(englishValue).join(","); + const actual = interpolatedVariables(translatedValue).join(","); + + if (expected !== actual) { + mismatches.push( + `${file} ${key}: English has [${expected}], ${code} has [${actual}]`, + ); + } + } + } + + expect(mismatches).toEqual([]); + }, + ); +}); diff --git a/locales/da/forms.json b/locales/da/forms.json index 8cceae3ab..42b0e70af 100644 --- a/locales/da/forms.json +++ b/locales/da/forms.json @@ -264,11 +264,11 @@ "labels.profileCommissionText": "info om bestilling", "labels.profileNewProfileEnabled": "", "bottomTexts.profileCustomAvatar": "", - "bottomTexts.profileCustomName": "Hvis feltet ikke udfyldes bruges dit discordbrugernavn: \"{{discordName}}\"", + "bottomTexts.profileCustomName": "Hvis feltet ikke udfyldes bruges dit discordbrugernavn", "bottomTexts.profileCustomUrl": "", "bottomTexts.profileInGameName": "", "bottomTexts.profileBattlefy": "Battlefy-brugernavn bruges til seeding og bekræftelse i nogle turneringer", - "bottomTexts.profileShowDiscordUniqueName": "Vil du gøre dit unikke Discord-brugernavn ({{discordUniqueName}}) synligt for offentligheden?", + "bottomTexts.profileShowDiscordUniqueName": "Vil du gøre dit unikke Discord-brugernavn synligt for offentligheden?", "bottomTexts.profileCommissionsOpen": "", "bottomTexts.profileCommissionText": "Pris, åbne pladser eller andre relevante informationer der er relateret til at afgive en bestilling til dig.", "bottomTexts.profileNewProfileEnabled": "", diff --git a/locales/es-ES/forms.json b/locales/es-ES/forms.json index 4bd06aa6a..a6075eb1e 100644 --- a/locales/es-ES/forms.json +++ b/locales/es-ES/forms.json @@ -268,7 +268,7 @@ "bottomTexts.profileCustomUrl": "Para los mecenas (Supporter y superior) hay un enlace corto disponible. Ej. en lugar de sendou.ink/u/sendou, se puede usar snd.ink/sendou.", "bottomTexts.profileInGameName": "Formato: Nombre#disc (ej. Jugador#1234)", "bottomTexts.profileBattlefy": "Se usa para el seed y la verificación en algunos torneos", - "bottomTexts.profileShowDiscordUniqueName": "¿Mostrar tu nombre de Discord ({{discordUniqueName}}) públicamente?", + "bottomTexts.profileShowDiscordUniqueName": "¿Mostrar tu nombre de Discord públicamente?", "bottomTexts.profileCommissionsOpen": "Las comisiones se cierran automáticamente después de un mes", "bottomTexts.profileCommissionText": "Precio, espacios abiertos, o cualquier otra información sobre tus comisiones.", "bottomTexts.profileNewProfileEnabled": "Habilitar la nueva página de perfil basada en widgets (solo para supporter)", diff --git a/locales/es-US/forms.json b/locales/es-US/forms.json index 633c5fdc3..bece92ed9 100644 --- a/locales/es-US/forms.json +++ b/locales/es-US/forms.json @@ -264,11 +264,11 @@ "labels.profileCommissionText": "Info de comisiones", "labels.profileNewProfileEnabled": "Nueva página de perfil", "bottomTexts.profileCustomAvatar": "Los mecenas (Supporter y superior) pueden subir una imagen para usarla en lugar del avatar de Discord", - "bottomTexts.profileCustomName": "Si vacío, se mostrará tu nombre de Discord: \"{{discordName}}\"", + "bottomTexts.profileCustomName": "Si vacío, se mostrará tu nombre de Discord", "bottomTexts.profileCustomUrl": "Para los mecenas (Supporter y superior) hay un enlace corto disponible. Ej. en lugar de sendou.ink/u/sendou, se puede usar snd.ink/sendou.", "bottomTexts.profileInGameName": "Formato: Nombre#disc (ej. Jugador#1234)", "bottomTexts.profileBattlefy": "El nombre de tu cuenta de Battlefy se utiliza para la clasificación y verificación en algunos torneos.", - "bottomTexts.profileShowDiscordUniqueName": "¿Mostrar tu nombre de Discord ({{discordUniqueName}}) públicamente?", + "bottomTexts.profileShowDiscordUniqueName": "¿Mostrar tu nombre de Discord públicamente?", "bottomTexts.profileCommissionsOpen": "Las comisiones se cierran automáticamente después de un mes", "bottomTexts.profileCommissionText": "Precio, espacios abiertos, o cualquier otra información sobre tus comisiones.", "bottomTexts.profileNewProfileEnabled": "Habilitar la nueva página de perfil basada en widgets (solo para supporter)", diff --git a/locales/fr-CA/forms.json b/locales/fr-CA/forms.json index 5905bfc18..5d0a295ac 100644 --- a/locales/fr-CA/forms.json +++ b/locales/fr-CA/forms.json @@ -268,7 +268,7 @@ "bottomTexts.profileCustomUrl": "", "bottomTexts.profileInGameName": "", "bottomTexts.profileBattlefy": "", - "bottomTexts.profileShowDiscordUniqueName": "Show your unique Discord name ({{discordUniqueName}}) publicly?", + "bottomTexts.profileShowDiscordUniqueName": "", "bottomTexts.profileCommissionsOpen": "", "bottomTexts.profileCommissionText": "Prix, disponibilités et tout autres info nécéssaires", "bottomTexts.profileNewProfileEnabled": "", diff --git a/locales/fr-EU/forms.json b/locales/fr-EU/forms.json index 13d9ea32e..31f8600a1 100644 --- a/locales/fr-EU/forms.json +++ b/locales/fr-EU/forms.json @@ -264,11 +264,11 @@ "labels.profileCommissionText": "Info pour les commissions", "labels.profileNewProfileEnabled": "", "bottomTexts.profileCustomAvatar": "", - "bottomTexts.profileCustomName": "Si il n'est pas présent, votre pseudo discord est utilisé: \"{{discordName}}\"", + "bottomTexts.profileCustomName": "Si il n'est pas présent, votre pseudo discord est utilisé", "bottomTexts.profileCustomUrl": "Pour les Supporter patrons (& plus), les liens courts sont disponibles. Exemple: Au mieux de sendou.ink/u/sendou, snd.ink/sendou peut être utilisé.", "bottomTexts.profileInGameName": "", "bottomTexts.profileBattlefy": "Votre nom Battlefy est utiliser pour le seeding et la verification de certains tournois", - "bottomTexts.profileShowDiscordUniqueName": "Show your unique Discord name ({{discordUniqueName}}) publicly?", + "bottomTexts.profileShowDiscordUniqueName": "", "bottomTexts.profileCommissionsOpen": "", "bottomTexts.profileCommissionText": "Prix, disponibilités et tout autres info nécéssaires", "bottomTexts.profileNewProfileEnabled": "", diff --git a/locales/he/forms.json b/locales/he/forms.json index 216bcd878..aaaecc428 100644 --- a/locales/he/forms.json +++ b/locales/he/forms.json @@ -268,7 +268,7 @@ "bottomTexts.profileCustomUrl": "", "bottomTexts.profileInGameName": "", "bottomTexts.profileBattlefy": "", - "bottomTexts.profileShowDiscordUniqueName": "להראות את שם ה-Discord היחודי שלכם ({{discordUniqueName}}) בפומבי?", + "bottomTexts.profileShowDiscordUniqueName": "להראות את שם ה-Discord היחודי שלכם בפומבי?", "bottomTexts.profileCommissionsOpen": "", "bottomTexts.profileCommissionText": "מחיר, כמות בקשות או מידע אחר שקשור לבקשות אלכם", "bottomTexts.profileNewProfileEnabled": "", diff --git a/locales/it/forms.json b/locales/it/forms.json index 9fd023eec..6268869f2 100644 --- a/locales/it/forms.json +++ b/locales/it/forms.json @@ -264,11 +264,11 @@ "labels.profileCommissionText": "Info sulle commissioni", "labels.profileNewProfileEnabled": "", "bottomTexts.profileCustomAvatar": "", - "bottomTexts.profileCustomName": "Se mancante, viene usato il tuo nome visualizzato Discord: \"{{discordName}}\"", + "bottomTexts.profileCustomName": "Se mancante, viene usato il tuo nome visualizzato Discord", "bottomTexts.profileCustomUrl": "Per gli iscritti al Patreon (Supporter compreso in su) è disponibile il link corto. Es. invece di sendou.ink/u/sendou, può essere usato snd.ink/sendou.", "bottomTexts.profileInGameName": "", "bottomTexts.profileBattlefy": "Il nome dell'account Battlefy è usato per il seeding e verifica in alcuni tornei", - "bottomTexts.profileShowDiscordUniqueName": "Mostrare il proprio nome unico Discord ({{discordUniqueName}}) pubblicamente?", + "bottomTexts.profileShowDiscordUniqueName": "Mostrare il proprio nome unico Discord pubblicamente?", "bottomTexts.profileCommissionsOpen": "", "bottomTexts.profileCommissionText": "Prezzo, posti liberi o altre info relative al commissionarti", "bottomTexts.profileNewProfileEnabled": "", diff --git a/locales/ja/forms.json b/locales/ja/forms.json index a8f3be794..abf0e5e97 100644 --- a/locales/ja/forms.json +++ b/locales/ja/forms.json @@ -264,11 +264,11 @@ "labels.profileCommissionText": "依頼に関する情報", "labels.profileNewProfileEnabled": "", "bottomTexts.profileCustomAvatar": "", - "bottomTexts.profileCustomName": "記入されてない場合ディスコードの表示名 \"{{discordName}}\"を使います", + "bottomTexts.profileCustomName": "記入されてない場合ディスコードの表示名を使います", "bottomTexts.profileCustomUrl": "", "bottomTexts.profileInGameName": "", "bottomTexts.profileBattlefy": "Battlefyのアカウント名は特定のトーナメンでシーディング及びにプレイヤー情報の確認に使用されます。", - "bottomTexts.profileShowDiscordUniqueName": "Discord のユニーク名 ({{discordUniqueName}}) 公表しますか?", + "bottomTexts.profileShowDiscordUniqueName": "Discord のユニーク名を公表しますか?", "bottomTexts.profileCommissionsOpen": "", "bottomTexts.profileCommissionText": "価格、受付数、その他依頼に関する情報", "bottomTexts.profileNewProfileEnabled": "", diff --git a/locales/pt-BR/forms.json b/locales/pt-BR/forms.json index 98d6edf90..1876831ba 100644 --- a/locales/pt-BR/forms.json +++ b/locales/pt-BR/forms.json @@ -268,7 +268,7 @@ "bottomTexts.profileCustomUrl": "", "bottomTexts.profileInGameName": "", "bottomTexts.profileBattlefy": "", - "bottomTexts.profileShowDiscordUniqueName": "Deixe ativado para mostrar seu nome de usuário único do Discord ({{discordUniqueName}}) publicamente.", + "bottomTexts.profileShowDiscordUniqueName": "Deixe ativado para mostrar seu nome de usuário único do Discord publicamente.", "bottomTexts.profileCommissionsOpen": "", "bottomTexts.profileCommissionText": "Preço, vagas abertas ou qualquer outra informação relacionada ao processo de fazer um pedido para você.", "bottomTexts.profileNewProfileEnabled": "", diff --git a/locales/ru/forms.json b/locales/ru/forms.json index a059b5e5c..a559f0fb0 100644 --- a/locales/ru/forms.json +++ b/locales/ru/forms.json @@ -264,11 +264,11 @@ "labels.profileCommissionText": "Информация о коммишенах", "labels.profileNewProfileEnabled": "", "bottomTexts.profileCustomAvatar": "", - "bottomTexts.profileCustomName": "Если пользовательское имя отсутствует, то будет использовано ваше имя в Discord: \"{{discordName}}\"", + "bottomTexts.profileCustomName": "Если пользовательское имя отсутствует, то будет использовано ваше имя в Discord", "bottomTexts.profileCustomUrl": "Для меценатов (Supporter и выше) доступна короткая ссылка. Например, вместо sendou.ink/u/sendou может быть использована сссылка snd.ink/sendou.", "bottomTexts.profileInGameName": "", "bottomTexts.profileBattlefy": "Имя на Battlefy может быть использовано для посева и верификации в некоторых турнирах", - "bottomTexts.profileShowDiscordUniqueName": "Показывать ваше уникальное Discord имя ({{discordUniqueName}})?", + "bottomTexts.profileShowDiscordUniqueName": "Показывать ваше уникальное Discord имя?", "bottomTexts.profileCommissionsOpen": "", "bottomTexts.profileCommissionText": "Цена, слоты и другая информация о ваших коммишенах", "bottomTexts.profileNewProfileEnabled": "",