mirror of https://github.com/halo-dev/halo
241 lines
6.8 KiB
JavaScript
241 lines
6.8 KiB
JavaScript
/**
|
|
* Apply Missing Translations
|
|
* -------------------------
|
|
* This script applies translated entries from "_missing_translations_[lang].yaml" files
|
|
* to their corresponding language files.
|
|
*
|
|
* For each missing translations file, it:
|
|
* 1. Compares entries with the English base file
|
|
* 2. Identifies which entries have been translated (values different from English)
|
|
* 3. Merges only the translated entries into the main language file
|
|
* 4. Updates the missing translations file to keep only untranslated entries
|
|
*
|
|
* Usage:
|
|
* node scripts/apply_missing_translations.mjs
|
|
*
|
|
* Example output:
|
|
* Processing: src/locales/_missing_translations_zh-TW.yaml for language: zh-TW
|
|
* Updated src/locales/zh-TW.yaml with 15 translated entries.
|
|
* Updated src/locales/_missing_translations_zh-TW.yaml with 10 remaining untranslated entries.
|
|
*
|
|
* This script is designed to be run repeatedly as you translate more entries in the
|
|
* missing translations files. It will only apply entries that differ from the English version.
|
|
*/
|
|
|
|
import { existsSync } from "fs";
|
|
import fs from "fs/promises";
|
|
import yaml from "js-yaml";
|
|
import path from "path";
|
|
import { fileURLToPath } from "url";
|
|
|
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
const translationsDirPath = path.resolve(__dirname, "../src/locales");
|
|
const baseFile = path.join(translationsDirPath, "en.yaml");
|
|
|
|
const VERBOSE = true;
|
|
|
|
async function main() {
|
|
try {
|
|
const dirEntries = await fs.readdir(translationsDirPath, {
|
|
withFileTypes: true,
|
|
});
|
|
|
|
const missingFiles = dirEntries
|
|
.filter(
|
|
(entry) =>
|
|
entry.isFile() &&
|
|
entry.name.includes("_missing_translations_") &&
|
|
entry.name.endsWith(".yaml")
|
|
)
|
|
.map((entry) => path.join(translationsDirPath, entry.name));
|
|
|
|
if (missingFiles.length === 0) {
|
|
console.log("No missing translation files found.");
|
|
return;
|
|
}
|
|
|
|
const enTranslations = await loadYamlFile(baseFile);
|
|
|
|
for (const missingFile of missingFiles) {
|
|
const fileName = path.basename(missingFile, ".yaml");
|
|
const langCode = fileName.replace("_missing_translations_", "");
|
|
const targetFile = path.join(translationsDirPath, `${langCode}.yaml`);
|
|
|
|
console.log(`\nProcessing: ${missingFile} for language: ${langCode}`);
|
|
|
|
if (!existsSync(targetFile)) {
|
|
console.log(`Target translation file ${targetFile} does not exist`);
|
|
continue;
|
|
}
|
|
|
|
try {
|
|
const missingTranslations = await loadYamlFile(missingFile);
|
|
const currentTranslations = await loadYamlFile(targetFile);
|
|
|
|
const translatedEntries = {};
|
|
const untranslatedEntries = {};
|
|
const stats = { added: 0, skipped: 0 };
|
|
|
|
const keyPaths = collectKeyPaths(missingTranslations);
|
|
console.log(
|
|
`Found ${keyPaths.length} keys in missing translations file.`
|
|
);
|
|
|
|
for (const keyPath of keyPaths) {
|
|
const missingValue = getValueByPath(missingTranslations, keyPath);
|
|
const enValue = getValueByPath(enTranslations, keyPath);
|
|
|
|
if (
|
|
missingValue !== enValue &&
|
|
missingValue !== null &&
|
|
missingValue !== undefined
|
|
) {
|
|
setValueByPath(translatedEntries, keyPath, missingValue);
|
|
stats.added++;
|
|
if (VERBOSE) {
|
|
console.log(
|
|
`✓ TRANSLATED: ${keyPath.join(
|
|
"."
|
|
)} = "${missingValue}" (EN: "${enValue}")`
|
|
);
|
|
}
|
|
} else {
|
|
setValueByPath(untranslatedEntries, keyPath, missingValue);
|
|
stats.skipped++;
|
|
if (VERBOSE) {
|
|
console.log(
|
|
`✗ NOT TRANSLATED: ${keyPath.join(
|
|
"."
|
|
)} = "${missingValue}" (same as EN: "${enValue}")`
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (stats.added > 0) {
|
|
const updatedTranslations = deepMerge(
|
|
currentTranslations,
|
|
translatedEntries
|
|
);
|
|
|
|
await saveYamlFile(updatedTranslations, targetFile);
|
|
console.log(
|
|
`Updated ${targetFile} with ${stats.added} translated entries.`
|
|
);
|
|
|
|
await saveYamlFile(untranslatedEntries, missingFile);
|
|
console.log(
|
|
`Updated ${missingFile} with ${stats.skipped} remaining untranslated entries.`
|
|
);
|
|
} else {
|
|
console.log(
|
|
`No translated entries found in ${missingFile}, files not updated.`
|
|
);
|
|
}
|
|
|
|
console.log(`\nSummary for ${langCode}:`);
|
|
console.log(`- Added: ${stats.added} translated entries`);
|
|
console.log(`- Remaining: ${stats.skipped} untranslated entries`);
|
|
} catch (e) {
|
|
console.error(`Error processing ${missingFile}:`, e);
|
|
}
|
|
}
|
|
} catch (e) {
|
|
console.error(`Error:`, e);
|
|
}
|
|
}
|
|
|
|
async function loadYamlFile(filePath) {
|
|
try {
|
|
const content = await fs.readFile(filePath, "utf8");
|
|
return yaml.load(content) || {};
|
|
} catch (error) {
|
|
console.error(`Error loading file ${filePath}:`, error);
|
|
return {};
|
|
}
|
|
}
|
|
|
|
async function saveYamlFile(data, filePath) {
|
|
try {
|
|
const yamlContent = yaml.dump(data, {
|
|
indent: 2,
|
|
lineWidth: -1,
|
|
});
|
|
await fs.writeFile(filePath, yamlContent, "utf8");
|
|
return true;
|
|
} catch (error) {
|
|
console.error(`Error saving file ${filePath}:`, error);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function collectKeyPaths(obj, currentPath = [], result = []) {
|
|
if (obj === null || typeof obj !== "object") {
|
|
return result;
|
|
}
|
|
|
|
for (const key of Object.keys(obj)) {
|
|
const newPath = [...currentPath, key];
|
|
|
|
if (obj[key] === null || typeof obj[key] !== "object") {
|
|
result.push(newPath);
|
|
} else {
|
|
collectKeyPaths(obj[key], newPath, result);
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
function getValueByPath(obj, path) {
|
|
let current = obj;
|
|
|
|
for (const key of path) {
|
|
if (
|
|
current === null ||
|
|
current === undefined ||
|
|
typeof current !== "object"
|
|
) {
|
|
return undefined;
|
|
}
|
|
current = current[key];
|
|
}
|
|
|
|
return current;
|
|
}
|
|
|
|
function setValueByPath(obj, path, value) {
|
|
let current = obj;
|
|
|
|
for (let i = 0; i < path.length - 1; i++) {
|
|
const key = path[i];
|
|
|
|
if (!current[key] || typeof current[key] !== "object") {
|
|
current[key] = {};
|
|
}
|
|
|
|
current = current[key];
|
|
}
|
|
|
|
const lastKey = path[path.length - 1];
|
|
current[lastKey] = value;
|
|
}
|
|
|
|
function deepMerge(target, source) {
|
|
const result = { ...target };
|
|
|
|
for (const key of Object.keys(source)) {
|
|
if (source[key] === null || typeof source[key] !== "object") {
|
|
result[key] = source[key];
|
|
} else if (target[key] === null || typeof target[key] !== "object") {
|
|
result[key] = { ...source[key] };
|
|
} else {
|
|
result[key] = deepMerge(target[key], source[key]);
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
main();
|