handlebars-i18n-cli 1.0.4 → 2.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/.github/workflows/coveralls.yml +35 -18
- package/.github/workflows/node.js.yml +2 -2
- package/README.md +384 -52
- package/bin/i18n-collect +36 -2
- package/bin/i18n-deepl +71 -0
- package/de.json +72 -0
- package/en.json +5 -0
- package/fi.json +5 -0
- package/my.json +23 -0
- package/package.json +27 -16
- package/src/i18n-collect.js +162 -289
- package/src/i18n-deepl.js +304 -0
- package/src/index.js +4 -0
- package/test/handlebars-i18n-cli.test.mjs +472 -0
- package/tsconfig.json +20 -0
- package/types/i18n-collect.d.ts +5 -0
- package/types/i18n-collect.d.ts.map +1 -0
- package/types/i18n-deepl.d.ts +44 -0
- package/types/i18n-deepl.d.ts.map +1 -0
- package/types/index.d.ts +7 -0
- package/types/index.d.ts.map +1 -0
- package/want.md +18 -0
- package/src/deepl-free-api-key.json +0 -3
- package/src/deepl-pro-api-key.json +0 -3
- package/test/handlebars-i18n-cli.test.js +0 -174
- package/test/test-generated/test-12.json +0 -8
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
/*********************************************************************
|
|
2
|
+
* i18n-deepl.js
|
|
3
|
+
* @author: Florian Walzel
|
|
4
|
+
*
|
|
5
|
+
* Get an API Key:
|
|
6
|
+
* @link https://www.deepl.com/de/pro-checkout/account?productId=1200&yearly=false&trial=false
|
|
7
|
+
*
|
|
8
|
+
* The API Docs:
|
|
9
|
+
* @link https://www.deepl.com/docs-api/translate-text/multiple-sentences/
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
/****************************************
|
|
14
|
+
* IMPORT
|
|
15
|
+
****************************************/
|
|
16
|
+
|
|
17
|
+
import deepl from 'deepl-node';
|
|
18
|
+
import axios from 'axios';
|
|
19
|
+
import fst from 'async-file-tried';
|
|
20
|
+
import path from 'path';
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
/****************************************
|
|
24
|
+
* PRIVATE FUNCTIONS
|
|
25
|
+
****************************************/
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Flatten an object by recursively writing its values to an array
|
|
29
|
+
*
|
|
30
|
+
* @param obj
|
|
31
|
+
* @returns {*[]}
|
|
32
|
+
*/
|
|
33
|
+
function __flattenObj(obj) {
|
|
34
|
+
const result = [];
|
|
35
|
+
|
|
36
|
+
function recurse(curr) {
|
|
37
|
+
for (let key in curr) {
|
|
38
|
+
if (typeof curr[key] === 'object' && curr[key] !== null) {
|
|
39
|
+
recurse(curr[key]);
|
|
40
|
+
} else {
|
|
41
|
+
result.push(curr[key]);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
recurse(obj);
|
|
47
|
+
return result
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* The inverse operation of __flattenObject(): maps the values of a flat array back to a given object
|
|
52
|
+
*
|
|
53
|
+
* @param obj
|
|
54
|
+
* @param values
|
|
55
|
+
* @param childParam
|
|
56
|
+
* @returns {*}
|
|
57
|
+
*/
|
|
58
|
+
function __mapArrayToObj(obj, values, childParam) {
|
|
59
|
+
let index = 0;
|
|
60
|
+
|
|
61
|
+
function recurse(curr) {
|
|
62
|
+
for (let key in curr) {
|
|
63
|
+
if (typeof curr[key] === 'object' && curr[key] !== null) {
|
|
64
|
+
recurse(curr[key]);
|
|
65
|
+
} else {
|
|
66
|
+
curr[key] = values[index++][childParam];
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
recurse(obj);
|
|
72
|
+
return obj
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Traverse an object by a given path of sub-nodes and retrieve its value
|
|
76
|
+
*
|
|
77
|
+
* @param obj
|
|
78
|
+
* @param path | given like "my.path.to", will retrieve {my: {path: {to: "Value" }}}
|
|
79
|
+
* @returns {*}
|
|
80
|
+
*/
|
|
81
|
+
function __getValueFromPath(obj, path) {
|
|
82
|
+
// Split the path into an array of keys
|
|
83
|
+
const keys = path.split('.');
|
|
84
|
+
|
|
85
|
+
// Use reduce to traverse the object
|
|
86
|
+
return keys.reduce((acc, key) => {
|
|
87
|
+
if (acc && acc.hasOwnProperty(key)) {
|
|
88
|
+
return acc[key];
|
|
89
|
+
}
|
|
90
|
+
return undefined; // Return undefined if any key is not found
|
|
91
|
+
}, obj);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Traverse an object by a given path of sub-nodes and set a value
|
|
95
|
+
* at the given position
|
|
96
|
+
*
|
|
97
|
+
* @param obj
|
|
98
|
+
* @param path
|
|
99
|
+
* @param val
|
|
100
|
+
* @param langCode
|
|
101
|
+
* @private
|
|
102
|
+
*/
|
|
103
|
+
function __setNestedValue(obj, path, val, langCode) {
|
|
104
|
+
const keys = path.split('.');
|
|
105
|
+
|
|
106
|
+
function iterate(ob, keys, insert, lngCode, i = 0) {
|
|
107
|
+
let key = keys[i];
|
|
108
|
+
if (i < keys.length - 1) {
|
|
109
|
+
i = i+1;
|
|
110
|
+
iterate(ob[key], keys, insert, langCode, i);
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
ob[key][langCode] = (typeof ob[key][langCode] === 'object')
|
|
114
|
+
? {...ob[key][langCode], ...insert}
|
|
115
|
+
: ob[key][langCode] = insert;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
iterate(obj, keys, val, langCode);
|
|
120
|
+
return true
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
/****************************************
|
|
125
|
+
* PUBLIC INTERFACE
|
|
126
|
+
****************************************/
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Write th DeepL auth key to .env file
|
|
130
|
+
*
|
|
131
|
+
* @param key
|
|
132
|
+
* @param path
|
|
133
|
+
* @returns {Promise<boolean>}
|
|
134
|
+
*/
|
|
135
|
+
async function setAuthKey(key, path='./') {
|
|
136
|
+
if (typeof key !== 'string' || key === '')
|
|
137
|
+
throw new Error('Provided argument is not a valid DeepL auth key.');
|
|
138
|
+
const file = '.env';
|
|
139
|
+
let [res, err] = await fst.writeFile([path, file], `export DEEPL_AUTH=${key}`);
|
|
140
|
+
if (err) {
|
|
141
|
+
throw new Error(`Failed to write file ${file}`);
|
|
142
|
+
}
|
|
143
|
+
return true;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Function to fetch supported languages from the DeepL API
|
|
148
|
+
*
|
|
149
|
+
* @param authKey
|
|
150
|
+
* @returns {Promise<*>}
|
|
151
|
+
*/
|
|
152
|
+
async function getSupportedLanguages(authKey) {
|
|
153
|
+
if (typeof authKey !== 'string' || authKey === '')
|
|
154
|
+
throw new Error('Invalid argument authKey provided.');
|
|
155
|
+
try {
|
|
156
|
+
const response = await axios.get('https://api-free.deepl.com/v2/languages', {
|
|
157
|
+
params: {
|
|
158
|
+
auth_key: authKey,
|
|
159
|
+
type: 'target'
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
return response.data;
|
|
163
|
+
} catch (error) {
|
|
164
|
+
console.error('Error fetching supported languages:', error.response ? error.response.data : error.message);
|
|
165
|
+
throw error;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Translate a string or an array of strings using the DeepL API
|
|
171
|
+
*
|
|
172
|
+
* @param authKey
|
|
173
|
+
* @param texts
|
|
174
|
+
* @param sourceLang
|
|
175
|
+
* @param targetLang
|
|
176
|
+
* @param options
|
|
177
|
+
* @returns {Promise<TextResult|TextResult[]>}
|
|
178
|
+
*/
|
|
179
|
+
async function translateTexts(authKey, texts, sourceLang, targetLang, options) {
|
|
180
|
+
if (typeof authKey !== 'string' || authKey === '')
|
|
181
|
+
throw new Error('Invalid argument authKey provided.');
|
|
182
|
+
const translator = new deepl.Translator(authKey);
|
|
183
|
+
let [res, err] =
|
|
184
|
+
await fst.asyncHandler(() => translator.translateText(texts, sourceLang, targetLang, options));
|
|
185
|
+
if (err)
|
|
186
|
+
throw (err);
|
|
187
|
+
return res;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
/** read a json file, translate it with the Deepl API, write the result as json file
|
|
192
|
+
*
|
|
193
|
+
* @param authKey
|
|
194
|
+
* @param JsonSrc
|
|
195
|
+
* @param JsonTarget
|
|
196
|
+
* @param targetLangCode
|
|
197
|
+
* @param sourceNested
|
|
198
|
+
* @param sourceLangCode
|
|
199
|
+
* @param log
|
|
200
|
+
* @param dryRun
|
|
201
|
+
* @param deeplOpts
|
|
202
|
+
* @returns {Promise<boolean>}
|
|
203
|
+
*/
|
|
204
|
+
async function translateToJSON(
|
|
205
|
+
authKey,
|
|
206
|
+
JsonSrc,
|
|
207
|
+
JsonTarget,
|
|
208
|
+
sourceLangCode,
|
|
209
|
+
targetLangCode,
|
|
210
|
+
deeplOpts,
|
|
211
|
+
sourceNested,
|
|
212
|
+
log,
|
|
213
|
+
dryRun) {
|
|
214
|
+
|
|
215
|
+
// read the json source
|
|
216
|
+
let [srcObj, err] = await fst.readJson(JsonSrc);
|
|
217
|
+
if (err) {
|
|
218
|
+
console.error(`Unable to read file: ${file}`);
|
|
219
|
+
throw err;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// make a copy of srcObj to avoid circular references
|
|
223
|
+
const modifiedObj = JSON.parse(JSON.stringify(srcObj));
|
|
224
|
+
|
|
225
|
+
// define variable to hold the parsed JSON
|
|
226
|
+
let srcObjParsed;
|
|
227
|
+
|
|
228
|
+
// extract the nested key if exists
|
|
229
|
+
if (typeof sourceNested === 'string' && sourceNested !== '') {
|
|
230
|
+
const subEntry = __getValueFromPath(srcObj, sourceNested);
|
|
231
|
+
if (!subEntry)
|
|
232
|
+
throw new Error(`The nested key "${sourceNested}" does not exist in JSON file "${JsonSrc}"`);
|
|
233
|
+
srcObjParsed = subEntry;
|
|
234
|
+
}
|
|
235
|
+
// if not, the source object is the parsed object
|
|
236
|
+
else
|
|
237
|
+
srcObjParsed = srcObj;
|
|
238
|
+
|
|
239
|
+
// in key destination access the child key with the source language code,
|
|
240
|
+
// otherwise assume we are already in the key with the source lang code
|
|
241
|
+
const srcObjPart = (srcObjParsed[sourceLangCode])
|
|
242
|
+
? srcObjParsed[sourceLangCode]
|
|
243
|
+
: srcObjParsed;
|
|
244
|
+
|
|
245
|
+
// flatten the resulting object to an array
|
|
246
|
+
const translValues = __flattenObj(srcObjPart);
|
|
247
|
+
|
|
248
|
+
// run translation array against DeepL API
|
|
249
|
+
const translRes = await translateTexts(authKey, translValues, sourceLangCode, targetLangCode, deeplOpts);
|
|
250
|
+
|
|
251
|
+
// re-build object structure from array
|
|
252
|
+
const translObj = __mapArrayToObj(srcObjPart, translRes, 'text');
|
|
253
|
+
|
|
254
|
+
// declare the object we are going to write out, holding the result
|
|
255
|
+
let resultObj;
|
|
256
|
+
|
|
257
|
+
// check if source and target are identical either as string, or resolve in the same file
|
|
258
|
+
if (JsonSrc === JsonTarget
|
|
259
|
+
|| (await fst.exists(JsonTarget) &&
|
|
260
|
+
fst.realpath(path.resolve(JsonSrc)) === fst.realpath(path.resolve(JsonTarget)))) {
|
|
261
|
+
|
|
262
|
+
// if the content comes from a nested source
|
|
263
|
+
if (typeof sourceNested === 'string' && sourceNested !== '') {
|
|
264
|
+
// ... traverse in the object and insert or merge the translation
|
|
265
|
+
__setNestedValue(modifiedObj, sourceNested, translObj, targetLangCode);
|
|
266
|
+
} else {
|
|
267
|
+
// ... if not, see if the target node exists
|
|
268
|
+
(modifiedObj[targetLangCode])
|
|
269
|
+
? Object.assign(modifiedObj[targetLangCode], translObj) // ... and merge data with existing prop
|
|
270
|
+
: modifiedObj[targetLangCode] = translObj; // ... else set a new prop
|
|
271
|
+
}
|
|
272
|
+
resultObj = modifiedObj;
|
|
273
|
+
} else {
|
|
274
|
+
// error if the target file name exists
|
|
275
|
+
if (await fst.exists(JsonTarget))
|
|
276
|
+
throw new Error(`The target file "${JsonTarget}" already exists.
|
|
277
|
+
Please prompt a different file name or remove the existing file.`);
|
|
278
|
+
// ... if ok, set the resulting object
|
|
279
|
+
resultObj = translObj;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// log if requested
|
|
283
|
+
if (log || dryRun)
|
|
284
|
+
console.log(resultObj);
|
|
285
|
+
|
|
286
|
+
// write out result if it is not a dry run
|
|
287
|
+
if (!dryRun) {
|
|
288
|
+
const [res, err] = await fst.writeJson(JsonTarget, resultObj);
|
|
289
|
+
if (err) {
|
|
290
|
+
console.error(`Unable to write file: ${JsonTarget}`);
|
|
291
|
+
throw err;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
return true
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Export the functions
|
|
299
|
+
export {
|
|
300
|
+
setAuthKey,
|
|
301
|
+
getSupportedLanguages,
|
|
302
|
+
translateTexts,
|
|
303
|
+
translateToJSON
|
|
304
|
+
};
|
package/src/index.js
ADDED