@localheroai/cli 0.0.2 → 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/README +98 -0
- package/package.json +63 -58
- package/src/api/auth.js +20 -11
- package/src/api/client.js +70 -28
- package/src/api/imports.js +15 -13
- package/src/api/projects.js +17 -17
- package/src/api/translations.js +50 -29
- package/src/cli.js +49 -42
- package/src/commands/init.js +436 -236
- package/src/commands/login.js +59 -48
- package/src/commands/sync.js +28 -0
- package/src/commands/translate.js +232 -247
- package/src/utils/auth.js +15 -15
- package/src/utils/config.js +115 -86
- package/src/utils/files.js +358 -116
- package/src/utils/git.js +64 -8
- package/src/utils/github.js +83 -47
- package/src/utils/import-service.js +111 -129
- package/src/utils/prompt-service.js +66 -50
- package/src/utils/sync-service.js +147 -0
- package/src/utils/translation-updater/common.js +44 -0
- package/src/utils/translation-updater/index.js +36 -0
- package/src/utils/translation-updater/json-handler.js +111 -0
- package/src/utils/translation-updater/yaml-handler.js +207 -0
- package/src/utils/translation-utils.js +278 -0
- package/src/utils/defaults.js +0 -7
- package/src/utils/helpers.js +0 -3
- package/src/utils/project-service.js +0 -11
- package/src/utils/translation-updater.js +0 -154
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { configService } from './config.js';
|
|
3
|
+
import { getUpdates } from '../api/translations.js';
|
|
4
|
+
import { updateTranslationFile, deleteKeysFromTranslationFile } from './translation-updater/index.js';
|
|
5
|
+
import { findTranslationFiles } from './files.js';
|
|
6
|
+
|
|
7
|
+
const MAX_PAGES = 20;
|
|
8
|
+
|
|
9
|
+
export const syncService = {
|
|
10
|
+
async checkForUpdates({ verbose = false } = {}) {
|
|
11
|
+
const config = await configService.getValidProjectConfig();
|
|
12
|
+
|
|
13
|
+
if (!config.projectId) {
|
|
14
|
+
throw new Error('Project not initialized. Please run `localhero init` first.');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const since = config.lastSyncedAt || new Date(0).toISOString();
|
|
18
|
+
|
|
19
|
+
if (verbose) {
|
|
20
|
+
console.log(chalk.blue(`Checking for updates since ${since}`));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
let allFiles = [];
|
|
24
|
+
let deletedKeys = [];
|
|
25
|
+
let currentPage = 1;
|
|
26
|
+
let hasMorePages = true;
|
|
27
|
+
|
|
28
|
+
while (hasMorePages && currentPage <= MAX_PAGES) {
|
|
29
|
+
const response = await getUpdates(config.projectId, { since, page: currentPage });
|
|
30
|
+
|
|
31
|
+
if (response.updates?.updated_keys?.length) {
|
|
32
|
+
allFiles = allFiles.concat(response.updates.updated_keys);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (response.updates?.deleted_keys?.length) {
|
|
36
|
+
deletedKeys = deletedKeys.concat(response.updates.deleted_keys);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (response.pagination) {
|
|
40
|
+
const { current_page, total_pages } = response.pagination;
|
|
41
|
+
hasMorePages = current_page < total_pages;
|
|
42
|
+
currentPage++;
|
|
43
|
+
|
|
44
|
+
if (verbose && hasMorePages) {
|
|
45
|
+
if (total_pages > MAX_PAGES) {
|
|
46
|
+
console.log(chalk.yellow(` ⚠️ Limiting to ${MAX_PAGES} pages out of ${total_pages} total`));
|
|
47
|
+
} else {
|
|
48
|
+
console.log(chalk.gray(` Fetching page ${currentPage} of ${total_pages}`));
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
} else {
|
|
52
|
+
hasMorePages = false;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (!allFiles.length && !deletedKeys.length) {
|
|
57
|
+
if (verbose) {
|
|
58
|
+
console.log(chalk.green('✓ All translations are up to date'));
|
|
59
|
+
}
|
|
60
|
+
return { hasUpdates: false };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
hasUpdates: true,
|
|
65
|
+
updates: {
|
|
66
|
+
updates: {
|
|
67
|
+
files: allFiles,
|
|
68
|
+
deleted_keys: deletedKeys
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
async applyUpdates(updates, { verbose = false } = {}) {
|
|
75
|
+
let totalUpdates = 0;
|
|
76
|
+
let totalDeleted = 0;
|
|
77
|
+
for (const file of updates.updates.files || []) {
|
|
78
|
+
for (const lang of file.languages) {
|
|
79
|
+
if (verbose) {
|
|
80
|
+
console.log(chalk.blue(`Updating ${lang.code} translations in ${file.path}`));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const translations = {};
|
|
84
|
+
for (const translation of lang.translations) {
|
|
85
|
+
translations[translation.key] = translation.value;
|
|
86
|
+
if (verbose) {
|
|
87
|
+
const displayValue = translation.value.length > 100 ? `${translation.value.slice(0, 100)}…` : translation.value;
|
|
88
|
+
console.log(chalk.gray(` ${translation.key} = ${displayValue}`));
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
await updateTranslationFile(file.path, translations, lang.code);
|
|
94
|
+
totalUpdates += Object.keys(translations).length;
|
|
95
|
+
} catch (error) {
|
|
96
|
+
console.error(chalk.yellow(`⚠️ Failed to update ${file.path}: ${error.message}`));
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
const deletedKeys = updates.updates.deleted_keys || [];
|
|
101
|
+
if (deletedKeys.length > 0) {
|
|
102
|
+
if (verbose) {
|
|
103
|
+
console.log(chalk.blue(`\nProcessing ${deletedKeys.length} deleted keys`));
|
|
104
|
+
}
|
|
105
|
+
const config = await configService.getValidProjectConfig();
|
|
106
|
+
const translationFiles = await findTranslationFiles(config, {
|
|
107
|
+
parseContent: false,
|
|
108
|
+
includeContent: false,
|
|
109
|
+
extractKeys: false,
|
|
110
|
+
verbose
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
if (verbose) {
|
|
114
|
+
console.log(chalk.blue(`Found ${translationFiles.length} translation files to check for deleted keys`));
|
|
115
|
+
}
|
|
116
|
+
const keysToDelete = deletedKeys.map(key => key.name);
|
|
117
|
+
for (const file of translationFiles) {
|
|
118
|
+
try {
|
|
119
|
+
if (verbose) {
|
|
120
|
+
console.log(chalk.blue(`Checking for deleted keys in ${file.path} (${file.locale})`));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const deletedFromFile = await deleteKeysFromTranslationFile(file.path, keysToDelete, file.locale);
|
|
124
|
+
|
|
125
|
+
if (deletedFromFile.length > 0) {
|
|
126
|
+
totalDeleted += deletedFromFile.length;
|
|
127
|
+
|
|
128
|
+
if (verbose) {
|
|
129
|
+
console.log(chalk.green(`✓ Deleted ${deletedFromFile.length} keys from ${file.path}`));
|
|
130
|
+
for (const key of deletedFromFile) {
|
|
131
|
+
console.log(chalk.gray(` - ${key}`));
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
} else if (verbose) {
|
|
135
|
+
console.log(chalk.gray(` No keys to delete in ${file.path}`));
|
|
136
|
+
}
|
|
137
|
+
} catch (error) {
|
|
138
|
+
console.error(chalk.yellow(`⚠️ Failed to delete keys from ${file.path}: ${error.message}`));
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
await configService.updateLastSyncedAt();
|
|
144
|
+
|
|
145
|
+
return { totalUpdates, totalDeleted };
|
|
146
|
+
}
|
|
147
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { promises as fs } from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
export const SPECIAL_CHARS_REGEX = /[:@#,[\]{}?|>&*!\n]/;
|
|
5
|
+
export const INTERPOLATION = '%{';
|
|
6
|
+
export const MAX_ARRAY_LENGTH = 1000;
|
|
7
|
+
|
|
8
|
+
export async function fileExists(filePath) {
|
|
9
|
+
try {
|
|
10
|
+
await fs.access(filePath);
|
|
11
|
+
return true;
|
|
12
|
+
} catch {
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function ensureDirectoryExists(filePath) {
|
|
18
|
+
const dir = path.dirname(filePath);
|
|
19
|
+
if (dir !== '.') {
|
|
20
|
+
await fs.mkdir(dir, { recursive: true });
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function tryParseJsonArray(value) {
|
|
25
|
+
if (typeof value !== 'string' || !value.startsWith('["') || !value.endsWith('"]')) {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
const parsed = JSON.parse(value);
|
|
31
|
+
if (!Array.isArray(parsed)) {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (parsed.length > MAX_ARRAY_LENGTH) {
|
|
36
|
+
console.warn(`Array length ${parsed.length} exceeds maximum allowed length of ${MAX_ARRAY_LENGTH}`);
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return parsed;
|
|
41
|
+
} catch {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import { fileExists, ensureDirectoryExists } from './common.js';
|
|
3
|
+
import { updateYamlFile, deleteKeysFromYamlFile } from './yaml-handler.js';
|
|
4
|
+
import { updateJsonFile, deleteKeysFromJsonFile } from './json-handler.js';
|
|
5
|
+
|
|
6
|
+
export async function updateTranslationFile(filePath, translations, languageCode = 'en') {
|
|
7
|
+
const fileExt = path.extname(filePath).slice(1).toLowerCase();
|
|
8
|
+
const result = {
|
|
9
|
+
updatedKeys: Object.keys(translations),
|
|
10
|
+
created: false
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
await ensureDirectoryExists(filePath);
|
|
14
|
+
|
|
15
|
+
if (fileExt === 'json') {
|
|
16
|
+
const jsonResult = await updateJsonFile(filePath, translations, languageCode);
|
|
17
|
+
return {
|
|
18
|
+
updatedKeys: result.updatedKeys,
|
|
19
|
+
created: jsonResult.created
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return updateYamlFile(filePath, translations, languageCode);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function deleteKeysFromTranslationFile(filePath, keysToDelete, languageCode = 'en') {
|
|
27
|
+
const exists = await fileExists(filePath);
|
|
28
|
+
if (!exists) {
|
|
29
|
+
return [];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const fileExt = path.extname(filePath).slice(1).toLowerCase();
|
|
33
|
+
return fileExt === 'json'
|
|
34
|
+
? deleteKeysFromJsonFile(filePath, keysToDelete, languageCode)
|
|
35
|
+
: deleteKeysFromYamlFile(filePath, keysToDelete, languageCode);
|
|
36
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { promises as fs } from 'fs';
|
|
2
|
+
import { detectJsonFormat, preserveJsonStructure } from '../files.js';
|
|
3
|
+
import { ensureDirectoryExists } from './common.js';
|
|
4
|
+
|
|
5
|
+
export async function updateJsonFile(filePath, translations, languageCode) {
|
|
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
|
+
};
|
|
14
|
+
|
|
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
|
+
}
|
|
29
|
+
|
|
30
|
+
let updatedContent;
|
|
31
|
+
|
|
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);
|
|
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
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export async function deleteKeysFromJsonFile(filePath, keysToDelete, languageCode) {
|
|
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
|
+
}
|
|
67
|
+
|
|
68
|
+
const deletedKeys = [];
|
|
69
|
+
|
|
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;
|
|
77
|
+
|
|
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;
|
|
83
|
+
}
|
|
84
|
+
parent = current;
|
|
85
|
+
keyInParent = key;
|
|
86
|
+
current = current[key];
|
|
87
|
+
}
|
|
88
|
+
|
|
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;
|
|
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
|
+
}
|
|
111
|
+
}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { promises as fs } from 'fs';
|
|
2
|
+
import yaml from 'yaml';
|
|
3
|
+
import { SPECIAL_CHARS_REGEX, INTERPOLATION, fileExists, tryParseJsonArray } from './common.js';
|
|
4
|
+
|
|
5
|
+
const NEEDS_QUOTES_REGEX = /[:,%{}[\]|><!&*?-]/;
|
|
6
|
+
const LINE_WIDTH = 80;
|
|
7
|
+
|
|
8
|
+
function detectYamlOptions(content) {
|
|
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;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const seqMatch = lines.find(line => /^\s*-\s+\S/.test(line));
|
|
28
|
+
if (seqMatch) {
|
|
29
|
+
options.indentSeq = /^\s+-\s+/.test(seqMatch);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return options;
|
|
33
|
+
}
|
|
34
|
+
|
|
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
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function shouldForceQuotes(str) {
|
|
47
|
+
if (typeof str !== 'string') return false;
|
|
48
|
+
|
|
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
|
+
}
|
|
54
|
+
|
|
55
|
+
return needsQuotes(str);
|
|
56
|
+
}
|
|
57
|
+
|
|
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';
|
|
63
|
+
}
|
|
64
|
+
return itemNode;
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
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
|
+
}
|
|
87
|
+
|
|
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
|
+
}
|
|
111
|
+
|
|
112
|
+
const lastKey = keys[keys.length - 1];
|
|
113
|
+
|
|
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
|
+
}
|
|
120
|
+
|
|
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
|
+
}
|
|
128
|
+
|
|
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
|
+
}
|
|
135
|
+
|
|
136
|
+
const node = yamlDoc.createNode(newValue);
|
|
137
|
+
if (needsQuotes(newValue)) {
|
|
138
|
+
node.type = 'QUOTE_DOUBLE';
|
|
139
|
+
}
|
|
140
|
+
current.set(lastKey, node);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export async function updateYamlFile(filePath, translations, languageCode) {
|
|
145
|
+
const { doc: yamlDoc, created, options } = await createYamlDocument(filePath);
|
|
146
|
+
|
|
147
|
+
await updateYamlTranslations(yamlDoc, translations, languageCode);
|
|
148
|
+
|
|
149
|
+
yamlDoc.options.indent = options.indent;
|
|
150
|
+
yamlDoc.options.indentSeq = options.indentSeq;
|
|
151
|
+
yamlDoc.options.lineWidth = LINE_WIDTH;
|
|
152
|
+
|
|
153
|
+
await fs.writeFile(filePath, yamlDoc.toString());
|
|
154
|
+
return {
|
|
155
|
+
updatedKeys: Object.keys(translations),
|
|
156
|
+
created
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export async function deleteKeysFromYamlFile(filePath, keysToDelete, languageCode) {
|
|
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
|
+
}
|
|
167
|
+
|
|
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;
|
|
184
|
+
}
|
|
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
|
+
}
|
|
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
|
+
}
|
|
207
|
+
}
|