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
336 lines
10 KiB
JavaScript
336 lines
10 KiB
JavaScript
#!/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();
|
|
}
|