mirror of
https://github.com/johannesjo/super-productivity.git
synced 2026-07-17 16:37:43 +00:00
feat(tools): enhance translation management script (#6171)
- Add extract and merge modes to manage missing translations - Implement functions to extract missing keys and merge WIP files - Create usage documentation for the translation management script
This commit is contained in:
parent
298c956518
commit
bc3df77e3f
3 changed files with 284 additions and 2 deletions
|
|
@ -48,3 +48,7 @@ The above will display the English text for `SOME_KEY`.
|
|||
- Use `en.json` as reference for context
|
||||
- Keep translations concise (UI space is limited)
|
||||
- Test your translations locally if possible (`ng serve`)
|
||||
|
||||
## Translation Management Script
|
||||
|
||||
For managing missing translations and maintaining consistency, use the `tools/add-missing-i18n-variables.js` script. See [i18n-script-usage.md](i18n-script-usage.md) for detailed instructions.
|
||||
|
|
|
|||
91
docs/i18n-script-usage.md
Normal file
91
docs/i18n-script-usage.md
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
# I18n Translation Management Script
|
||||
|
||||
This document describes the usage of the `tools/add-missing-i18n-variables.js` script, which helps manage internationalization (i18n) files for Super Productivity.
|
||||
|
||||
## Overview
|
||||
|
||||
The script manages translation files located in `src/assets/i18n/`. The base language is English (`en.json`), and other languages like `de.json` (German), `tr.json` (Turkish), etc., contain translations.
|
||||
|
||||
The script supports three modes:
|
||||
|
||||
- **Extract mode**: Creates work-in-progress (WIP) files with missing translations for a specific language.
|
||||
- **Merge mode**: Merges translated WIP files back into the main language file.
|
||||
- **Legacy mode**: Adds missing keys to all language files (default when no mode is specified).
|
||||
|
||||
## File Structure
|
||||
|
||||
- `en.json`: The reference English file with all keys.
|
||||
- `{lang}.json`: Main translation files (e.g., `de.json`, `tr.json`).
|
||||
- `{lang}-wip.json`: Temporary work-in-progress files containing only missing translations.
|
||||
|
||||
## Usage
|
||||
|
||||
### Extract Missing Translations
|
||||
|
||||
To extract missing translations for a specific language (e.g., Turkish):
|
||||
|
||||
```bash
|
||||
node tools/add-missing-i18n-variables.js extract tr
|
||||
```
|
||||
|
||||
This creates `tr-wip.json` with all keys that exist in `en.json` but are missing or empty in `tr.json`.
|
||||
|
||||
### Translate WIP File
|
||||
|
||||
Edit the generated `{lang}-wip.json` file and provide translations for the keys.
|
||||
|
||||
Example `tr-wip.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"APP": {
|
||||
"SKIP_SYNC_WAIT": "Skip waiting for sync"
|
||||
},
|
||||
"F": {
|
||||
"CALDAV": {
|
||||
"ISSUE_CONTENT": {
|
||||
"DESCRIPTION": "Description"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Merge Translations
|
||||
|
||||
After translating the WIP file, merge it back into the main language file:
|
||||
|
||||
```bash
|
||||
node tools/add-missing-i18n-variables.js merge tr
|
||||
```
|
||||
|
||||
This:
|
||||
|
||||
- Merges translations from `tr-wip.json` into `tr.json`
|
||||
- Maintains the same key order as `en.json`
|
||||
- Validates that all keys are present
|
||||
- Deletes the `tr-wip.json` file
|
||||
|
||||
### Legacy Mode (Update All Files)
|
||||
|
||||
To add missing keys to all language files at once (preserving existing translations):
|
||||
|
||||
```bash
|
||||
node tools/add-missing-i18n-variables.js
|
||||
```
|
||||
|
||||
This updates all `{lang}.json` files to include any new keys from `en.json`, placing them in the same order.
|
||||
|
||||
## Workflow Example
|
||||
|
||||
1. New features are added, updating `en.json` with new keys.
|
||||
2. Run extract for your language: `node tools/add-missing-i18n-variables.js extract de`
|
||||
3. Translate the keys in `de-wip.json`.
|
||||
4. Run merge: `node tools/add-missing-i18n-variables.js merge de`
|
||||
5. Submit a pull request with the updated `de.json`.
|
||||
|
||||
## Notes
|
||||
|
||||
- The script preserves the order of keys to match `en.json`.
|
||||
- Empty strings in translation files trigger English fallback.
|
||||
- WIP files are temporary and it would be deleted via the merge command.
|
||||
|
|
@ -21,6 +21,83 @@ function mergeInOrder(enObj, langObj) {
|
|||
return result;
|
||||
}
|
||||
|
||||
// Extract keys that exist in en.json but not in langObj
|
||||
function extractMissingKeys(enObj, langObj) {
|
||||
const missing = {};
|
||||
|
||||
for (const key of Object.keys(enObj)) {
|
||||
if (typeof enObj[key] === 'object' && !Array.isArray(enObj[key])) {
|
||||
const nested = extractMissingKeys(enObj[key], langObj?.[key] || {});
|
||||
if (Object.keys(nested).length > 0) {
|
||||
missing[key] = nested;
|
||||
}
|
||||
} else if (!langObj || !(key in langObj)) {
|
||||
missing[key] = enObj[key];
|
||||
}
|
||||
}
|
||||
|
||||
return missing;
|
||||
}
|
||||
|
||||
// Merge WIP translations into main language file
|
||||
// IMPORTANT: Must maintain same order as en.json
|
||||
function mergeWipToMain(enObj, langObj, wipObj) {
|
||||
const result = {};
|
||||
|
||||
// Iterate through en.json keys to maintain order
|
||||
for (const key of Object.keys(enObj)) {
|
||||
if (typeof enObj[key] === 'object' && !Array.isArray(enObj[key])) {
|
||||
result[key] = mergeWipToMain(enObj[key], langObj?.[key] || {}, wipObj?.[key] || {});
|
||||
} else {
|
||||
// Priority: WIP > existing lang > English
|
||||
if (wipObj && key in wipObj) {
|
||||
result[key] = wipObj[key];
|
||||
} else if (langObj && key in langObj) {
|
||||
result[key] = langObj[key];
|
||||
} else {
|
||||
result[key] = enObj[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// Verify all en.json keys exist in langObj after merge
|
||||
function validateComplete(enObj, langObj, prefix = '') {
|
||||
const missing = [];
|
||||
|
||||
for (const key of Object.keys(enObj)) {
|
||||
const path = prefix ? `${prefix}.${key}` : key;
|
||||
|
||||
if (typeof enObj[key] === 'object' && !Array.isArray(enObj[key])) {
|
||||
missing.push(...validateComplete(enObj[key], langObj?.[key], path));
|
||||
} else if (!langObj || !(key in langObj)) {
|
||||
missing.push(path);
|
||||
}
|
||||
}
|
||||
|
||||
return missing;
|
||||
}
|
||||
|
||||
// Count total translation keys (leaf nodes only)
|
||||
function countKeys(obj) {
|
||||
let count = 0;
|
||||
for (const key of Object.keys(obj)) {
|
||||
if (typeof obj[key] === 'object' && !Array.isArray(obj[key])) {
|
||||
count += countKeys(obj[key]);
|
||||
} else {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
// Parse CLI arguments
|
||||
const args = process.argv.slice(2);
|
||||
const mode = args[0]; // 'extract', 'merge', or undefined for legacy
|
||||
const language = args[1]; // e.g., 'tr', 'de'
|
||||
|
||||
if (!fs.existsSync(enPath)) {
|
||||
console.error('en.json not found in src/assets/i18n/');
|
||||
process.exit(1);
|
||||
|
|
@ -34,10 +111,116 @@ if (!fs.existsSync(i18nDir)) {
|
|||
// Read the English reference file
|
||||
const en = JSON.parse(fs.readFileSync(enPath, 'utf8'));
|
||||
|
||||
// Get all i18n files except en.json
|
||||
// ========== EXTRACT MODE ==========
|
||||
if (mode === 'extract') {
|
||||
if (!language) {
|
||||
console.error('Error: Language code required for extract mode');
|
||||
console.error('Usage: node tools/add-missing-i18n-variables.js extract <language>');
|
||||
console.error('Example: node tools/add-missing-i18n-variables.js extract tr');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const langPath = path.join(i18nDir, `${language}.json`);
|
||||
const wipPath = path.join(i18nDir, `${language}-wip.json`);
|
||||
|
||||
// Read existing language file or create empty object
|
||||
let langObj = {};
|
||||
if (fs.existsSync(langPath)) {
|
||||
const content = fs.readFileSync(langPath, 'utf8');
|
||||
if (content.trim()) {
|
||||
langObj = JSON.parse(content);
|
||||
}
|
||||
}
|
||||
|
||||
// Extract missing keys
|
||||
const missing = extractMissingKeys(en, langObj);
|
||||
const missingCount = countKeys(missing);
|
||||
|
||||
if (missingCount === 0) {
|
||||
console.log(`✓ No missing translations for ${language}`);
|
||||
console.log(` All ${countKeys(en)} keys are present in ${language}.json`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Write WIP file
|
||||
fs.writeFileSync(wipPath, JSON.stringify(missing, null, 2) + '\n', 'utf8');
|
||||
|
||||
console.log(`✓ Found ${missingCount} missing translations`);
|
||||
console.log(`✓ Created ${language}-wip.json`);
|
||||
console.log('');
|
||||
console.log('Next steps:');
|
||||
console.log(` 1. Translate the keys in ${language}-wip.json`);
|
||||
console.log(` 2. Run: node tools/add-missing-i18n-variables.js merge ${language}`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// ========== MERGE MODE ==========
|
||||
if (mode === 'merge') {
|
||||
if (!language) {
|
||||
console.error('Error: Language code required for merge mode');
|
||||
console.error('Usage: node tools/add-missing-i18n-variables.js merge <language>');
|
||||
console.error('Example: node tools/add-missing-i18n-variables.js merge tr');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const langPath = path.join(i18nDir, `${language}.json`);
|
||||
const wipPath = path.join(i18nDir, `${language}-wip.json`);
|
||||
|
||||
if (!fs.existsSync(wipPath)) {
|
||||
console.error(`Error: ${language}-wip.json not found`);
|
||||
console.error(
|
||||
`Run extract first: node tools/add-missing-i18n-variables.js extract ${language}`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Read files
|
||||
let langObj = {};
|
||||
if (fs.existsSync(langPath)) {
|
||||
const content = fs.readFileSync(langPath, 'utf8');
|
||||
if (content.trim()) {
|
||||
langObj = JSON.parse(content);
|
||||
}
|
||||
}
|
||||
|
||||
const wipObj = JSON.parse(fs.readFileSync(wipPath, 'utf8'));
|
||||
const wipCount = countKeys(wipObj);
|
||||
|
||||
// Merge WIP into main file (maintaining en.json order)
|
||||
const merged = mergeWipToMain(en, langObj, wipObj);
|
||||
|
||||
// Validate
|
||||
const missingKeys = validateComplete(en, merged);
|
||||
if (missingKeys.length > 0) {
|
||||
console.error('✗ Validation failed: Some keys are still missing:');
|
||||
missingKeys.slice(0, 10).forEach((key) => console.error(` - ${key}`));
|
||||
if (missingKeys.length > 10) {
|
||||
console.error(` ... and ${missingKeys.length - 10} more`);
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Write merged file
|
||||
fs.writeFileSync(langPath, JSON.stringify(merged, null, 2) + '\n', 'utf8');
|
||||
|
||||
// Delete WIP file
|
||||
fs.unlinkSync(wipPath);
|
||||
|
||||
console.log(`✓ Merged ${wipCount} translations from ${language}-wip.json`);
|
||||
console.log(`✓ Updated ${language}.json (maintaining en.json order)`);
|
||||
console.log(`✓ Validation passed: All ${countKeys(en)} keys present`);
|
||||
console.log(`✓ Deleted ${language}-wip.json`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// ========== LEGACY MODE (default) ==========
|
||||
|
||||
// Get all i18n files except en.json and -wip.json files
|
||||
const i18nFiles = fs
|
||||
.readdirSync(i18nDir)
|
||||
.filter((file) => file.endsWith('.json') && file !== 'en.json')
|
||||
.filter(
|
||||
(file) => file.endsWith('.json') && file !== 'en.json' && !file.endsWith('-wip.json'),
|
||||
)
|
||||
.sort();
|
||||
|
||||
console.log(`Found ${i18nFiles.length} language files to update:`);
|
||||
|
|
@ -92,6 +275,10 @@ if (errors === 0) {
|
|||
console.log(
|
||||
'All language files updated successfully with missing keys in the same order as en.json.',
|
||||
);
|
||||
console.log('');
|
||||
console.log('💡 Tip: Use extract/merge workflow for incremental translations:');
|
||||
console.log(' node tools/add-missing-i18n-variables.js extract <lang>');
|
||||
console.log(' node tools/add-missing-i18n-variables.js merge <lang>');
|
||||
} else {
|
||||
console.log('');
|
||||
console.log('Some files had errors. Please check the output above.');
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue