Merged in feat/lokalise-rebuild (pull request #2993)
Feat/lokalise rebuild * chore(lokalise): update translation ids * chore(lokalise): easier to switch between projects * chore(lokalise): update translation ids * . * . * . * . * . * . * chore(lokalise): update translation ids * chore(lokalise): update translation ids * . * . * . * chore(lokalise): update translation ids * chore(lokalise): update translation ids * . * . * chore(lokalise): update translation ids * chore(lokalise): update translation ids * chore(lokalise): new translations * merge * switch to errors for missing id's * merge * sync translations Approved-by: Linus Flood
This commit is contained in:
335
scripts/i18n/updateIds/index.ts
Normal file
335
scripts/i18n/updateIds/index.ts
Normal file
@@ -0,0 +1,335 @@
|
||||
#!/usr/bin/env node
|
||||
import * as fs from "fs/promises";
|
||||
import * as path from "path";
|
||||
|
||||
interface TranslationEntry {
|
||||
translation: string;
|
||||
}
|
||||
|
||||
interface TranslationFile {
|
||||
[key: string]: TranslationEntry;
|
||||
}
|
||||
|
||||
interface IdMapping {
|
||||
oldId: string;
|
||||
newId: string;
|
||||
englishText: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a normalized string representation of translation entries for comparison
|
||||
* @param entries Array of translation entries
|
||||
* @returns Normalized string for comparison
|
||||
*/
|
||||
function normalizeTranslationEntries(entries: TranslationEntry): string {
|
||||
return JSON.stringify(entries);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares two translation files and creates a mapping of old IDs to new IDs
|
||||
* @param oldFilePath Path to the file with autogenerated IDs
|
||||
* @param newFilePath Path to the file with manually set IDs
|
||||
* @returns Array of ID mappings
|
||||
*/
|
||||
export async function createIdMapping(
|
||||
oldFilePath: string,
|
||||
newFilePath: string
|
||||
): Promise<IdMapping[]> {
|
||||
try {
|
||||
const oldFileContent = await fs.readFile(oldFilePath, "utf-8");
|
||||
const newFileContent = await fs.readFile(newFilePath, "utf-8");
|
||||
|
||||
const oldTranslations: TranslationFile = JSON.parse(oldFileContent);
|
||||
const newTranslations: TranslationFile = JSON.parse(newFileContent);
|
||||
|
||||
const mappings: IdMapping[] = [];
|
||||
|
||||
// Create a reverse lookup for new translations based on complete entry structure
|
||||
const newTranslationsByText = new Map<string, string>();
|
||||
for (const [newId, entries] of Object.entries(newTranslations)) {
|
||||
const normalizedEntries = normalizeTranslationEntries(entries);
|
||||
if (normalizedEntries && normalizedEntries.length > 0) {
|
||||
newTranslationsByText.set(normalizedEntries, newId);
|
||||
}
|
||||
}
|
||||
|
||||
// Match old IDs with new IDs based on complete entry structure
|
||||
for (const [oldId, entries] of Object.entries(oldTranslations)) {
|
||||
const normalizedEntries = normalizeTranslationEntries(entries);
|
||||
if (normalizedEntries && normalizedEntries.length > 0) {
|
||||
const englishText = entries.translation; // Keep first entry for display purposes
|
||||
const newId = newTranslationsByText.get(normalizedEntries);
|
||||
|
||||
if (newId && newId !== oldId) {
|
||||
mappings.push({
|
||||
oldId,
|
||||
newId,
|
||||
englishText,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return mappings;
|
||||
} catch (error) {
|
||||
console.error("Error creating ID mapping:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates a translation file by replacing old IDs with new IDs
|
||||
* @param filePath Path to the translation file to update
|
||||
* @param mappings Array of ID mappings
|
||||
* @returns Number of replacements made
|
||||
*/
|
||||
export async function updateTranslationFile(
|
||||
filePath: string,
|
||||
mappings: IdMapping[]
|
||||
): Promise<number> {
|
||||
try {
|
||||
const fileContent = await fs.readFile(filePath, "utf-8");
|
||||
const translations: TranslationFile = JSON.parse(fileContent);
|
||||
|
||||
let replacementCount = 0;
|
||||
const updatedTranslations: TranslationFile = {};
|
||||
|
||||
// Create a mapping lookup for efficient searching
|
||||
const mappingLookup = new Map(mappings.map((m) => [m.oldId, m.newId]));
|
||||
|
||||
for (const [oldId, entries] of Object.entries(translations)) {
|
||||
const newId = mappingLookup.get(oldId);
|
||||
if (newId) {
|
||||
updatedTranslations[newId] = entries;
|
||||
replacementCount++;
|
||||
console.log(` ${oldId} → ${newId}`);
|
||||
} else {
|
||||
updatedTranslations[oldId] = entries;
|
||||
}
|
||||
}
|
||||
|
||||
// Write the updated file with proper formatting
|
||||
await fs.writeFile(
|
||||
filePath,
|
||||
JSON.stringify(updatedTranslations, null, 2) + "\n",
|
||||
"utf-8"
|
||||
);
|
||||
|
||||
return replacementCount;
|
||||
} catch (error) {
|
||||
console.error(`Error updating file ${filePath}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates multiple translation files with the same ID mappings
|
||||
* @param mappings Array of ID mappings
|
||||
* @param translationFilePaths Array of file paths to update
|
||||
*/
|
||||
export async function updateAllTranslationFiles(
|
||||
mappings: IdMapping[],
|
||||
translationFilePaths: string[]
|
||||
): Promise<void> {
|
||||
console.log(`Found ${mappings.length} ID mappings to apply`);
|
||||
|
||||
for (const filePath of translationFilePaths) {
|
||||
try {
|
||||
const fileName = path.basename(filePath);
|
||||
console.log(`\nUpdating ${fileName}...`);
|
||||
|
||||
const replacementCount = await updateTranslationFile(
|
||||
filePath,
|
||||
mappings
|
||||
);
|
||||
console.log(
|
||||
` ✓ Made ${replacementCount} replacements in ${fileName}`
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
` ✗ Failed to update ${path.basename(filePath)}:`,
|
||||
error
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates that the mapping is correct by checking if the English text matches
|
||||
* @param mappings Array of ID mappings
|
||||
* @param oldFilePath Path to the old translation file
|
||||
* @param newFilePath Path to the new translation file
|
||||
*/
|
||||
export async function validateMappings(
|
||||
mappings: IdMapping[],
|
||||
oldFilePath: string,
|
||||
newFilePath: string
|
||||
): Promise<{ validCount: number; invalidCount: number }> {
|
||||
const oldTranslations: TranslationFile = JSON.parse(
|
||||
await fs.readFile(oldFilePath, "utf-8")
|
||||
);
|
||||
const newTranslations: TranslationFile = JSON.parse(
|
||||
await fs.readFile(newFilePath, "utf-8")
|
||||
);
|
||||
|
||||
console.log("\nValidating mappings:");
|
||||
let validCount = 0;
|
||||
let invalidCount = 0;
|
||||
|
||||
for (const mapping of mappings) {
|
||||
const oldEntry = oldTranslations[mapping.oldId];
|
||||
const newEntry = newTranslations[mapping.newId];
|
||||
|
||||
if (!oldEntry || !newEntry) {
|
||||
console.log(
|
||||
` ✗ Missing entry for ${mapping.oldId} → ${mapping.newId}`
|
||||
);
|
||||
invalidCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
const oldNormalized = normalizeTranslationEntries(oldEntry);
|
||||
const newNormalized = normalizeTranslationEntries(newEntry);
|
||||
|
||||
if (oldNormalized === newNormalized) {
|
||||
validCount++;
|
||||
} else {
|
||||
console.log(
|
||||
` ✗ Entry structure mismatch for ${mapping.oldId} → ${mapping.newId}`
|
||||
);
|
||||
console.log(` Old: ${JSON.stringify(oldEntry, null, 2)}`);
|
||||
console.log(` New: ${JSON.stringify(newEntry, null, 2)}`);
|
||||
invalidCount++;
|
||||
}
|
||||
}
|
||||
|
||||
console.log(
|
||||
`\nValidation complete: ${validCount} valid, ${invalidCount} invalid mappings`
|
||||
);
|
||||
return { validCount, invalidCount };
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds all translation files in a directory
|
||||
* @param directory Directory to search in
|
||||
* @returns Array of translation file paths
|
||||
*/
|
||||
export async function findTranslationFiles(
|
||||
directory: string
|
||||
): Promise<string[]> {
|
||||
try {
|
||||
const files = await fs.readdir(directory);
|
||||
return files
|
||||
.filter((file) => file.endsWith(".json"))
|
||||
.map((file) => path.join(directory, file));
|
||||
} catch (error) {
|
||||
console.error(`Error reading directory ${directory}:`, error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// CLI functionality
|
||||
async function main() {
|
||||
const args = process.argv.slice(2);
|
||||
|
||||
if (args.length < 2) {
|
||||
console.log(`
|
||||
Usage:
|
||||
npm run update-ids <old-en.json> <new-en.json> [translation-directory]
|
||||
|
||||
Examples:
|
||||
# Create mapping and update files in the same directory
|
||||
npm run update-ids ./old-en.json ./new-en.json
|
||||
|
||||
# Create mapping and update files in specific directory
|
||||
npm run update-ids ./old-en.json ./new-en.json ./translations/
|
||||
|
||||
# Just create mapping (no updates)
|
||||
npm run update-ids ./old-en.json ./new-en.json --dry-run
|
||||
`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const oldFilePath = path.resolve(args[0]);
|
||||
const newFilePath = path.resolve(args[1]);
|
||||
const isDryRun = args.includes("--dry-run");
|
||||
|
||||
// Determine translation directory
|
||||
let translationDirectory: string;
|
||||
if (args[2] && !args[2].startsWith("--")) {
|
||||
translationDirectory = path.resolve(args[2]);
|
||||
} else {
|
||||
translationDirectory = path.dirname(newFilePath);
|
||||
}
|
||||
|
||||
if (isDryRun) {
|
||||
console.log("\n🔍 DRY RUN MODE");
|
||||
}
|
||||
|
||||
try {
|
||||
// Create the ID mappings
|
||||
const mappings = await createIdMapping(oldFilePath, newFilePath);
|
||||
|
||||
if (mappings.length === 0) {
|
||||
console.log(
|
||||
"No ID mappings found. Files might already be synchronized."
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("Mappings that will be applied:");
|
||||
mappings.forEach((m) => {
|
||||
if (!m.englishText) {
|
||||
console.warn(
|
||||
` ⚠️ Invalid entry for old ID ${m.oldId} ${m.newId}`
|
||||
);
|
||||
}
|
||||
|
||||
console.log(
|
||||
` ${m.oldId} → ${m.newId} ("${m.englishText?.substring(0, 20)}...")`
|
||||
);
|
||||
});
|
||||
|
||||
// Validate mappings in dry-run mode (read-only)
|
||||
const { invalidCount } = await validateMappings(
|
||||
mappings,
|
||||
oldFilePath,
|
||||
newFilePath
|
||||
);
|
||||
|
||||
if (isDryRun) {
|
||||
console.log("\nExiting - No files were be modified");
|
||||
return;
|
||||
}
|
||||
|
||||
if (invalidCount > 0) {
|
||||
console.log("\n✗ Aborting due to invalid mappings");
|
||||
return;
|
||||
}
|
||||
|
||||
// Find all translation files
|
||||
const translationFiles =
|
||||
await findTranslationFiles(translationDirectory);
|
||||
const filesToUpdate = translationFiles.filter(
|
||||
(f) => f !== newFilePath && f !== oldFilePath
|
||||
);
|
||||
|
||||
if (filesToUpdate.length === 0) {
|
||||
console.log("No translation files found to update.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Update all translation files
|
||||
await updateAllTranslationFiles(mappings, filesToUpdate);
|
||||
|
||||
console.log("\n✓ All translation files updated successfully!");
|
||||
} catch (error) {
|
||||
console.error("Error:", error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Run CLI if this file is executed directly
|
||||
if (require.main === module) {
|
||||
main();
|
||||
}
|
||||
Reference in New Issue
Block a user