chore: add check locales keys

main
jialin 11 months ago
parent 2079fc1801
commit c0d92fbdd5

@ -3,6 +3,7 @@
"author": "jialin",
"scripts": {
"build": "max build",
"check:locales": "npx tsx ./src/locales/check.ts",
"dev": "max dev",
"format": "prettier --cache --write .",
"postinstall": "max setup",

@ -35,3 +35,9 @@ To add a new language configuration, follow these steps:
- Review and ensure all translations are complete.
- If any translations are missing, they will default to `English`.
- Your new language configuration is now ready for use!
## 5. **Verify that all language keys are consistent.**
```bash
npm run check:locales
```

@ -12,6 +12,7 @@ const existingEntries = new Set(fs.readdirSync(localesDir));
const errors: string[] = [];
// ================ check if all locales are correctly named start ====================
Object.values(langConfigMap).forEach(({ lang }) => {
const expectedDir = lang;
const expectedFile = `${lang}.ts`;
@ -25,10 +26,107 @@ Object.values(langConfigMap).forEach(({ lang }) => {
}
});
// ================ check if all locales are correctly named end ====================
const baseLang = 'en-US'; // standard language
const fileExt = '.ts';
function parseLangFile(filePath: string): Record<string, any> {
if (!fs.existsSync(filePath)) return {};
const content = fs.readFileSync(filePath, 'utf-8').trim();
try {
// 1. extract the default export object
const match = content.match(/export\s+default\s+({[\s\S]*});?/);
if (match) {
let jsonString = match[1];
// 2. remove trailing commas
jsonString = jsonString.replace(/,\s*}/g, '}');
// 3. parse the object
return new Function(`return ${jsonString}`)();
}
} catch (error) {
console.error(`❌ Error parsing ${filePath}:`, error);
}
return {};
}
// get all modules in a language directory
function getLangFiles(lang: string): Record<string, Record<string, any>> {
const langPath = path.join(localesDir, lang);
if (!fs.existsSync(langPath)) return {};
return fs
.readdirSync(langPath)
.filter((file) => file.endsWith(fileExt))
.reduce(
(acc, file) => {
const filePath = path.join(langPath, file);
acc[file] = parseLangFile(filePath);
return acc;
},
{} as Record<string, Record<string, any>>
);
}
function compareKeys(
base: Record<string, any>,
target: Record<string, any>
): { missing: string[]; extra: string[] } {
const baseKeys = new Set(Object.keys(base));
const targetKeys = new Set(Object.keys(target));
return {
missing: [...baseKeys].filter((key) => !targetKeys.has(key)),
extra: [...targetKeys].filter((key) => !baseKeys.has(key))
};
}
// varify all locales base on en-US
function validateLocales() {
const baseFiles = getLangFiles(baseLang); // en-US as base
const languages = fs
.readdirSync(localesDir)
.filter(
(dir) =>
fs.statSync(path.join(localesDir, dir)).isDirectory() &&
dir !== baseLang
);
let hasErrors = false;
languages.forEach((lang) => {
console.log(`🔍 Checking ${lang}...`);
const targetFiles = getLangFiles(lang);
Object.entries(baseFiles).forEach(([fileName, baseData]) => {
const targetData = targetFiles[fileName] || {};
const { missing, extra } = compareKeys(baseData, targetData);
if (missing.length || extra.length) {
hasErrors = true;
console.log(`❌ [${lang}] ${fileName}:`);
if (missing.length)
console.log(` ===== Missing keys: ===== \n`, missing);
if (extra.length) console.log(' ===== Extra keys: ===== \n', extra);
}
});
});
return !hasErrors;
}
if (errors.length) {
console.error('❌ Errors found:');
errors.forEach(console.error);
process.exit(1);
} else if (!validateLocales()) {
process.exit(1);
} else {
console.log('✅ All locales are correctly named.');
console.log('✅ All keys are consistent!');
}

@ -123,3 +123,8 @@ export default {
'models.form.files': 'files',
'models.table.status': 'Status'
};
// ========== To-Do: Translate Keys (Remove After Translation) ==========
// 1. 'models.form.files',
// 2. 'models.table.status',
// ========== End of To-Do List ==========

@ -116,5 +116,12 @@ export default {
'models.form.backend.warning.llamabox':
'llama-boxバックエンドを使用するには、モデルファイルのフルパスを指定してください例:<span style="font-weight: 700">/data/models/model.gguf</span>)。分割モデルの場合、最初のシャードのパスを指定してください(例:<span style="font-weight: 700">/data/models/model-00001-of-00004.gguf</span>)。',
'models.form.keyvalue.paste':
'複数行のテキストを貼り付けます。各行にはキーと値のペアが含まれ、キーと値は=記号で区切られ、異なるキーと値のペアは改行文字で区切られます。'
'複数行のテキストを貼り付けます。各行にはキーと値のペアが含まれ、キーと値は=記号で区切られ、異なるキーと値のペアは改行文字で区切られます。',
'models.form.files': 'files',
'models.table.status': 'Status'
};
// ========== To-Do: Translate Keys (Remove After Translation) ==========
// 1. 'models.form.files',
// 2. 'models.table.status'
// ========== End of To-Do List ==========

@ -56,5 +56,34 @@ export default {
'ラベルを選択してコマンドを生成し、コピーを使用してコマンドをコピーします。',
'resources.worker.script.install': 'スクリプトインストール',
'resources.worker.container.install': 'コンテナインストールLinuxのみ',
'resources.worker.cann.tips': `<span style='color: #000;font-weight: 600'>ASCEND_VISIBLE_DEVICES</span> を必要なGPUインデックスに設定します。GPU0からGPU3の場合、<span style='color: #000;font-weight: 600'>ASCEND_VISIBLE_DEVICES=0,1,2,3</span> または <span style='color: #000;font-weight: 600'>ASCEND_VISIBLE_DEVICES=0-3</span> を使用します。`
'resources.worker.cann.tips': `<span style='color: #000;font-weight: 600'>ASCEND_VISIBLE_DEVICES</span> を必要なGPUインデックスに設定します。GPU0からGPU3の場合、<span style='color: #000;font-weight: 600'>ASCEND_VISIBLE_DEVICES=0,1,2,3</span> または <span style='color: #000;font-weight: 600'>ASCEND_VISIBLE_DEVICES=0-3</span> を使用します。`,
'resources.modelfiles.form.path': 'Storage path',
'resources.modelfiles.modelfile': 'Model Files',
'resources.modelfiles.download': 'Add Model File',
'resources.modelfiles.size': 'Size',
'resources.modelfiles.selecttarget': 'Select Target',
'resources.modelfiles.form.localdir': 'Local Directory',
'resources.modelfiles.form.localdir.tips':
'The default storage directory is <span class="desc-block">/var/lib/gpustack/cache</span> or the directory specified with <span class="desc-block">--data-dir</span>.',
'resources.modelfiles.retry.download': 'Retry Download',
'resources.modelfiles.storagePath.holder':
'Waiting for download to complete...',
'resources.filter.worker': 'Filter by Worker',
'resources.filter.source': 'Filter by Source',
'resources.modelfiles.delete.tips': 'Also delete the file from disk!'
};
// ========== To-Do: Translate Keys (Remove After Translation) ==========
// 1. 'resources.modelfiles.form.path',
// 2. 'resources.modelfiles.modelfile',
// 3. 'resources.modelfiles.download',
// 4. 'resources.modelfiles.size',
// 5. 'resources.modelfiles.selecttarget',
// 6. 'resources.modelfiles.form.localdir',
// 7. 'resources.modelfiles.form.localdir.tips',
// 8. 'resources.modelfiles.retry.download',
// 9. 'resources.modelfiles.storagePath.holder',
// 10. 'resources.filter.worker',
// 11. 'resources.filter.source',
// 12. 'resources.modelfiles.delete.tips'
// ========== End of To-Do List ==========

@ -226,5 +226,7 @@ export default {
'common.button.moreInfo': '更多信息',
'common.text.warning': '警告',
'common.text.error': '错误',
'common.text.tips': '提示'
'common.text.tips': '提示',
'settings.system': '系统设置',
'common.filter.byId': '按 ID 查询'
};

Loading…
Cancel
Save