@localheroai/cli 0.0.3 → 0.0.5
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/package.json +8 -3
- package/src/commands/translate.js +5 -0
- package/src/utils/files.js +25 -5
- package/src/utils/github.js +8 -2
- package/src/utils/import-service.js +8 -9
- package/src/utils/translation-updater/json-handler.js +92 -93
- package/src/utils/translation-updater/yaml-handler.js +170 -144
- package/src/utils/translation-utils.js +45 -4
package/package.json
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@localheroai/cli",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.5",
|
|
4
4
|
"description": "CLI tool for managing translations with LocalHero.ai",
|
|
5
|
+
"homepage": "https://localhero.ai",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/localheroai/cli"
|
|
9
|
+
},
|
|
5
10
|
"type": "module",
|
|
6
11
|
"main": "src/index.js",
|
|
7
12
|
"bin": {
|
|
@@ -41,7 +46,7 @@
|
|
|
41
46
|
},
|
|
42
47
|
"files": [
|
|
43
48
|
"src",
|
|
44
|
-
"README
|
|
49
|
+
"README",
|
|
45
50
|
"LICENSE"
|
|
46
51
|
],
|
|
47
52
|
"jest": {
|
|
@@ -59,4 +64,4 @@
|
|
|
59
64
|
]
|
|
60
65
|
}
|
|
61
66
|
}
|
|
62
|
-
}
|
|
67
|
+
}
|
|
@@ -76,6 +76,11 @@ export async function translate(options = {}, deps = defaultDeps) {
|
|
|
76
76
|
|
|
77
77
|
if (sourceFiles.length === 0) {
|
|
78
78
|
console.error(chalk.red(`\n✖ No source files found for locale ${config.sourceLocale}\n`));
|
|
79
|
+
console.error(chalk.yellow(`This could be due to one of the following issues:`));
|
|
80
|
+
console.error(chalk.yellow(` 1. No translation files with the source locale "${config.sourceLocale}" exist in the configured paths`));
|
|
81
|
+
console.error(chalk.yellow(` 2. The locale identifiers in your filenames don't match the expected pattern`));
|
|
82
|
+
console.error(chalk.yellow(` 3. There was an error parsing one or more files (check for syntax errors in YAML or JSON)\n`));
|
|
83
|
+
console.error(chalk.yellow(`Try running with the --verbose flag for more detailed information.\n`));
|
|
79
84
|
process.exit(1);
|
|
80
85
|
}
|
|
81
86
|
|
package/src/utils/files.js
CHANGED
|
@@ -5,14 +5,22 @@ import path from 'path';
|
|
|
5
5
|
import yaml from 'yaml';
|
|
6
6
|
import { promises as fs } from 'fs';
|
|
7
7
|
|
|
8
|
-
export function parseFile(content, format) {
|
|
8
|
+
export function parseFile(content, format, filePath = '') {
|
|
9
9
|
try {
|
|
10
10
|
if (format === 'json') {
|
|
11
|
-
|
|
11
|
+
try {
|
|
12
|
+
return JSON.parse(content);
|
|
13
|
+
} catch (jsonError) {
|
|
14
|
+
const errorInfo = jsonError.message.match(/at position (\d+)/)
|
|
15
|
+
? jsonError.message
|
|
16
|
+
: `${jsonError.message} (check for missing commas, quotes, or brackets)`;
|
|
17
|
+
throw new Error(errorInfo);
|
|
18
|
+
}
|
|
12
19
|
}
|
|
13
20
|
return yaml.parse(content);
|
|
14
21
|
} catch (error) {
|
|
15
|
-
|
|
22
|
+
const location = filePath ? ` in ${filePath}` : '';
|
|
23
|
+
throw new Error(`Failed to parse ${format} file${location}: ${error.message}`);
|
|
16
24
|
}
|
|
17
25
|
}
|
|
18
26
|
|
|
@@ -270,7 +278,7 @@ export async function findTranslationFiles(config, options = {}) {
|
|
|
270
278
|
|
|
271
279
|
if (parseContent) {
|
|
272
280
|
const content = await readFile(filePath, 'utf8');
|
|
273
|
-
const parsedContent = parseFile(content, format);
|
|
281
|
+
const parsedContent = parseFile(content, format, filePath);
|
|
274
282
|
|
|
275
283
|
if (includeContent) {
|
|
276
284
|
result.content = Buffer.from(content).toString('base64');
|
|
@@ -291,7 +299,19 @@ export async function findTranslationFiles(config, options = {}) {
|
|
|
291
299
|
|
|
292
300
|
processedFiles.push(result);
|
|
293
301
|
} catch (error) {
|
|
294
|
-
if (
|
|
302
|
+
if (error.message.includes('Failed to parse') ||
|
|
303
|
+
error.message.includes('JSON') ||
|
|
304
|
+
error.message.includes('Unexpected token') ||
|
|
305
|
+
error.message.includes('Missing closing')) {
|
|
306
|
+
console.warn(chalk.yellow(`\nWarning: ${error.message}`));
|
|
307
|
+
|
|
308
|
+
const format = path.extname(file).slice(1);
|
|
309
|
+
if (format === 'json') {
|
|
310
|
+
console.warn(chalk.gray(' Tip: Check for missing commas, quotes, or brackets in your JSON file.'));
|
|
311
|
+
} else if (format === 'yml' || format === 'yaml') {
|
|
312
|
+
console.warn(chalk.gray(' Tip: Check for proper indentation and quote matching in your YAML file.'));
|
|
313
|
+
}
|
|
314
|
+
} else if (verbose) {
|
|
295
315
|
console.warn(chalk.yellow(`Warning: ${error.message}`));
|
|
296
316
|
}
|
|
297
317
|
}
|
package/src/utils/github.js
CHANGED
|
@@ -34,7 +34,13 @@ export const githubService = {
|
|
|
34
34
|
on:
|
|
35
35
|
pull_request:
|
|
36
36
|
paths:
|
|
37
|
-
${translationPaths.map(p =>
|
|
37
|
+
${translationPaths.map(p => {
|
|
38
|
+
// Check if path already contains a file pattern (*, ?, or {})
|
|
39
|
+
const hasPattern = /[*?{}]/.test(p);
|
|
40
|
+
// If it has a pattern, use it as is; otherwise, append /**
|
|
41
|
+
const formattedPath = hasPattern ? p : `${p}${p.endsWith('/') ? '' : '/'}**`;
|
|
42
|
+
return `- "${formattedPath}"`;
|
|
43
|
+
}).join('\n ')}
|
|
38
44
|
|
|
39
45
|
jobs:
|
|
40
46
|
translate:
|
|
@@ -53,7 +59,7 @@ jobs:
|
|
|
53
59
|
- name: Set up Node.js
|
|
54
60
|
uses: actions/setup-node@v4
|
|
55
61
|
with:
|
|
56
|
-
node-version:
|
|
62
|
+
node-version: 22
|
|
57
63
|
|
|
58
64
|
- name: Run LocalHero CLI
|
|
59
65
|
env:
|
|
@@ -95,28 +95,27 @@ export const importService = {
|
|
|
95
95
|
translations: allTranslations
|
|
96
96
|
});
|
|
97
97
|
|
|
98
|
-
if (importResult.status === 'failed') {
|
|
98
|
+
if (importResult.import?.status === 'failed') {
|
|
99
99
|
return {
|
|
100
|
-
...importResult,
|
|
100
|
+
...importResult.import,
|
|
101
101
|
files: importedFiles
|
|
102
102
|
};
|
|
103
103
|
}
|
|
104
104
|
|
|
105
105
|
let finalImportResult = importResult;
|
|
106
|
-
while (finalImportResult.status === 'processing') {
|
|
107
|
-
await new Promise(resolve => setTimeout(resolve, finalImportResult.poll_interval * 1000));
|
|
108
|
-
finalImportResult = await checkImportStatus(config.projectId, finalImportResult.id);
|
|
106
|
+
while (finalImportResult.import?.status === 'processing') {
|
|
107
|
+
await new Promise(resolve => setTimeout(resolve, finalImportResult.import.poll_interval * 1000));
|
|
108
|
+
finalImportResult = await checkImportStatus(config.projectId, finalImportResult.import.id);
|
|
109
109
|
|
|
110
|
-
if (finalImportResult.status === 'failed') {
|
|
110
|
+
if (finalImportResult.import?.status === 'failed') {
|
|
111
111
|
return {
|
|
112
|
-
...finalImportResult,
|
|
112
|
+
...finalImportResult.import,
|
|
113
113
|
files: importedFiles
|
|
114
114
|
};
|
|
115
115
|
}
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
-
|
|
119
|
-
const { status, statistics, warnings, translations_url, sourceImport } = finalImportResult;
|
|
118
|
+
const { import: { status, statistics, warnings, translations_url, sourceImport } = {} } = finalImportResult;
|
|
120
119
|
return {
|
|
121
120
|
status,
|
|
122
121
|
statistics,
|
|
@@ -1,112 +1,111 @@
|
|
|
1
1
|
import { promises as fs } from 'fs';
|
|
2
|
-
import path from 'path';
|
|
3
2
|
import { detectJsonFormat, preserveJsonStructure } from '../files.js';
|
|
4
3
|
import { ensureDirectoryExists } from './common.js';
|
|
5
4
|
|
|
6
5
|
export async function updateJsonFile(filePath, translations, languageCode) {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
try {
|
|
17
|
-
const content = await fs.readFile(filePath, 'utf8');
|
|
18
|
-
existingContent = JSON.parse(content);
|
|
19
|
-
if (existingContent[languageCode] && typeof existingContent[languageCode] === 'object') {
|
|
20
|
-
hasLanguageWrapper = true;
|
|
21
|
-
jsonFormat = detectJsonFormat(existingContent[languageCode]);
|
|
22
|
-
} else {
|
|
23
|
-
jsonFormat = detectJsonFormat(existingContent);
|
|
24
|
-
}
|
|
25
|
-
} catch {
|
|
26
|
-
console.warn(`Creating new JSON file: ${filePath}`);
|
|
27
|
-
result.created = true;
|
|
28
|
-
await ensureDirectoryExists(filePath);
|
|
29
|
-
}
|
|
6
|
+
try {
|
|
7
|
+
let existingContent = {};
|
|
8
|
+
let jsonFormat = 'nested';
|
|
9
|
+
let hasLanguageWrapper = false;
|
|
10
|
+
const result = {
|
|
11
|
+
updatedKeys: Object.keys(translations),
|
|
12
|
+
created: false
|
|
13
|
+
};
|
|
30
14
|
|
|
31
|
-
|
|
15
|
+
try {
|
|
16
|
+
const content = await fs.readFile(filePath, 'utf8');
|
|
17
|
+
existingContent = JSON.parse(content);
|
|
18
|
+
if (existingContent[languageCode] && typeof existingContent[languageCode] === 'object') {
|
|
19
|
+
hasLanguageWrapper = true;
|
|
20
|
+
jsonFormat = detectJsonFormat(existingContent[languageCode]);
|
|
21
|
+
} else {
|
|
22
|
+
jsonFormat = detectJsonFormat(existingContent);
|
|
23
|
+
}
|
|
24
|
+
} catch {
|
|
25
|
+
console.warn(`Creating new JSON file: ${filePath}`);
|
|
26
|
+
result.created = true;
|
|
27
|
+
await ensureDirectoryExists(filePath);
|
|
28
|
+
}
|
|
32
29
|
|
|
33
|
-
|
|
34
|
-
updatedContent = {
|
|
35
|
-
[languageCode]: preserveJsonStructure({}, translations, jsonFormat)
|
|
36
|
-
};
|
|
37
|
-
} else if (hasLanguageWrapper) {
|
|
38
|
-
existingContent[languageCode] = existingContent[languageCode] || {};
|
|
39
|
-
updatedContent = JSON.parse(JSON.stringify(existingContent));
|
|
40
|
-
const mergedContent = preserveJsonStructure(
|
|
41
|
-
existingContent[languageCode],
|
|
42
|
-
translations,
|
|
43
|
-
jsonFormat
|
|
44
|
-
);
|
|
45
|
-
updatedContent[languageCode] = mergedContent;
|
|
46
|
-
} else {
|
|
47
|
-
const existingCopy = JSON.parse(JSON.stringify(existingContent));
|
|
48
|
-
updatedContent = preserveJsonStructure(existingCopy, translations, jsonFormat);
|
|
49
|
-
}
|
|
30
|
+
let updatedContent;
|
|
50
31
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
32
|
+
if (result.created) {
|
|
33
|
+
updatedContent = {
|
|
34
|
+
[languageCode]: preserveJsonStructure({}, translations, jsonFormat)
|
|
35
|
+
};
|
|
36
|
+
} else if (hasLanguageWrapper) {
|
|
37
|
+
existingContent[languageCode] = existingContent[languageCode] || {};
|
|
38
|
+
updatedContent = JSON.parse(JSON.stringify(existingContent));
|
|
39
|
+
const mergedContent = preserveJsonStructure(
|
|
40
|
+
existingContent[languageCode],
|
|
41
|
+
translations,
|
|
42
|
+
jsonFormat
|
|
43
|
+
);
|
|
44
|
+
updatedContent[languageCode] = mergedContent;
|
|
45
|
+
} else {
|
|
46
|
+
const existingCopy = JSON.parse(JSON.stringify(existingContent));
|
|
47
|
+
updatedContent = preserveJsonStructure(existingCopy, translations, jsonFormat);
|
|
55
48
|
}
|
|
49
|
+
|
|
50
|
+
await fs.writeFile(filePath, JSON.stringify(updatedContent, null, 2));
|
|
51
|
+
return result;
|
|
52
|
+
} catch (error) {
|
|
53
|
+
throw new Error(`Failed to update JSON file ${filePath}: ${error.message}`);
|
|
54
|
+
}
|
|
56
55
|
}
|
|
57
56
|
|
|
58
57
|
export async function deleteKeysFromJsonFile(filePath, keysToDelete, languageCode) {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
const deletedKeys = [];
|
|
58
|
+
try {
|
|
59
|
+
const content = await fs.readFile(filePath, 'utf8');
|
|
60
|
+
let jsonContent = JSON.parse(content);
|
|
61
|
+
let hasLanguageWrapper = false;
|
|
62
|
+
let rootContent = jsonContent;
|
|
63
|
+
if (jsonContent[languageCode] && typeof jsonContent[languageCode] === 'object') {
|
|
64
|
+
hasLanguageWrapper = true;
|
|
65
|
+
rootContent = jsonContent[languageCode];
|
|
66
|
+
}
|
|
70
67
|
|
|
71
|
-
|
|
72
|
-
const keys = keyPath.split('.');
|
|
73
|
-
const lastIndex = keys.length - 1;
|
|
74
|
-
let current = rootContent;
|
|
75
|
-
let parent = null;
|
|
76
|
-
let keyInParent = '';
|
|
77
|
-
let found = true;
|
|
68
|
+
const deletedKeys = [];
|
|
78
69
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
keyInParent = key;
|
|
87
|
-
current = current[key];
|
|
88
|
-
}
|
|
70
|
+
for (const keyPath of keysToDelete) {
|
|
71
|
+
const keys = keyPath.split('.');
|
|
72
|
+
const lastIndex = keys.length - 1;
|
|
73
|
+
let current = rootContent;
|
|
74
|
+
let parent = null;
|
|
75
|
+
let keyInParent = '';
|
|
76
|
+
let found = true;
|
|
89
77
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
if (parent && Object.keys(current).length === 0) {
|
|
96
|
-
delete parent[keyInParent];
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
if (hasLanguageWrapper) {
|
|
102
|
-
jsonContent[languageCode] = rootContent;
|
|
103
|
-
} else {
|
|
104
|
-
jsonContent = rootContent;
|
|
78
|
+
for (let i = 0; i < lastIndex; i++) {
|
|
79
|
+
const key = keys[i];
|
|
80
|
+
if (!current[key] || typeof current[key] !== 'object') {
|
|
81
|
+
found = false;
|
|
82
|
+
break;
|
|
105
83
|
}
|
|
106
|
-
|
|
84
|
+
parent = current;
|
|
85
|
+
keyInParent = key;
|
|
86
|
+
current = current[key];
|
|
87
|
+
}
|
|
107
88
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
89
|
+
if (found) {
|
|
90
|
+
const lastKey = keys[lastIndex];
|
|
91
|
+
if (current[lastKey] !== undefined) {
|
|
92
|
+
delete current[lastKey];
|
|
93
|
+
deletedKeys.push(keyPath);
|
|
94
|
+
if (parent && Object.keys(current).length === 0) {
|
|
95
|
+
delete parent[keyInParent];
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
if (hasLanguageWrapper) {
|
|
101
|
+
jsonContent[languageCode] = rootContent;
|
|
102
|
+
} else {
|
|
103
|
+
jsonContent = rootContent;
|
|
111
104
|
}
|
|
105
|
+
await fs.writeFile(filePath, JSON.stringify(jsonContent, null, 2));
|
|
106
|
+
|
|
107
|
+
return deletedKeys;
|
|
108
|
+
} catch (error) {
|
|
109
|
+
throw new Error(`Failed to delete keys from JSON file ${filePath}: ${error.message}`);
|
|
110
|
+
}
|
|
112
111
|
}
|
|
@@ -2,180 +2,206 @@ import { promises as fs } from 'fs';
|
|
|
2
2
|
import yaml from 'yaml';
|
|
3
3
|
import { SPECIAL_CHARS_REGEX, INTERPOLATION, fileExists, tryParseJsonArray } from './common.js';
|
|
4
4
|
|
|
5
|
+
const NEEDS_QUOTES_REGEX = /[:,%{}[\]|><!&*?-]/;
|
|
6
|
+
const LINE_WIDTH = 80;
|
|
7
|
+
|
|
5
8
|
function detectYamlOptions(content) {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
}
|
|
9
|
+
const lines = content
|
|
10
|
+
.split('\n')
|
|
11
|
+
.filter(line => line.trim())
|
|
12
|
+
.slice(0, 10);
|
|
13
|
+
|
|
14
|
+
const options = {
|
|
15
|
+
indent: 2,
|
|
16
|
+
indentSeq: true
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const indentMatch = lines.find(line => /^\s+\S/.test(line))?.match(/^(\s+)\S/);
|
|
20
|
+
if (indentMatch) {
|
|
21
|
+
options.indent = indentMatch[1].length;
|
|
22
|
+
if (indentMatch[1].includes('\t')) {
|
|
23
|
+
options.indent = 2;
|
|
22
24
|
}
|
|
25
|
+
}
|
|
23
26
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
27
|
+
const seqMatch = lines.find(line => /^\s*-\s+\S/.test(line));
|
|
28
|
+
if (seqMatch) {
|
|
29
|
+
options.indentSeq = /^\s+-\s+/.test(seqMatch);
|
|
30
|
+
}
|
|
28
31
|
|
|
29
|
-
|
|
32
|
+
return options;
|
|
30
33
|
}
|
|
31
34
|
|
|
32
|
-
function
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
return itemNode;
|
|
42
|
-
});
|
|
35
|
+
function needsQuotes(str) {
|
|
36
|
+
if (typeof str !== 'string') return false;
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
SPECIAL_CHARS_REGEX.test(str) ||
|
|
40
|
+
str.includes(INTERPOLATION) ||
|
|
41
|
+
NEEDS_QUOTES_REGEX.test(str) ||
|
|
42
|
+
(str.includes(' ') && /[:"']/g.test(str))
|
|
43
|
+
);
|
|
43
44
|
}
|
|
44
45
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
if (!exists) {
|
|
48
|
-
console.warn(`Creating new file: ${filePath}`);
|
|
49
|
-
const doc = new yaml.Document();
|
|
50
|
-
doc.contents = doc.createNode({});
|
|
51
|
-
return { doc, created: true, options: { indent: 2, indentSeq: true } };
|
|
52
|
-
}
|
|
46
|
+
function shouldForceQuotes(str) {
|
|
47
|
+
if (typeof str !== 'string') return false;
|
|
53
48
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
options
|
|
60
|
-
};
|
|
61
|
-
}
|
|
49
|
+
// Special case: strings containing quotes but no interpolation
|
|
50
|
+
// don't need outer quotes
|
|
51
|
+
if (str.includes('"') && !str.includes(INTERPOLATION)) {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
62
54
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
yamlDoc.contents = yamlDoc.createNode({});
|
|
66
|
-
}
|
|
55
|
+
return needsQuotes(str);
|
|
56
|
+
}
|
|
67
57
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
58
|
+
function processArrayItems(array, yamlDoc) {
|
|
59
|
+
return array.map(item => {
|
|
60
|
+
const itemNode = yamlDoc.createNode(item);
|
|
61
|
+
if (shouldForceQuotes(item)) {
|
|
62
|
+
itemNode.type = 'QUOTE_DOUBLE';
|
|
71
63
|
}
|
|
64
|
+
return itemNode;
|
|
65
|
+
});
|
|
66
|
+
}
|
|
72
67
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
68
|
+
async function createYamlDocument(filePath) {
|
|
69
|
+
const exists = await fileExists(filePath);
|
|
70
|
+
if (!exists) {
|
|
71
|
+
console.warn(`Creating new file: ${filePath}`);
|
|
72
|
+
const doc = new yaml.Document();
|
|
73
|
+
doc.contents = doc.createNode({});
|
|
74
|
+
return { doc, created: true, options: { indent: 2, indentSeq: true, lineWidth: LINE_WIDTH } };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const content = await fs.readFile(filePath, 'utf8');
|
|
78
|
+
const options = detectYamlOptions(content);
|
|
79
|
+
const doc = yaml.parseDocument(content);
|
|
80
|
+
doc.options.lineWidth = LINE_WIDTH;
|
|
81
|
+
return {
|
|
82
|
+
doc,
|
|
83
|
+
created: false,
|
|
84
|
+
options
|
|
85
|
+
};
|
|
86
|
+
}
|
|
78
87
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
88
|
+
async function updateYamlTranslations(yamlDoc, translations, languageCode) {
|
|
89
|
+
if (!yamlDoc.contents) {
|
|
90
|
+
yamlDoc.contents = yamlDoc.createNode({});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const rootNode = yamlDoc.contents;
|
|
94
|
+
if (!rootNode.has(languageCode)) {
|
|
95
|
+
rootNode.set(languageCode, yamlDoc.createNode({}));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const langNode = rootNode.get(languageCode);
|
|
99
|
+
|
|
100
|
+
for (const [keyPath, newValue] of Object.entries(translations)) {
|
|
101
|
+
const keys = keyPath.split('.');
|
|
102
|
+
let current = langNode;
|
|
103
|
+
|
|
104
|
+
for (let i = 0; i < keys.length - 1; i++) {
|
|
105
|
+
const key = keys[i];
|
|
106
|
+
if (!current.has(key)) {
|
|
107
|
+
current.set(key, yamlDoc.createNode({}));
|
|
108
|
+
}
|
|
109
|
+
current = current.get(key);
|
|
110
|
+
}
|
|
86
111
|
|
|
87
|
-
|
|
112
|
+
const lastKey = keys[keys.length - 1];
|
|
88
113
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
114
|
+
if (Array.isArray(newValue)) {
|
|
115
|
+
const arrayNode = new yaml.YAMLSeq();
|
|
116
|
+
processArrayItems(newValue, yamlDoc).forEach(item => arrayNode.add(item));
|
|
117
|
+
current.set(lastKey, arrayNode);
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
95
120
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
121
|
+
const array = tryParseJsonArray(newValue);
|
|
122
|
+
if (array) {
|
|
123
|
+
const arrayNode = new yaml.YAMLSeq();
|
|
124
|
+
processArrayItems(array, yamlDoc).forEach(item => arrayNode.add(item));
|
|
125
|
+
current.set(lastKey, arrayNode);
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
103
128
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
129
|
+
if (typeof newValue === 'string' && newValue.includes('\n')) {
|
|
130
|
+
const scalar = new yaml.Scalar(newValue);
|
|
131
|
+
scalar.type = 'BLOCK_LITERAL';
|
|
132
|
+
current.set(lastKey, scalar);
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
110
135
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
}
|
|
115
|
-
current.set(lastKey, node);
|
|
136
|
+
const node = yamlDoc.createNode(newValue);
|
|
137
|
+
if (needsQuotes(newValue)) {
|
|
138
|
+
node.type = 'QUOTE_DOUBLE';
|
|
116
139
|
}
|
|
140
|
+
current.set(lastKey, node);
|
|
141
|
+
}
|
|
117
142
|
}
|
|
118
143
|
|
|
119
144
|
export async function updateYamlFile(filePath, translations, languageCode) {
|
|
120
|
-
|
|
145
|
+
const { doc: yamlDoc, created, options } = await createYamlDocument(filePath);
|
|
121
146
|
|
|
122
|
-
|
|
147
|
+
await updateYamlTranslations(yamlDoc, translations, languageCode);
|
|
123
148
|
|
|
124
|
-
|
|
125
|
-
|
|
149
|
+
yamlDoc.options.indent = options.indent;
|
|
150
|
+
yamlDoc.options.indentSeq = options.indentSeq;
|
|
151
|
+
yamlDoc.options.lineWidth = LINE_WIDTH;
|
|
126
152
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
153
|
+
await fs.writeFile(filePath, yamlDoc.toString());
|
|
154
|
+
return {
|
|
155
|
+
updatedKeys: Object.keys(translations),
|
|
156
|
+
created
|
|
157
|
+
};
|
|
132
158
|
}
|
|
133
159
|
|
|
134
160
|
export async function deleteKeysFromYamlFile(filePath, keysToDelete, languageCode) {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
161
|
+
try {
|
|
162
|
+
const content = await fs.readFile(filePath, 'utf8');
|
|
163
|
+
const yamlDoc = yaml.parseDocument(content);
|
|
164
|
+
if (!yamlDoc.contents || !yamlDoc.contents.has(languageCode)) {
|
|
165
|
+
return [];
|
|
166
|
+
}
|
|
141
167
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
}
|
|
159
|
-
parent = current;
|
|
160
|
-
keyInParent = key;
|
|
161
|
-
current = current.get(key);
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
if (found) {
|
|
165
|
-
const lastKey = keys[lastIndex];
|
|
166
|
-
if (current.has(lastKey)) {
|
|
167
|
-
current.delete(lastKey);
|
|
168
|
-
deletedKeys.push(keyPath);
|
|
169
|
-
if (parent && current.items.length === 0) {
|
|
170
|
-
parent.delete(keyInParent);
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
}
|
|
168
|
+
const langNode = yamlDoc.contents.get(languageCode);
|
|
169
|
+
const deletedKeys = [];
|
|
170
|
+
|
|
171
|
+
for (const keyPath of keysToDelete) {
|
|
172
|
+
const keys = keyPath.split('.');
|
|
173
|
+
const lastIndex = keys.length - 1;
|
|
174
|
+
let current = langNode;
|
|
175
|
+
let parent = null;
|
|
176
|
+
let keyInParent = '';
|
|
177
|
+
let found = true;
|
|
178
|
+
|
|
179
|
+
for (let i = 0; i < lastIndex; i++) {
|
|
180
|
+
const key = keys[i];
|
|
181
|
+
if (!current.has(key)) {
|
|
182
|
+
found = false;
|
|
183
|
+
break;
|
|
174
184
|
}
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
185
|
+
parent = current;
|
|
186
|
+
keyInParent = key;
|
|
187
|
+
current = current.get(key);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (found) {
|
|
191
|
+
const lastKey = keys[lastIndex];
|
|
192
|
+
if (current.has(lastKey)) {
|
|
193
|
+
current.delete(lastKey);
|
|
194
|
+
deletedKeys.push(keyPath);
|
|
195
|
+
if (parent && current.items.length === 0) {
|
|
196
|
+
parent.delete(keyInParent);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
180
200
|
}
|
|
201
|
+
|
|
202
|
+
await fs.writeFile(filePath, yamlDoc.toString());
|
|
203
|
+
return deletedKeys;
|
|
204
|
+
} catch (error) {
|
|
205
|
+
throw new Error(`Failed to delete keys from YAML file ${filePath}: ${error.message}`);
|
|
206
|
+
}
|
|
181
207
|
}
|
|
@@ -155,11 +155,53 @@ export function batchKeysWithMissing(sourceFiles, missingByLocale, batchSize = 1
|
|
|
155
155
|
}
|
|
156
156
|
|
|
157
157
|
export function findTargetFile(targetFiles, targetLocale, sourceFile, sourceLocale) {
|
|
158
|
-
|
|
158
|
+
// First try exact directory matching (existing logic)
|
|
159
|
+
let found = targetFiles.find(f =>
|
|
159
160
|
f.locale === targetLocale &&
|
|
160
|
-
|
|
161
|
-
|
|
161
|
+
path.dirname(f.path) === path.dirname(sourceFile.path) &&
|
|
162
|
+
path.basename(f.path, path.extname(f.path)) === path.basename(sourceFile.path, path.extname(sourceFile.path)).replace(sourceLocale, targetLocale)
|
|
162
163
|
);
|
|
164
|
+
|
|
165
|
+
if (found) return found;
|
|
166
|
+
|
|
167
|
+
// Then try filename-based matching regardless of directory (existing logic)
|
|
168
|
+
found = targetFiles.find(f =>
|
|
169
|
+
f.locale === targetLocale &&
|
|
170
|
+
path.basename(f.path, path.extname(f.path)) === path.basename(sourceFile.path, path.extname(sourceFile.path)).replace(sourceLocale, targetLocale)
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
if (found) return found;
|
|
174
|
+
|
|
175
|
+
const sourceDirParts = path.dirname(sourceFile.path).split(path.sep);
|
|
176
|
+
const sourceFileBaseName = path.basename(sourceFile.path, path.extname(sourceFile.path));
|
|
177
|
+
|
|
178
|
+
// Check for corresponding file in subdirectories or parent directories
|
|
179
|
+
return targetFiles.find(f => {
|
|
180
|
+
if (f.locale !== targetLocale) return false;
|
|
181
|
+
|
|
182
|
+
// Handle cases where files are in different subdirectories
|
|
183
|
+
const targetDirParts = path.dirname(f.path).split(path.sep);
|
|
184
|
+
const targetFileBaseName = path.basename(f.path, path.extname(f.path));
|
|
185
|
+
|
|
186
|
+
if (
|
|
187
|
+
sourceFileBaseName === sourceLocale &&
|
|
188
|
+
targetFileBaseName === targetLocale &&
|
|
189
|
+
sourceDirParts.length === targetDirParts.length
|
|
190
|
+
) {
|
|
191
|
+
return true;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Nested directory structure
|
|
195
|
+
if (sourceDirParts.includes(sourceLocale) && targetDirParts.includes(targetLocale)) {
|
|
196
|
+
const sourceBasePath = sourceDirParts.slice(0, sourceDirParts.indexOf(sourceLocale)).join(path.sep);
|
|
197
|
+
const targetBasePath = targetDirParts.slice(0, targetDirParts.indexOf(targetLocale)).join(path.sep);
|
|
198
|
+
|
|
199
|
+
return sourceBasePath === targetBasePath &&
|
|
200
|
+
sourceFileBaseName === targetFileBaseName;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return false;
|
|
204
|
+
});
|
|
163
205
|
}
|
|
164
206
|
|
|
165
207
|
export function generateTargetPath(sourceFile, targetLocale, sourceLocale) {
|
|
@@ -195,7 +237,6 @@ export function generateTargetPath(sourceFile, targetLocale, sourceLocale) {
|
|
|
195
237
|
// construct the target path by replacing the locale in the filename only
|
|
196
238
|
const dirPath = path.dirname(sourceFile.path);
|
|
197
239
|
const fileName = path.basename(sourceFile.path);
|
|
198
|
-
// Use regex to match the exact locale string to avoid partial matches
|
|
199
240
|
const localeRegex = new RegExp(`\\b${sourceLocale}\\b`, 'g');
|
|
200
241
|
const newFileName = fileName.replace(localeRegex, targetLocale);
|
|
201
242
|
return path.join(dirPath, newFileName);
|