diff --git a/extra/check_translations.py b/extra/check_translations.py deleted file mode 100644 index 920234d5c..000000000 --- a/extra/check_translations.py +++ /dev/null @@ -1,87 +0,0 @@ -import json -import os -import re - - -def find_missing_translations(): - # Load the English translation file - with open("src/lang/en.json", "r", encoding="utf-8") as f: - en_translations = json.load(f) - - # Regex to find i18n keys in Vue files - translation_regex = re.compile( - # Matches $t('key'), $t("key"), $t('key', [ ... ]) - r"""\$t\((['"])(?P.*?)\1\s*[,)]""" - + "|" - + - # Matches i18n-t keypath="key" - r"""i18n-t\s+keypath="(?P[^"]+)" """, - re.VERBOSE, - ) - - missing_keys = [] - - # Walk through the src directory - for root, _, files in os.walk("src"): - for file in files: - if file.endswith((".vue", ".js")): - file_path = os.path.join(root, file) - try: - with open(file_path, "r", encoding="utf-8") as f: - lines = f.readlines() - for line_num, line in enumerate(lines, 1): - for match in translation_regex.finditer(line): - key = ( - match.group("key1") - or match.group("key2") - or match.group("key3") - ) - if key and key not in en_translations: - # Find start and end of the key itself - for group_name in ["key1", "key2", "key3"]: - if match.group(group_name): - start, end = match.span(group_name) - break - missing_keys.append( - ( - file_path, - line_num, - key, - line.rstrip(), - start, - end, - ) - ) - - except UnicodeDecodeError: - print(f"Skipping file due to UnicodeDecodeError: {file_path}") - - # Print the report - if not missing_keys: - print("No missing translation keys found.") - else: - for file_path, line_num, key, line_content, start, end in missing_keys: - print(f"\nerror: Missing translation key: '{key}'") - print(f" --> {file_path}:{line_num}:{start}") - print(" |") - print(f"{line_num:<5}| {line_content}") - arrow = " " * (start - 1) + "^" * (end - start + 2) - print(f" | {arrow} unrecognized translation key") - print(" |") - print( - f" = note: please register the translation key '{key}' in en.json so that our awesome team of translators can translate them" - ) - print( - " = tip: if you want to contribute translations, please visit our https://weblate.kuma.pet" - ) - print("") - - print("===============================") - file_count = len(set([item[0] for item in missing_keys])) - print( - f"Found a total of {len(missing_keys)} missing keys in {file_count} files." - ) - - -if __name__ == "__main__": - find_missing_translations() diff --git a/test/backend-test/check-translations.test.js b/test/backend-test/check-translations.test.js new file mode 100644 index 000000000..23d3247a3 --- /dev/null +++ b/test/backend-test/check-translations.test.js @@ -0,0 +1,76 @@ +const { describe, it } = require("node:test"); +const assert = require("node:assert"); +const fs = require("fs"); +const path = require("path"); + +function* walk(dir) { + const files = fs.readdirSync(dir, { withFileTypes: true }); + for (const file of files) { + if (file.isDirectory()) { + yield* walk(path.join(dir, file.name)); + } else { + yield path.join(dir, file.name); + } + } +} + +describe("Check Translations", () => { + it("should not have missing translation keys", () => { + const enTranslations = JSON.parse(fs.readFileSync("src/lang/en.json", "utf-8")); + + const translationRegex = /\$t\(['"](?.*?)['"]\s*[,)]|i18n-t\s+keypath=\"(?[^\"]+)\"/g; + + const missingKeys = []; + + for (const filePath of walk("src")) { + if (filePath.endsWith(".vue") || filePath.endsWith(".js")) { + try { + const lines = fs.readFileSync(filePath, "utf-8").split("\n"); + lines.forEach((line, lineNum) => { + let match; + while ((match = translationRegex.exec(line)) !== null) { + const key = match.groups.key1 || match.groups.key2; + if (key && !enTranslations[key]) { + const start = match.index; + const end = start + key.length; + missingKeys.push({ + filePath, + lineNum: lineNum + 1, + key, + line: line.trim(), + start, + end, + }); + } + } + }); + } catch (e) { + if (e instanceof TypeError && e.message.includes("is not a function")) { + // Ignore errors from binary files + } else { + console.error(`Error processing file: ${filePath}`, e); + } + } + } + } + + if (missingKeys.length > 0) { + let report = "Missing translation keys found:\n"; + missingKeys.forEach(({ filePath, lineNum, key, line, start, end }) => { + report += `\nerror: Missing translation key: '${key}'`; + report += `\n --> ${filePath}:${lineNum}:${start}`; + report += "\n |"; + report += `\n${String(lineNum).padEnd(5)}| ${line}`; + const arrow = " ".repeat(start) + "^".repeat(end - start); + report += `\n | ${arrow} unrecognized translation key`; + report += "\n |"; + report += `\n = note: please register the translation key '${key}' in en.json so that our awesome team of translators can translate them`; + report += "\n = tip: if you want to contribute translations, please visit our https://weblate.kuma.pet\n"; + }); + report += `\n===============================`; + const fileCount = new Set(missingKeys.map(item => item.filePath)).size; + report += `\nFound a total of ${missingKeys.length} missing keys in ${fileCount} files.`; + assert.fail(report); + } + }); +});