homepage/scripts/check-translations.ts
2025-02-03 22:22:07 +08:00

183 lines
6.7 KiB
TypeScript
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import fs from 'fs-extra';
import path from 'path';
import * as globModule from 'glob';
const glob = globModule.glob;
import chalk from 'chalk';
interface TranslationMap {
[key: string]: string | TranslationMap;
}
interface TranslationResult {
missingKeys: string[];
unusedKeys: string[];
}
// 递归获取所有翻译键
function getAllTranslationKeys(obj: TranslationMap, prefix = ''): string[] {
return Object.entries(obj).reduce<string[]>((acc, [key, value]) => {
const currentKey = prefix ? `${prefix}.${key}` : key;
if (typeof value === 'object') {
return [...acc, ...getAllTranslationKeys(value, currentKey)];
}
// 如果键以 translation. 开头,移除这个前缀
const finalKey = currentKey.startsWith('translation.')
? currentKey.substring('translation.'.length)
: currentKey;
if (!shouldIgnoreKey(finalKey)) {
return [...acc, finalKey];
}
return acc;
}, []);
}
// 从源代码中提取翻译键的正则表达式
const translationKeyRegexes = [
/(?:t|i18n\.t)\(['"]([a-zA-Z][a-zA-Z0-9._-]+)['"](?:\s*,\s*{[^}]*})?\)/gi, // t('key') or t('key', {})
/(?:t|i18n\.t)\(\s*(?:[a-zA-Z][a-zA-Z0-9._]+\s*\?\s*)?['"]([a-zA-Z][a-zA-Z0-9._-]+)['"](?:\s*:\s*['"]([a-zA-Z][a-zA-Z0-9._-]+)['"])?(?:\s*,\s*{[^}]*})?\)/gi, // t(isOpen ? 'key1' : 'key2') or t('key')
/useTranslation\(['"]([a-zA-Z][a-zA-Z0-9._-]+)['"]\)/gi, // useTranslation('key')
/<Trans[^>]*i18nKey=["']([a-zA-Z][a-zA-Z0-9._-]+)["']/gi, // <Trans i18nKey="key" />
];
// 从文件内容中提取翻译键
function extractTranslationKeys(content: string): Set<string> {
const keys = new Set<string>();
for (const regex of translationKeyRegexes) {
let match;
while ((match = regex.exec(content)) !== null) {
// 如果有多个捕获组(三元运算符的情况),添加所有捕获的键
for (let i = 1; i < match.length; i++) {
const key = match[i];
if (key && !shouldIgnoreKey(key)) {
keys.add(key);
}
}
}
}
return keys;
}
// 忽略的翻译键
const ignoredKeys = new Set([
'content-type',
'page',
'tags'
]);
// 忽略的翻译键前缀
const ignoredPrefixes = [
'projects.status.',
'projects.tags.',
'social.links',
'updates.tags',
'data.'
];
// 检查键是否应该被忽略
function shouldIgnoreKey(key: string): boolean {
if (ignoredKeys.has(key)) return true;
return ignoredPrefixes.some(prefix => key.startsWith(prefix));
}
// 比较翻译键
function compareTranslations(usedKeys: Set<string>, availableKeys: Set<string>): TranslationResult {
const missingKeys = Array.from(usedKeys)
.filter(key => !availableKeys.has(key))
.sort();
const unusedKeys = Array.from(availableKeys)
.filter(key => !usedKeys.has(key))
.sort();
return { missingKeys, unusedKeys };
}
async function main() {
console.log(chalk.blue('Checking translations...'));
// 获取所有支持的语言
const dataDir = path.resolve('data');
const langs = fs.readdirSync(dataDir)
.filter(file => fs.statSync(path.join(dataDir, file)).isDirectory())
.filter(dir => /^[a-z]{2}-[A-Z]{2}$/.test(dir));
// 扫描源代码文件
const srcFiles = await glob('src/**/*.{ts,tsx,js,jsx}', { absolute: true });
const usedKeys = new Set<string>();
// 从所有源代码文件中提取翻译键
for (const file of srcFiles) {
const content = await fs.readFile(file, 'utf-8');
const keys = extractTranslationKeys(content);
keys.forEach(key => usedKeys.add(key));
}
// 检查每个语言的翻译
let hasWarnings = false;
for (const lang of langs) {
const indexPath = path.join(dataDir, lang, 'index.json');
if (fs.existsSync(indexPath)) {
const content = await fs.readJson(indexPath);
// 确保我们从 translations 对象开始
const translationsObj = content.translations || content;
const availableKeys = new Set(getAllTranslationKeys(translationsObj));
const { missingKeys, unusedKeys } = compareTranslations(usedKeys, availableKeys);
if (missingKeys.length > 0 || unusedKeys.length > 0) {
hasWarnings = true;
console.log(chalk.yellow(`\n[${lang}]`));
if (missingKeys.length > 0) {
console.log(chalk.yellow(` Missing translations: (${missingKeys.length} keys)`));
// 按命名空间分组输出
const groupedKeys = missingKeys.reduce((acc, key) => {
const namespace = key.split('.')[0];
if (!acc[namespace]) acc[namespace] = [];
acc[namespace].push(key);
return acc;
}, {} as Record<string, string[]>);
Object.entries(groupedKeys)
.sort(([a], [b]) => a.localeCompare(b))
.forEach(([namespace, keys]) => {
console.log(chalk.cyan(`\n ${namespace}:`));
keys.forEach(key => {
console.log(chalk.red(` ${key}`));
});
});
}
if (unusedKeys.length > 0) {
console.log(chalk.yellow(`\n Unused translations: (${unusedKeys.length} keys)`));
// 按命名空间分组输出
const groupedKeys = unusedKeys.reduce((acc, key) => {
const namespace = key.split('.')[0];
if (!acc[namespace]) acc[namespace] = [];
acc[namespace].push(key);
return acc;
}, {} as Record<string, string[]>);
Object.entries(groupedKeys)
.sort(([a], [b]) => a.localeCompare(b))
.forEach(([namespace, keys]) => {
console.log(chalk.cyan(`\n ${namespace}:`));
keys.forEach(key => {
console.log(chalk.yellow(` ${key}`));
});
});
}
}
}
}
if (!hasWarnings) {
console.log(chalk.green('\n✓ All translations are complete and clean!'));
} else {
console.log(chalk.yellow('\n⚠ Please review the translation issues listed above.'));
}
}
main().catch(error => {
console.error(chalk.red('Error:'), error);
process.exit(1);
});