@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 CHANGED
@@ -1,7 +1,12 @@
1
1
  {
2
2
  "name": "@localheroai/cli",
3
- "version": "0.0.3",
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.md",
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
 
@@ -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
- return JSON.parse(content);
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
- throw new Error(`Failed to parse ${format} file: ${error.message}`);
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 (verbose) {
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
  }
@@ -34,7 +34,13 @@ export const githubService = {
34
34
  on:
35
35
  pull_request:
36
36
  paths:
37
- ${translationPaths.map(p => `- "${p}"`).join('\n ')}
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: 18
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
- // Ensure we pass through all relevant fields from the API response
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
- try {
8
- let existingContent = {};
9
- let jsonFormat = 'nested';
10
- let hasLanguageWrapper = false;
11
- const result = {
12
- updatedKeys: Object.keys(translations),
13
- created: false
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
- let updatedContent;
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
- if (result.created) {
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
- await fs.writeFile(filePath, JSON.stringify(updatedContent, null, 2));
52
- return result;
53
- } catch (error) {
54
- throw new Error(`Failed to update JSON file ${filePath}: ${error.message}`);
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
- try {
60
- const content = await fs.readFile(filePath, 'utf8');
61
- let jsonContent = JSON.parse(content);
62
- let hasLanguageWrapper = false;
63
- let rootContent = jsonContent;
64
- if (jsonContent[languageCode] && typeof jsonContent[languageCode] === 'object') {
65
- hasLanguageWrapper = true;
66
- rootContent = jsonContent[languageCode];
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
- for (const keyPath of keysToDelete) {
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
- for (let i = 0; i < lastIndex; i++) {
80
- const key = keys[i];
81
- if (!current[key] || typeof current[key] !== 'object') {
82
- found = false;
83
- break;
84
- }
85
- parent = current;
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
- if (found) {
91
- const lastKey = keys[lastIndex];
92
- if (current[lastKey] !== undefined) {
93
- delete current[lastKey];
94
- deletedKeys.push(keyPath);
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
- await fs.writeFile(filePath, JSON.stringify(jsonContent, null, 2));
84
+ parent = current;
85
+ keyInParent = key;
86
+ current = current[key];
87
+ }
107
88
 
108
- return deletedKeys;
109
- } catch (error) {
110
- throw new Error(`Failed to delete keys from JSON file ${filePath}: ${error.message}`);
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
- const lines = content
7
- .split('\n')
8
- .filter(line => line.trim())
9
- .slice(0, 10);
10
-
11
- const options = {
12
- indent: 2,
13
- indentSeq: true
14
- };
15
-
16
- const indentMatch = lines.find(line => /^\s+\S/.test(line))?.match(/^(\s+)\S/);
17
- if (indentMatch) {
18
- options.indent = indentMatch[1].length;
19
- if (indentMatch[1].includes('\t')) {
20
- options.indent = 2;
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
- const seqMatch = lines.find(line => /^\s*-\s+\S/.test(line));
25
- if (seqMatch) {
26
- options.indentSeq = /^\s+-\s+/.test(seqMatch);
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
- return options;
32
+ return options;
30
33
  }
31
34
 
32
- function processArrayItems(array, yamlDoc) {
33
- return array.map(item => {
34
- const itemNode = yamlDoc.createNode(item);
35
- if (typeof item === 'string') {
36
- const needsQuotes = item.includes(INTERPOLATION) || SPECIAL_CHARS_REGEX.test(item);
37
- if (needsQuotes) {
38
- itemNode.type = 'QUOTE_DOUBLE';
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
- async function createYamlDocument(filePath) {
46
- const exists = await fileExists(filePath);
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
- const content = await fs.readFile(filePath, 'utf8');
55
- const options = detectYamlOptions(content);
56
- return {
57
- doc: yaml.parseDocument(content),
58
- created: false,
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
- async function updateYamlTranslations(yamlDoc, translations, languageCode) {
64
- if (!yamlDoc.contents) {
65
- yamlDoc.contents = yamlDoc.createNode({});
66
- }
55
+ return needsQuotes(str);
56
+ }
67
57
 
68
- const rootNode = yamlDoc.contents;
69
- if (!rootNode.has(languageCode)) {
70
- rootNode.set(languageCode, yamlDoc.createNode({}));
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
- const langNode = rootNode.get(languageCode);
74
-
75
- for (const [keyPath, newValue] of Object.entries(translations)) {
76
- const keys = keyPath.split('.');
77
- let current = langNode;
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
- for (let i = 0; i < keys.length - 1; i++) {
80
- const key = keys[i];
81
- if (!current.has(key)) {
82
- current.set(key, yamlDoc.createNode({}));
83
- }
84
- current = current.get(key);
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
- const lastKey = keys[keys.length - 1];
112
+ const lastKey = keys[keys.length - 1];
88
113
 
89
- if (Array.isArray(newValue)) {
90
- const arrayNode = new yaml.YAMLSeq();
91
- processArrayItems(newValue, yamlDoc).forEach(item => arrayNode.add(item));
92
- current.set(lastKey, arrayNode);
93
- continue;
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
- const array = tryParseJsonArray(newValue);
97
- if (array) {
98
- const arrayNode = new yaml.YAMLSeq();
99
- processArrayItems(array, yamlDoc).forEach(item => arrayNode.add(item));
100
- current.set(lastKey, arrayNode);
101
- continue;
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
- if (typeof newValue === 'string' && newValue.includes('\n')) {
105
- const scalar = new yaml.Scalar(newValue);
106
- scalar.type = 'BLOCK_LITERAL';
107
- current.set(lastKey, scalar);
108
- continue;
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
- const node = yamlDoc.createNode(newValue);
112
- if (typeof newValue === 'string' && (newValue.includes(INTERPOLATION) || SPECIAL_CHARS_REGEX.test(newValue))) {
113
- node.type = 'QUOTE_DOUBLE';
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
- const { doc: yamlDoc, created, options } = await createYamlDocument(filePath);
145
+ const { doc: yamlDoc, created, options } = await createYamlDocument(filePath);
121
146
 
122
- await updateYamlTranslations(yamlDoc, translations, languageCode);
147
+ await updateYamlTranslations(yamlDoc, translations, languageCode);
123
148
 
124
- yamlDoc.options.indent = options.indent;
125
- yamlDoc.options.indentSeq = options.indentSeq;
149
+ yamlDoc.options.indent = options.indent;
150
+ yamlDoc.options.indentSeq = options.indentSeq;
151
+ yamlDoc.options.lineWidth = LINE_WIDTH;
126
152
 
127
- await fs.writeFile(filePath, yamlDoc.toString());
128
- return {
129
- updatedKeys: Object.keys(translations),
130
- created
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
- try {
136
- const content = await fs.readFile(filePath, 'utf8');
137
- const yamlDoc = yaml.parseDocument(content);
138
- if (!yamlDoc.contents || !yamlDoc.contents.has(languageCode)) {
139
- return [];
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
- const langNode = yamlDoc.contents.get(languageCode);
143
- const deletedKeys = [];
144
-
145
- for (const keyPath of keysToDelete) {
146
- const keys = keyPath.split('.');
147
- const lastIndex = keys.length - 1;
148
- let current = langNode;
149
- let parent = null;
150
- let keyInParent = '';
151
- let found = true;
152
-
153
- for (let i = 0; i < lastIndex; i++) {
154
- const key = keys[i];
155
- if (!current.has(key)) {
156
- found = false;
157
- break;
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
- await fs.writeFile(filePath, yamlDoc.toString());
177
- return deletedKeys;
178
- } catch (error) {
179
- throw new Error(`Failed to delete keys from YAML file ${filePath}: ${error.message}`);
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
- return targetFiles.find(f =>
158
+ // First try exact directory matching (existing logic)
159
+ let found = targetFiles.find(f =>
159
160
  f.locale === targetLocale &&
160
- (path.dirname(f.path) === path.dirname(sourceFile.path) ||
161
- path.basename(f.path, path.extname(f.path)) === path.basename(sourceFile.path, path.extname(sourceFile.path)).replace(sourceLocale, targetLocale))
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);