handlebars-i18n-cli 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/.travis.yml +15 -0
- package/LICENSE +21 -0
- package/README.md +253 -0
- package/bin/i18n-collect +4 -0
- package/examples/generated/translations.json +145 -0
- package/examples/templates/sample-template.html +363 -0
- package/package.json +49 -0
- package/src/i18n-collect.js +561 -0
- package/test/handlebars-i18n-cli.test.js +174 -0
- package/test/test-assets/custom-func.html +10 -0
- package/test/test-assets/empty.html +9 -0
- package/test/test-assets/multiple.html +12 -0
- package/test/test-assets/simple.html +10 -0
- package/test/test-generated/test-12.json +8 -0
|
@@ -0,0 +1,561 @@
|
|
|
1
|
+
/*********************************************************************
|
|
2
|
+
* i18n-collect.js
|
|
3
|
+
*
|
|
4
|
+
* @author: Florian Walzel
|
|
5
|
+
* @date: 2022-10
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* $ i18n-collect <source> <target> <options...>
|
|
9
|
+
*
|
|
10
|
+
* valid options:
|
|
11
|
+
* --alphabetical || -a
|
|
12
|
+
* --dryRun || -dr
|
|
13
|
+
* --empty || -e
|
|
14
|
+
* --lng=de,fr,es,etc…
|
|
15
|
+
* --log || -l
|
|
16
|
+
* --separateLngFiles || -sf
|
|
17
|
+
* --translFunc=yourCustomFunctionName
|
|
18
|
+
* --update || -u*
|
|
19
|
+
*
|
|
20
|
+
* Copyright (c) 2020 Florian Walzel, MIT LICENSE
|
|
21
|
+
*
|
|
22
|
+
* Permission is hereby granted, free of charge, to any person
|
|
23
|
+
* obtaininga copy of this software and associated documentation
|
|
24
|
+
* files (the "Software"), to deal in the Software without restriction,
|
|
25
|
+
* including without limitation the rights to use, copy, modify, merge,
|
|
26
|
+
* publish, distribute, sublicense, and/or sell copies of the Software,
|
|
27
|
+
* and to permit persons to whom the Software is furnished to do so,
|
|
28
|
+
* subject to the following conditions:
|
|
29
|
+
*
|
|
30
|
+
* The above copyright notice and this permission notice shall be
|
|
31
|
+
* included in all copies or substantial portions of the Software.
|
|
32
|
+
*
|
|
33
|
+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
34
|
+
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
|
35
|
+
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
36
|
+
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
|
|
37
|
+
* BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
|
|
38
|
+
* ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
|
39
|
+
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
40
|
+
* THE SOFTWARE.
|
|
41
|
+
*
|
|
42
|
+
*********************************************************************/
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
'use strict';
|
|
46
|
+
|
|
47
|
+
/****************************************
|
|
48
|
+
* REQUIRE & DEFINE
|
|
49
|
+
****************************************/
|
|
50
|
+
|
|
51
|
+
const fs = require('fs')
|
|
52
|
+
const { promisify } = require('util')
|
|
53
|
+
const readFileAsync = promisify(fs.readFile)
|
|
54
|
+
const writeFileAsync = promisify(fs.writeFile)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
/****************************************
|
|
58
|
+
* FUNCTIONS
|
|
59
|
+
****************************************/
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Asynchronously read file
|
|
63
|
+
*
|
|
64
|
+
* @param file
|
|
65
|
+
* @returns {Promise<*>}
|
|
66
|
+
*/
|
|
67
|
+
async function readFile (file) {
|
|
68
|
+
try {
|
|
69
|
+
const data = await readFileAsync(file, 'utf8')
|
|
70
|
+
return data
|
|
71
|
+
}
|
|
72
|
+
catch (e) {
|
|
73
|
+
console.log('\x1b[31m%s\x1b[0m', `Error. Could not read ${file}`)
|
|
74
|
+
console.error(e)
|
|
75
|
+
return false
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Asynchronously write file in utf8 encoding
|
|
81
|
+
*
|
|
82
|
+
* @param file
|
|
83
|
+
* @param data
|
|
84
|
+
* @returns {Promise<boolean>}
|
|
85
|
+
*/
|
|
86
|
+
async function writeFile(file, data) {
|
|
87
|
+
try {
|
|
88
|
+
await writeFileAsync(file, data, 'utf8')
|
|
89
|
+
return true
|
|
90
|
+
}
|
|
91
|
+
catch (e) {
|
|
92
|
+
console.log('\x1b[31m%s\x1b[0m', `Error. Could not write ${file}`)
|
|
93
|
+
console.error(e)
|
|
94
|
+
return false
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Simple object check.
|
|
100
|
+
*
|
|
101
|
+
* @param item
|
|
102
|
+
* @returns {boolean}
|
|
103
|
+
*/
|
|
104
|
+
function isObject(item) {
|
|
105
|
+
return (item && typeof item === 'object' && ! Array.isArray(item))
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Conditionally removes a substring (file extension)
|
|
110
|
+
* from end of given string.
|
|
111
|
+
*
|
|
112
|
+
* @param str
|
|
113
|
+
* @param ending
|
|
114
|
+
* @returns {*|string[]}
|
|
115
|
+
*/
|
|
116
|
+
function sanitizeFileExt(str, ending='.json') {
|
|
117
|
+
return str.toLowerCase().endsWith(ending) ? str.slice(0, ending.length * (-1)) : str
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Log the help information to console
|
|
122
|
+
* @returns {boolean}
|
|
123
|
+
*/
|
|
124
|
+
function logHelp() {
|
|
125
|
+
console.log('\x1b[2m%s', 'Usage:')
|
|
126
|
+
console.log('i18n-collect <source> <target> <options...>')
|
|
127
|
+
console.log('')
|
|
128
|
+
console.log('<source> path to handlebars.js template file(s), glob pattern allowed')
|
|
129
|
+
console.log('<target> json file(s) to write result to')
|
|
130
|
+
console.log('')
|
|
131
|
+
console.log('<options>')
|
|
132
|
+
console.log('--alphabetical or -a will order the keys to the translation strings alphabetically in the json')
|
|
133
|
+
console.log(' default: keys in order of appearance as within the template(s)')
|
|
134
|
+
console.log('--dryRun or -dr will log the result(s) but not write out json file(s)')
|
|
135
|
+
console.log('--empty or -e will create empty value strings for the translations in the json')
|
|
136
|
+
console.log(' default: value strings contain current language and key name')
|
|
137
|
+
console.log('--lng=en,fr,es,… the languages you want to be generated')
|
|
138
|
+
console.log(' default: en')
|
|
139
|
+
console.log('--log or -l log final results to console')
|
|
140
|
+
console.log('--separateLngFiles write each language in a separate json file')
|
|
141
|
+
console.log(' or -sf default: all languages are written as arrays in one json file')
|
|
142
|
+
console.log('--translFunc=customName a custom name of the translation function used in the templates')
|
|
143
|
+
console.log(' default: __ like handlebars-i18n notation: {{__ keyToTranslate}}')
|
|
144
|
+
console.log('--update or -u updates existing json files(s) after changes made in template file(s)')
|
|
145
|
+
console.log('\x1b[0m')
|
|
146
|
+
return true
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* A collection of functions to extract and handle the
|
|
151
|
+
* strings between mustaches {{ ... }}
|
|
152
|
+
*
|
|
153
|
+
* @type {{str: string,
|
|
154
|
+
* removeFromBetween: ((function(*, *): boolean)|*),
|
|
155
|
+
* getSorted: (function(*, *, *=, *=): []),
|
|
156
|
+
* results: *[], getFromBetween: ((function(*, *): (string|boolean))|*),
|
|
157
|
+
* getAllResults: mustacheBetweens.getAllResults}}
|
|
158
|
+
*/
|
|
159
|
+
const mustacheBetweens = {
|
|
160
|
+
results : [ ],
|
|
161
|
+
str : '',
|
|
162
|
+
/**
|
|
163
|
+
* Returns a substring between an opening and
|
|
164
|
+
* a closing string sequence in a given string
|
|
165
|
+
*
|
|
166
|
+
* @param sub1
|
|
167
|
+
* @param sub2
|
|
168
|
+
* @returns {string|boolean}
|
|
169
|
+
*/
|
|
170
|
+
getFromBetween : function(sub1, sub2) {
|
|
171
|
+
if (this.str.indexOf(sub1) < 0 || this.str.indexOf(sub2) < 0)
|
|
172
|
+
return false
|
|
173
|
+
let SP = this.str.indexOf(sub1) + sub1.length,
|
|
174
|
+
string1 = this.str.substr(0, SP),
|
|
175
|
+
string2 = this.str.substr(SP),
|
|
176
|
+
TP = string1.length + string2.indexOf(sub2)
|
|
177
|
+
return this.str.substring(SP, TP)
|
|
178
|
+
},
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Removes a found sequence between an opening and
|
|
182
|
+
* a closing string from a a given string
|
|
183
|
+
*
|
|
184
|
+
* @param sub1
|
|
185
|
+
* @param sub2
|
|
186
|
+
* @returns {boolean}
|
|
187
|
+
*/
|
|
188
|
+
removeFromBetween : function(sub1, sub2) {
|
|
189
|
+
if (this.str.indexOf(sub1) < 0 || this.str.indexOf(sub2) < 0)
|
|
190
|
+
return false
|
|
191
|
+
let removal = sub1 + this.getFromBetween(sub1, sub2) + sub2
|
|
192
|
+
this.str = this.str.replace(removal,'')
|
|
193
|
+
},
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* gets all substrings of a string that are between an opening character sequence (sub1)
|
|
197
|
+
* and a closing sequence (sub2). The Result is stored in parent results array.
|
|
198
|
+
*
|
|
199
|
+
* @param sub1
|
|
200
|
+
* @param sub2
|
|
201
|
+
*/
|
|
202
|
+
getAllResults : function(sub1, sub2) {
|
|
203
|
+
// first check to see if we do have both substrings
|
|
204
|
+
if (this.str.indexOf(sub1) < 0 || this.str.indexOf(sub2) < 0)
|
|
205
|
+
return false
|
|
206
|
+
// find first result
|
|
207
|
+
let result = this.getFromBetween(sub1, sub2)
|
|
208
|
+
// replace multiple spaces by a single one, then trim and push it to the results array
|
|
209
|
+
this.results.push(result.replace(/ +(?= )/g,'').trim())
|
|
210
|
+
// remove the most recently found one from the string
|
|
211
|
+
this.removeFromBetween(sub1, sub2)
|
|
212
|
+
// recursion in case there are more substrings
|
|
213
|
+
if (this.str.indexOf(sub1) > -1 && this.str.indexOf(sub2) > -1)
|
|
214
|
+
this.getAllResults(sub1, sub2)
|
|
215
|
+
},
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
*
|
|
219
|
+
* @param string
|
|
220
|
+
* @param sub1
|
|
221
|
+
* @param sub2
|
|
222
|
+
* @returns {*}
|
|
223
|
+
*/
|
|
224
|
+
getSorted : function(string, translFuncName, sub1='{{', sub2='}}') {
|
|
225
|
+
this.str = string
|
|
226
|
+
this.getAllResults(sub1, sub2)
|
|
227
|
+
this.results =
|
|
228
|
+
this.results.filter(
|
|
229
|
+
(el) => {
|
|
230
|
+
return typeof el === 'string' && el.startsWith(`${translFuncName} `)
|
|
231
|
+
})
|
|
232
|
+
.map(
|
|
233
|
+
(el) => {
|
|
234
|
+
// remove leading translation function and explode string by space
|
|
235
|
+
let splited = el.replace(`${translFuncName} `, '').split(' ')
|
|
236
|
+
// remove quotation marks around key name in element 0 of array
|
|
237
|
+
splited[0] = splited[0]
|
|
238
|
+
.replace(/"/g, '')
|
|
239
|
+
.replace(/'/g, '')
|
|
240
|
+
// split remaining string in first element of array by dot (.) to get separate keys of a dot-notated object
|
|
241
|
+
let keys = splited[0].split('.')
|
|
242
|
+
// transformed is a container object for key
|
|
243
|
+
let transformed = { }
|
|
244
|
+
transformed.keys = keys
|
|
245
|
+
transformed.replacementVars = [ ]
|
|
246
|
+
// split following elements by '=' and preserve first element of split
|
|
247
|
+
for (let i = 1; i < splited.length; i++)
|
|
248
|
+
transformed.replacementVars[i-1] = splited[i].split('=')[0]
|
|
249
|
+
|
|
250
|
+
return transformed
|
|
251
|
+
})
|
|
252
|
+
return this.results
|
|
253
|
+
}
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Filter specific array so that all elements with redundant
|
|
258
|
+
* values keys are removed. Array looks like:
|
|
259
|
+
* [
|
|
260
|
+
* { keys: [ 'b' ], replacementVars: [] },
|
|
261
|
+
* { keys: [ 'c' ], replacementVars: [] },
|
|
262
|
+
* …
|
|
263
|
+
* ]
|
|
264
|
+
*
|
|
265
|
+
* @param arr
|
|
266
|
+
* @returns {*}
|
|
267
|
+
*/
|
|
268
|
+
const arrRmvDuplicateValues = (arr) => {
|
|
269
|
+
let seen = { }
|
|
270
|
+
return arr.filter((item) => {
|
|
271
|
+
return seen.hasOwnProperty(item.keys) ? false : seen[item.keys] = true
|
|
272
|
+
})
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Builds a nested object with key-value-pairs from a three-dimensional array.
|
|
278
|
+
*
|
|
279
|
+
* @param arr
|
|
280
|
+
* @param lang
|
|
281
|
+
* @param empty
|
|
282
|
+
* @returns {{}}
|
|
283
|
+
*/
|
|
284
|
+
function objectify (arr, lang='en', empty = false) {
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
*
|
|
288
|
+
* @param obj
|
|
289
|
+
* @param val
|
|
290
|
+
* @param arr
|
|
291
|
+
* @param pos
|
|
292
|
+
*/
|
|
293
|
+
function __iterateArr (obj, val, arr, pos) {
|
|
294
|
+
if (! obj.hasOwnProperty(arr[pos])) {
|
|
295
|
+
if (pos + 1 < arr.length) {
|
|
296
|
+
obj[arr[pos]] = { }
|
|
297
|
+
__iterateArr(obj[arr[pos]], val, arr, pos + 1)
|
|
298
|
+
}
|
|
299
|
+
else
|
|
300
|
+
obj[arr[pos]] = val;
|
|
301
|
+
}
|
|
302
|
+
else if (pos+1 < arr.length)
|
|
303
|
+
__iterateArr(obj[arr[pos]], val, arr, pos + 1);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Return a joined string form an array, where each element is
|
|
308
|
+
* wrapped in Mustaches {{ }}. ["a", "b", "c"] becomes "{{a}} {{b}} {{c}}"
|
|
309
|
+
*
|
|
310
|
+
* @param arr
|
|
311
|
+
* @param textBefore
|
|
312
|
+
* @returns {string}
|
|
313
|
+
*/
|
|
314
|
+
function __listTranslVariables(arr, textBefore= '') {
|
|
315
|
+
let str = '';
|
|
316
|
+
if (arr.length === 0)
|
|
317
|
+
return str;
|
|
318
|
+
for (let elem of arr)
|
|
319
|
+
str += `{{${elem}}} `;
|
|
320
|
+
return textBefore + str.slice(0, -1);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
let obj = { }
|
|
324
|
+
arr.forEach((el) => {
|
|
325
|
+
let prop;
|
|
326
|
+
if (empty)
|
|
327
|
+
prop = __listTranslVariables(el.replacementVars);
|
|
328
|
+
else
|
|
329
|
+
prop = `${lang} of ${el.keys.join('.') + __listTranslVariables(el.replacementVars, ' with variables ')}`
|
|
330
|
+
__iterateArr(obj, prop, el.keys, 0)
|
|
331
|
+
})
|
|
332
|
+
|
|
333
|
+
return obj
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Deep merge two objects whereby all properties of
|
|
338
|
+
* sources are kept and the target properties are added.
|
|
339
|
+
*
|
|
340
|
+
* @param target
|
|
341
|
+
* @param ...sources
|
|
342
|
+
*/
|
|
343
|
+
function mergeDeep(target, ...sources) {
|
|
344
|
+
if (! sources.length) return target;
|
|
345
|
+
const source = sources.shift();
|
|
346
|
+
|
|
347
|
+
if (isObject(target) && isObject(source)) {
|
|
348
|
+
for (const key in source) {
|
|
349
|
+
if (isObject(source[key])) {
|
|
350
|
+
if (! target[key])
|
|
351
|
+
Object.assign(target, {[key]: { }});
|
|
352
|
+
mergeDeep(target[key], source[key]);
|
|
353
|
+
} else
|
|
354
|
+
Object.assign(target, {[key]: source[key]});
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
return mergeDeep(target, ...sources);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* deep sort an array by the values stored
|
|
363
|
+
* in the "keys" property. Incoming array looks like this:
|
|
364
|
+
* [
|
|
365
|
+
* { keys: [ 'a' ], replacementVars: [] },
|
|
366
|
+
* { keys: [ 'b', 'a' ], replacementVars: [] },
|
|
367
|
+
* …
|
|
368
|
+
* ]
|
|
369
|
+
* @param arr
|
|
370
|
+
* @returns arr
|
|
371
|
+
*/
|
|
372
|
+
function deepSort(arr) {
|
|
373
|
+
// determine the longest array in keys properties
|
|
374
|
+
let depth = 0;
|
|
375
|
+
for (let inst of arr)
|
|
376
|
+
if (inst.keys.length > depth)
|
|
377
|
+
depth = inst.keys.length;
|
|
378
|
+
|
|
379
|
+
// iterate from longest to shortest and sort
|
|
380
|
+
for (let i = depth-1; i>=0; i--) {
|
|
381
|
+
arr = arr.sort((a, b) => {
|
|
382
|
+
if ( a.keys[i] !== undefined && b.keys[i] !== undefined )
|
|
383
|
+
return a.keys[i] > b.keys[i] ? -1 : 1;
|
|
384
|
+
else
|
|
385
|
+
return a.keys[i] !== undefined ? -1 : 1
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
// we get a descending array, so we invert it
|
|
389
|
+
return arr.reverse();
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
/****************************************
|
|
394
|
+
* EXPORT PUBLIC INTERFACE
|
|
395
|
+
****************************************/
|
|
396
|
+
|
|
397
|
+
exports.cli = async (argv) => {
|
|
398
|
+
|
|
399
|
+
// take in cli arguments from process argv
|
|
400
|
+
let args = [ ]
|
|
401
|
+
for (let i = 2; i < argv.length; i++)
|
|
402
|
+
args.push(argv[i])
|
|
403
|
+
|
|
404
|
+
// validation: error when no argument was given
|
|
405
|
+
if (args.length === 0)
|
|
406
|
+
throw new Error(`No arguments given. Please specify SOURCE and TARGET.
|
|
407
|
+
Call argument --help for further Information.`)
|
|
408
|
+
|
|
409
|
+
// only one argument was given
|
|
410
|
+
if (args.length === 1) {
|
|
411
|
+
// answer to argument 'help'
|
|
412
|
+
if (['help', '--help', '-h'].includes(args[0]))
|
|
413
|
+
return logHelp()
|
|
414
|
+
// error the missing second argument
|
|
415
|
+
else
|
|
416
|
+
throw new Error(`Missing second argument for TARGET.
|
|
417
|
+
Call argument --help for further Information.`)
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// register vars
|
|
421
|
+
let hndlbrKeys = [ ],
|
|
422
|
+
sources,
|
|
423
|
+
targetFileName,
|
|
424
|
+
targetFileNameSeparated,
|
|
425
|
+
translationFuncName,
|
|
426
|
+
pos = -1,
|
|
427
|
+
languages,
|
|
428
|
+
translObj,
|
|
429
|
+
outputObj;
|
|
430
|
+
|
|
431
|
+
// create an array "sources" by filtering out all options from args
|
|
432
|
+
sources = args.filter(
|
|
433
|
+
(el) => ! (el === 'help' || el.startsWith('--') || el.startsWith('-'))
|
|
434
|
+
)
|
|
435
|
+
|
|
436
|
+
// then removing last element as being the <target>
|
|
437
|
+
targetFileName = sources.pop()
|
|
438
|
+
|
|
439
|
+
// check for argument '--translFunc=someName' => a custom function name was given
|
|
440
|
+
args.forEach((el, key) => {
|
|
441
|
+
if (el.startsWith('--translFunc=')) return pos = key
|
|
442
|
+
})
|
|
443
|
+
translationFuncName = (pos >= 0) ? args[pos].split('=')[1] : '__'
|
|
444
|
+
|
|
445
|
+
// read in file(s) and join contents keeping only unique key names
|
|
446
|
+
for (let file of sources) {
|
|
447
|
+
console.log(`Now processing ${file}`)
|
|
448
|
+
let content = await readFile(file)
|
|
449
|
+
hndlbrKeys = hndlbrKeys.concat(
|
|
450
|
+
mustacheBetweens.getSorted(content, translationFuncName)
|
|
451
|
+
)
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// break if no strings for translation where found
|
|
455
|
+
if (hndlbrKeys.length === 0)
|
|
456
|
+
return console.log('No strings for translation found, no files written.')
|
|
457
|
+
|
|
458
|
+
// remove all duplicate value entries in position 'keys' of array hndlbrKeys
|
|
459
|
+
hndlbrKeys = arrRmvDuplicateValues(hndlbrKeys)
|
|
460
|
+
|
|
461
|
+
// evaluate argument '--alphabetical' for sorting
|
|
462
|
+
if (args.includes('--alphabetical') || args.includes('-a'))
|
|
463
|
+
hndlbrKeys = deepSort(hndlbrKeys)
|
|
464
|
+
|
|
465
|
+
// form an array of languages from argument '--lng='
|
|
466
|
+
languages = args.filter((el) => {
|
|
467
|
+
return el.startsWith('--lng=')
|
|
468
|
+
}).map((el) => {
|
|
469
|
+
return el.split('=')[1].split(',')
|
|
470
|
+
})[0]
|
|
471
|
+
|
|
472
|
+
// if no language parameter is passed set 'en' as default language
|
|
473
|
+
if (typeof languages === 'undefined') languages = ['en']
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
// WRITE TO ONE FILE PER LANGUAGE
|
|
477
|
+
// ------------------------------------------------
|
|
478
|
+
|
|
479
|
+
// evaluate argument '--separateLngFiles' to output each language in a separate file
|
|
480
|
+
if (args.includes('--separateLngFiles') || args.includes('-sf')) {
|
|
481
|
+
|
|
482
|
+
// if user entered argument for target ending with .json, remove it
|
|
483
|
+
targetFileName = sanitizeFileExt(targetFileName)
|
|
484
|
+
|
|
485
|
+
for (let lng of languages) {
|
|
486
|
+
// join file name per language such as myfile.de.json, myfile.en.json, ...
|
|
487
|
+
targetFileNameSeparated =
|
|
488
|
+
(targetFileName.startsWith('/') ? targetFileName.substring(1) : targetFileName) + '.' + lng + '.json'
|
|
489
|
+
|
|
490
|
+
// create output object per language and add keys in nested object form
|
|
491
|
+
outputObj = { }
|
|
492
|
+
outputObj[lng] = objectify(hndlbrKeys, lng, args.includes('--empty') || args.includes('-e'))
|
|
493
|
+
|
|
494
|
+
// if argument '--update' was given, existing files per language are read in, parsed,
|
|
495
|
+
// and the new translation Object is merged onto the existing translation
|
|
496
|
+
if (args.includes('--update') || args.includes('-u')) {
|
|
497
|
+
let existingTransl = await readFile(targetFileNameSeparated)
|
|
498
|
+
existingTransl = JSON.parse(existingTransl)
|
|
499
|
+
outputObj = mergeDeep(outputObj[lng], existingTransl)
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// convert output object to json with linebreaks and indenting of 2 spaces
|
|
503
|
+
const fileOutputJson = JSON.stringify(outputObj, null, 2)
|
|
504
|
+
|
|
505
|
+
// log output per language
|
|
506
|
+
if (args.includes('--log') || args.includes('-l')
|
|
507
|
+
|| args.includes('--dryRun') || args.includes('-dr'))
|
|
508
|
+
console.log(fileOutputJson)
|
|
509
|
+
|
|
510
|
+
// write files only if no --dryRun option was set
|
|
511
|
+
if (! args.includes('--dryRun') && ! args.includes('-dr'))
|
|
512
|
+
// write out the json to target file per language
|
|
513
|
+
if (await writeFile(targetFileNameSeparated, fileOutputJson))
|
|
514
|
+
console.log('\x1b[34m%s\x1b[0m', `Wrote language keys for '${lng}' to ${targetFileNameSeparated}`)
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
return (args.includes('--dryRun') || args.includes('-dr')) ?
|
|
518
|
+
console.log('\x1b[36m%s\x1b[0m', 'This was a dry run. No files witten.')
|
|
519
|
+
:
|
|
520
|
+
console.log('\x1b[32m%s\x1b[0m', `You’re good. All Done.`)
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// WRITE SINGLE FILE CONTAINING ALL LANGUAGES
|
|
524
|
+
// ------------------------------------------------
|
|
525
|
+
else {
|
|
526
|
+
// create object to hold the translations and create a key for every language
|
|
527
|
+
// add all handlebars translation keys to each language key as nested objects
|
|
528
|
+
translObj = {translations: { }}
|
|
529
|
+
languages.forEach((lng) => {
|
|
530
|
+
translObj.translations[lng] = objectify(hndlbrKeys, lng, args.includes('--empty') || args.includes('-e'))
|
|
531
|
+
})
|
|
532
|
+
|
|
533
|
+
// if argument '--update' was given, an existing file is read in, parsed,
|
|
534
|
+
// and the new translation Object is merged onto the existing translations
|
|
535
|
+
if (args.includes('--update') || args.includes('-u')) {
|
|
536
|
+
let existingTransl = await readFile(targetFileName)
|
|
537
|
+
existingTransl = JSON.parse(existingTransl)
|
|
538
|
+
outputObj = mergeDeep(translObj, existingTransl)
|
|
539
|
+
}
|
|
540
|
+
else
|
|
541
|
+
outputObj = translObj
|
|
542
|
+
|
|
543
|
+
// convert output object to json with linebreaks and indenting of 2 spaces
|
|
544
|
+
const fileOutputJson = JSON.stringify(outputObj,null, 2)
|
|
545
|
+
|
|
546
|
+
// log the final object to console if option '--log' or '--dryRun' was set
|
|
547
|
+
if (args.includes('--log') || args.includes('-l')
|
|
548
|
+
|| args.includes('--dryRun') || args.includes('-dr'))
|
|
549
|
+
console.log(fileOutputJson)
|
|
550
|
+
|
|
551
|
+
// exit if option '--dryRun' was set
|
|
552
|
+
if (args.includes('--dryRun') || args.includes('-dr')) {
|
|
553
|
+
console.log('\x1b[36m%s\x1b[0m', 'This was a dry run. No file witten.')
|
|
554
|
+
process.exit(0)
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
// write out the json to target file
|
|
558
|
+
if (await writeFile(targetFileName, fileOutputJson))
|
|
559
|
+
return console.log('\x1b[32m%s\x1b[0m', `Done and Ready! Your output was written to ${targetFileName}`)
|
|
560
|
+
}
|
|
561
|
+
}
|