localize-sync 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/cli.js ADDED
@@ -0,0 +1,53 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { program } from 'commander';
4
+ import { loadConfig } from '../src/utils/loadconfig.js';
5
+ import { check } from '../src/commands/check.js';
6
+ import { report } from '../src/commands/report.js';
7
+ import { translate } from '../src/commands/translate.js';
8
+
9
+ program
10
+ .name('localize-sync')
11
+ .description('Auto detect and translate missing i18n JSON keys')
12
+ .version('1.0.0');
13
+
14
+ const sharedOptions = (cmd) => {
15
+ return cmd
16
+ .option('-d, --dir <path>', 'Path to locales directory')
17
+ .option('-s, --source <lang>', 'Source language file (without .json)');
18
+ };
19
+
20
+ sharedOptions(
21
+ program.command('check').description('Check missing and extra keys across all locale files')
22
+ ).action(async (options) => {
23
+ const config = await loadConfig(options);
24
+ await check(config);
25
+ });
26
+
27
+ sharedOptions(
28
+ program.command('report').description('Generate a JSON report of translation coverage')
29
+ ).action(async (options) => {
30
+ const config = await loadConfig(options);
31
+ const results = await check(config);
32
+ await report(results, config);
33
+ });
34
+
35
+ sharedOptions(
36
+ program.command('translate').description('Auto translate missing keys using MyMemory API')
37
+ ).action(async (options) => {
38
+ const config = await loadConfig(options);
39
+ const results = await check(config);
40
+ await translate(results, config);
41
+ });
42
+
43
+ sharedOptions(
44
+ program.command('all').description('Run check, translate and report together')
45
+ ).action(async (options) => {
46
+ const config = await loadConfig(options);
47
+ const results = await check(config);
48
+ await translate(results, config);
49
+ const updatedResults = await check(config);
50
+ await report(updatedResults, config);
51
+ });
52
+
53
+ program.parse();
package/package.json ADDED
@@ -0,0 +1,21 @@
1
+ {
2
+ "name": "localize-sync",
3
+ "version": "1.0.0",
4
+ "description": "Auto translate i18n JSON files using MyMemory API",
5
+ "type": "module",
6
+ "bin": {
7
+ "localize-sync": "./bin/cli.js"
8
+ },
9
+ "scripts": {
10
+ "start": "node bin/cli.js"
11
+ },
12
+ "keywords": [
13
+ "i18n",
14
+ "translate",
15
+ "cli",
16
+ "localization",
17
+ "json"
18
+ ],
19
+ "author": "Amir Hossein Jamshidi",
20
+ "license": "MIT"
21
+ }
package/readme ADDED
@@ -0,0 +1,120 @@
1
+ # localize-sync
2
+
3
+ Auto-detect and translate missing i18n JSON keys using the MyMemory API.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install -g localize-sync
9
+ ```
10
+
11
+ Or use without installing:
12
+
13
+ ```bash
14
+ npx localize-sync <command>
15
+ ```
16
+
17
+ ---
18
+
19
+ ## How it works
20
+
21
+ `localize-sync` treats one JSON file as the **source of truth** (default: `fa.json`) and compares all other JSON files in the same directory against it. Missing keys are automatically translated via the [MyMemory API](https://mymemory.translated.net/).
22
+
23
+ ---
24
+
25
+ ## Commands
26
+
27
+ | Command | Description |
28
+ |---|---|
29
+ | `check` | Show missing and extra keys across all locale files |
30
+ | `translate` | Auto-translate missing keys |
31
+ | `report` | Generate a `i18n-report.json` coverage report |
32
+ | `all` | Run check → translate → report in sequence |
33
+
34
+ ---
35
+
36
+ ## Options
37
+
38
+ | Flag | Alias | Default | Description |
39
+ |---|---|---|---|
40
+ | `--dir <path>` | `-d` | `./locales` | Path to your locales directory |
41
+ | `--source <lang>` | `-s` | `fa` | Source language filename (without `.json`) |
42
+
43
+ ---
44
+
45
+ ## Usage
46
+
47
+ ```bash
48
+ # Check missing keys (source: fa.json, dir: ./locales)
49
+ localize-sync check
50
+
51
+ # Translate missing keys in a custom directory
52
+ localize-sync translate --dir ./src/translations
53
+
54
+ # Use a different source language
55
+ localize-sync all --source en
56
+
57
+ # Combine both options
58
+ localize-sync all --dir ./src/i18n --source en
59
+ ```
60
+
61
+ ---
62
+
63
+ ## Config file (optional)
64
+
65
+ Instead of passing flags every time, create an `i18n.config.js` in your project root:
66
+
67
+ ```js
68
+ // i18n.config.js
69
+ export default {
70
+ dir: './src/locales',
71
+ source: 'fa',
72
+ }
73
+ ```
74
+
75
+ Priority order: `CLI flags` > `i18n.config.js` > `defaults`
76
+
77
+ ---
78
+
79
+ ## Example
80
+
81
+ Given this structure:
82
+
83
+ ```
84
+ locales/
85
+ fa.json ← source of truth
86
+ en.json ← partially translated
87
+ de.json ← empty
88
+ ```
89
+
90
+ Running `localize-sync all` will:
91
+
92
+ 1. Detect missing keys in `en.json` and `de.json`
93
+ 2. Translate them automatically from Persian
94
+ 3. Save a coverage report to `i18n-report.json`
95
+
96
+ ---
97
+
98
+ ## Report output
99
+
100
+ ```json
101
+ {
102
+ "en": {
103
+ "total": 9,
104
+ "translated": 9,
105
+ "coverage": "100%",
106
+ "missingKeys": [],
107
+ "extraKeys": []
108
+ }
109
+ }
110
+ ```
111
+
112
+ ---
113
+
114
+ ## Notes
115
+
116
+ - Translation is powered by [MyMemory](https://mymemory.translated.net/) — free, no API key required
117
+ - A 300ms delay is applied between requests to avoid rate limiting
118
+ - If a translation fails, a `āš ļø MISSING: <original>` placeholder is inserted
119
+
120
+ ---
@@ -0,0 +1,57 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { flattenKeys } from '../utils/flattenKeys.js';
4
+
5
+ export async function check(config) {
6
+ const files = fs.readdirSync(config.dir).filter((f) => f.endsWith('.json'));
7
+
8
+ if (files.length === 0) {
9
+ console.error('āŒ No JSON files found in:', config.dir);
10
+ process.exit(1);
11
+ }
12
+
13
+ const sourceFile = `${config.source}.json`;
14
+
15
+ if (!files.includes(sourceFile)) {
16
+ console.error(`āŒ Source file "${sourceFile}" not found in ${config.dir}`);
17
+ process.exit(1);
18
+ }
19
+
20
+ const sourceData = JSON.parse(fs.readFileSync(path.join(config.dir, sourceFile), 'utf-8'));
21
+ const sourceKeys = flattenKeys(sourceData);
22
+
23
+ console.log(`\nšŸ“‹ Source: ${sourceFile} — ${Object.keys(sourceKeys).length} keys\n`);
24
+
25
+ const results = [];
26
+
27
+ files
28
+ .filter((f) => f !== sourceFile)
29
+ .forEach((file) => {
30
+ const lang = file.replace('.json', '');
31
+ const targetData = JSON.parse(fs.readFileSync(path.join(config.dir, file)));
32
+ const targetKeys = flattenKeys(targetData);
33
+
34
+ const missingKeys = Object.keys(sourceKeys).filter((k) => !(k in targetKeys));
35
+ const extraKeys = Object.keys(targetKeys).filter((k) => !(k in sourceKeys));
36
+
37
+ console.log(`šŸŒ ${file}`);
38
+ console.log(
39
+ ` āœ… Translated: ${Object.keys(targetKeys).length} / ${Object.keys(sourceKeys).length}`
40
+ );
41
+
42
+ if (missingKeys.length > 0) {
43
+ console.log(` āŒ Missing keys (${missingKeys.length}):`);
44
+ } else {
45
+ console.log(` šŸŽ‰ All keys are translated!`);
46
+ }
47
+
48
+ if (extraKeys.length > 0) {
49
+ console.log(` āš ļø Extra keys (${extraKeys.length}):`);
50
+ extraKeys.forEach((k) => console.log(` - ${k}`));
51
+ }
52
+
53
+ results.push({ lang, sourceKeys, targetKeys, missingKeys, extraKeys });
54
+ });
55
+
56
+ return results;
57
+ }
@@ -0,0 +1,20 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+
4
+ export async function report(results, config) {
5
+ const output = {};
6
+
7
+ results.forEach(({ lang, sourceKeys, targetKeys, missingKeys, extraKeys }) => {
8
+ output[lang] = {
9
+ total: Object.keys(sourceKeys).length,
10
+ translated: Object.keys(targetKeys).length,
11
+ coverage: `${Math.round((Object.keys(targetKeys).length / Object.keys(sourceKeys).length) * 100)}%`,
12
+ missingKeys,
13
+ extraKeys
14
+ };
15
+ });
16
+
17
+ const reportPath = path.resolve(process.cwd(), 'i18n-report.json');
18
+ fs.writeFileSync(reportPath, JSON.stringify(output, null, 2), 'utf-8');
19
+ console.log(`\nšŸ“„ Report saved: ${reportPath}\n`);
20
+ }
@@ -0,0 +1,39 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { translateText } from '../utils/translateText.js';
4
+
5
+ export async function translate(results, config) {
6
+ for (const result of results) {
7
+ const { lang, missingKeys, sourceKeys } = result;
8
+ const filePath = path.join(config.dir, `${lang}.json`);
9
+ const targetData = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
10
+ if (missingKeys.length === 0) {
11
+ console.log(`āœ… ${lang}.json - Nothing to translate`);
12
+ continue;
13
+ }
14
+
15
+ console.log(`\nšŸŒ ${lang}.json - Translating ${missingKeys.length} keys...\n`);
16
+
17
+ for (const flatKey of missingKeys) {
18
+ const originalText = sourceKeys[flatKey];
19
+ process.stdout.write(` šŸ”„ ${flatKey}: "${originalText}" → `);
20
+ const translated = await translateText(originalText, config.source, lang);
21
+
22
+ const parts = flatKey.split('.');
23
+ let current = targetData;
24
+ parts.forEach((part, index) => {
25
+ if (index === parts.length - 1) {
26
+ current[part] = translated ?? `āš ļø MISSING: ${originalText}`;
27
+ } else {
28
+ if (!current[part]) current[part] = {};
29
+ current = current[part];
30
+ }
31
+ });
32
+ console.log(translated ? `"${translated}"` : `āš ļø Translation failed, placeholder inserted`);
33
+ await new Promise((r) => setTimeout(r, 300));
34
+ }
35
+
36
+ fs.writeFileSync(filePath, JSON.stringify(targetData, null, 2), 'utf-8');
37
+ console.log(`\nšŸ’¾ ${lang}.json saved`);
38
+ }
39
+ }
@@ -0,0 +1,12 @@
1
+ export function flattenKeys(obj, prefix = '') {
2
+ return Object.keys(obj).reduce((acc, key) => {
3
+ const fullKey = prefix ? `${prefix}.${key}` : key;
4
+ const value = obj[key];
5
+ if (typeof value === 'object' && value !== null) {
6
+ Object.assign(acc, flattenKeys(value, fullKey));
7
+ } else {
8
+ acc[fullKey] = String(value);
9
+ }
10
+ return acc;
11
+ }, {});
12
+ }
@@ -0,0 +1,44 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+
4
+ const DEFAULTS = {
5
+ dir: './locales',
6
+ source: 'fa'
7
+ };
8
+
9
+ export async function loadConfig(flags = {}) {
10
+ let fileConfig = {};
11
+ const configPath = path.resolve(process.cwd(), 'i18n.config.js');
12
+
13
+ if (fs.existsSync(configPath)) {
14
+ try {
15
+ const imported = await import(configPath);
16
+ fileConfig = imported.default || {};
17
+ console.log('āš™ļø Config loaded from i18n.config.js');
18
+ } catch (err) {
19
+ console.warn('āš ļø Failed to load i18n.config.js, using defaults');
20
+ }
21
+ }
22
+
23
+ const config = {
24
+ dir: flags.dir || fileConfig.dir || DEFAULTS.dir,
25
+ source: flags.source || fileConfig.source || DEFAULTS.source
26
+ };
27
+
28
+ const resolvedDir = path.isAbsolute(config.dir)
29
+ ? config.dir
30
+ : path.resolve(process.cwd(), config.dir);
31
+ if (!fs.existsSync(resolvedDir)) {
32
+ console.log(`āŒ Directory not found: ${resolvedDir}`);
33
+ process.exit(1);
34
+ }
35
+
36
+ const stat = fs.statSync(resolvedDir);
37
+ if (!stat.isDirectory()) {
38
+ console.error(`āŒ Path is not a directory: ${resolvedDir}`);
39
+ process.exit(1);
40
+ }
41
+
42
+ config.dir = resolvedDir;
43
+ return config;
44
+ }
@@ -0,0 +1,28 @@
1
+ import fetch from 'node-fetch';
2
+
3
+ const LANG_MAP = {
4
+ en: 'en-US',
5
+ de: 'de-DE',
6
+ fr: 'fr-FR',
7
+ ar: 'ar-SA',
8
+ es: 'es-ES',
9
+ it: 'it-IT',
10
+ tr: 'tr-TR'
11
+ };
12
+
13
+ export async function translateText(text, fromLang, toLang) {
14
+ const langPair = `${LANG_MAP[fromLang] || fromLang}|${LANG_MAP[toLang] || toLang}`;
15
+ const url = `https://api.mymemory.translated.net/get?q=${encodeURIComponent(text)}&langpair=${langPair}`;
16
+
17
+ try {
18
+ const res = await fetch(url);
19
+ const data = await res.json();
20
+ if (data.responseStatus === 200) {
21
+ return data.responseData.translatedText;
22
+ }
23
+ return null;
24
+ } catch (err) {
25
+ console.error(`āŒ Error translating "${text}":`, err.message);
26
+ return null;
27
+ }
28
+ }