adapt-project 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/.github/workflows/releases.yml +32 -0
- package/.github/workflows/standardjs.yml +13 -0
- package/.github/workflows/tests.yml +13 -0
- package/README.md +168 -0
- package/eslint.config.js +10 -0
- package/index.js +13 -0
- package/lib/Data.js +185 -0
- package/lib/Framework.js +295 -0
- package/lib/JSONFile.js +104 -0
- package/lib/JSONFileItem.js +27 -0
- package/lib/Plugins.js +82 -0
- package/lib/Schemas.js +208 -0
- package/lib/Translate.js +495 -0
- package/lib/data/Language.js +301 -0
- package/lib/data/LanguageFile.js +35 -0
- package/lib/plugins/Plugin.js +143 -0
- package/lib/schema/ExtensionSchema.js +26 -0
- package/lib/schema/GlobalsSchema.js +65 -0
- package/lib/schema/ModelSchema.js +45 -0
- package/lib/schema/ModelSchemas.js +41 -0
- package/lib/schema/Schema.js +191 -0
- package/package.json +54 -0
package/lib/Translate.js
ADDED
|
@@ -0,0 +1,495 @@
|
|
|
1
|
+
import path from 'path'
|
|
2
|
+
import _ from 'lodash'
|
|
3
|
+
import fs from 'fs-extra'
|
|
4
|
+
import csv from 'csv'
|
|
5
|
+
import { XMLParser } from 'fast-xml-parser'
|
|
6
|
+
import async from 'async'
|
|
7
|
+
import globs from 'globs'
|
|
8
|
+
import jschardet from 'jschardet'
|
|
9
|
+
import iconv from 'iconv-lite'
|
|
10
|
+
import Data from './Data.js'
|
|
11
|
+
import Schemas from './Schemas.js'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @typedef {import('./Framework')} Framework
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Pulls together schemas and data to enable both the export and import of data item attributes
|
|
19
|
+
* marked by the schemas as translatable.
|
|
20
|
+
*/
|
|
21
|
+
class Translate {
|
|
22
|
+
/**
|
|
23
|
+
* @param {Object} options
|
|
24
|
+
* @param {Framework} options.framework
|
|
25
|
+
* @param {function} options.includedFilter
|
|
26
|
+
* @param {string} options.masterLang
|
|
27
|
+
* @param {string} options.targetLang
|
|
28
|
+
* @param {string} options.format
|
|
29
|
+
* @param {string} options.csvDelimiter
|
|
30
|
+
* @param {boolean} options.shouldReplaceExisting
|
|
31
|
+
* @param {string} options.jsonext
|
|
32
|
+
* @param {string} options.sourcePath
|
|
33
|
+
* @param {string} options.languagePath
|
|
34
|
+
* @param {string} options.outputPath
|
|
35
|
+
* @param {boolean} options.isTest
|
|
36
|
+
* @param {function} options.log
|
|
37
|
+
*/
|
|
38
|
+
constructor ({
|
|
39
|
+
framework = null,
|
|
40
|
+
includedFilter = function () { return true },
|
|
41
|
+
masterLang = 'en',
|
|
42
|
+
targetLang = null,
|
|
43
|
+
format = 'csv',
|
|
44
|
+
csvDelimiter = null,
|
|
45
|
+
shouldReplaceExisting = false,
|
|
46
|
+
jsonext = 'json',
|
|
47
|
+
sourcePath = '',
|
|
48
|
+
languagePath = path.join(process.cwd(), 'languagefiles'),
|
|
49
|
+
outputPath = '',
|
|
50
|
+
courseDir = 'course',
|
|
51
|
+
useOutputData = false,
|
|
52
|
+
isTest = false,
|
|
53
|
+
log = console.log,
|
|
54
|
+
warn = console.warn
|
|
55
|
+
} = {}) {
|
|
56
|
+
/** @type {Framework} */
|
|
57
|
+
this.framework = framework
|
|
58
|
+
/** @type {function} */
|
|
59
|
+
this.includedFilter = includedFilter
|
|
60
|
+
/** @type {string} */
|
|
61
|
+
this.masterLang = masterLang
|
|
62
|
+
/** @type {string} */
|
|
63
|
+
this.targetLang = targetLang
|
|
64
|
+
// format can be raw, json or csv
|
|
65
|
+
/** @type {string} */
|
|
66
|
+
this.format = format
|
|
67
|
+
/** @type {string} */
|
|
68
|
+
this.csvDelimiter = csvDelimiter
|
|
69
|
+
/** @type {boolean} */
|
|
70
|
+
this.shouldReplaceExisting = shouldReplaceExisting
|
|
71
|
+
/** @type {string} */
|
|
72
|
+
this.jsonext = jsonext
|
|
73
|
+
/** @type {string} */
|
|
74
|
+
this.sourcePath = sourcePath.replace(/\\/g, '/')
|
|
75
|
+
/** @type {string} */
|
|
76
|
+
this.outputPath = outputPath.replace(/\\/g, '/')
|
|
77
|
+
/** @type {string} */
|
|
78
|
+
this.courseDir = courseDir
|
|
79
|
+
/** @type {Framework} */
|
|
80
|
+
this.useOutputData = useOutputData
|
|
81
|
+
/** @type {string} */
|
|
82
|
+
this.languagePath = languagePath.replace(/\\/g, '/')
|
|
83
|
+
/** @type {Data} */
|
|
84
|
+
this.data = null
|
|
85
|
+
/** @type {boolean} */
|
|
86
|
+
this.isTest = isTest
|
|
87
|
+
/** @type {function} */
|
|
88
|
+
this.log = log
|
|
89
|
+
/** @type {function} */
|
|
90
|
+
this.warn = warn
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** @returns {Translate} */
|
|
94
|
+
load () {
|
|
95
|
+
this.data = new Data({
|
|
96
|
+
framework: this.framework,
|
|
97
|
+
sourcePath: this.useOutputData ? this.outputPath : this.sourcePath,
|
|
98
|
+
courseDir: this.courseDir,
|
|
99
|
+
jsonext: this.jsonext,
|
|
100
|
+
trackingIdType: this.framework.trackingIdType,
|
|
101
|
+
log: this.log
|
|
102
|
+
})
|
|
103
|
+
this.data.load()
|
|
104
|
+
return this
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Produces a single JSON file or a series of CSV files representing all of the
|
|
109
|
+
* files and file items in the data structure.
|
|
110
|
+
* @returns {Translate}
|
|
111
|
+
*/
|
|
112
|
+
async export () {
|
|
113
|
+
const schemas = new Schemas({ framework: this.framework, includedFilter: this.includedFilter, sourcePath: this.sourcePath })
|
|
114
|
+
schemas.load()
|
|
115
|
+
|
|
116
|
+
const exportTextData = []
|
|
117
|
+
|
|
118
|
+
// collection translatable texts
|
|
119
|
+
const language = this.data.getLanguage(this.masterLang)
|
|
120
|
+
language.getAllFileItems().forEach(({ file, item }) => {
|
|
121
|
+
const applicableSchemas = schemas.getSchemasForModelJSON(item)
|
|
122
|
+
const translatablePaths = applicableSchemas.getTranslatablePaths()
|
|
123
|
+
|
|
124
|
+
function recursiveJSONProcess (data, level, path, lookupPath, id, file, component) {
|
|
125
|
+
if (level === 0) {
|
|
126
|
+
// at the root
|
|
127
|
+
id = Object.prototype.hasOwnProperty.call(data, '_id') ? data._id : null
|
|
128
|
+
component = Object.prototype.hasOwnProperty.call(data, '_component') ? data._component : null
|
|
129
|
+
}
|
|
130
|
+
if (Array.isArray(data)) {
|
|
131
|
+
for (let i = 0; i < data.length; i++) {
|
|
132
|
+
recursiveJSONProcess(data[i], level += 1, path + i + '/', lookupPath, id, file, component)
|
|
133
|
+
}
|
|
134
|
+
return
|
|
135
|
+
}
|
|
136
|
+
if (typeof data === 'object') {
|
|
137
|
+
for (const attribute in data) {
|
|
138
|
+
recursiveJSONProcess(data[attribute], level += 1, path + attribute + '/', lookupPath + attribute + '/', id, file, component)
|
|
139
|
+
}
|
|
140
|
+
return
|
|
141
|
+
}
|
|
142
|
+
if (data && translatablePaths.includes(lookupPath)) {
|
|
143
|
+
exportTextData.push({
|
|
144
|
+
file,
|
|
145
|
+
id,
|
|
146
|
+
path,
|
|
147
|
+
value: data
|
|
148
|
+
})
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const filename = path.parse(file.path).name.split('.')[0]
|
|
153
|
+
recursiveJSONProcess(item, 0, '/', '/', null, filename, null)
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
// maintain order with original translate tasks
|
|
157
|
+
const typeSortLevel = {
|
|
158
|
+
course: 1,
|
|
159
|
+
contentObjects: 2,
|
|
160
|
+
articles: 3,
|
|
161
|
+
blocks: 4,
|
|
162
|
+
components: 5
|
|
163
|
+
}
|
|
164
|
+
exportTextData.sort((a, b) => {
|
|
165
|
+
const typeSort = ((typeSortLevel[a.file] || 100) - (typeSortLevel[b.file] || 100))
|
|
166
|
+
return typeSort || a.id.length - b.id.length || a.id.localeCompare(b.id)
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
// output based upon format options
|
|
170
|
+
const outputFolder = path.join(this.languagePath, this.masterLang)
|
|
171
|
+
fs.mkdirpSync(outputFolder)
|
|
172
|
+
|
|
173
|
+
if (this.format === 'json' || this.format === 'raw') {
|
|
174
|
+
const filePath = path.join(outputFolder, 'export.json')
|
|
175
|
+
this.log(`Exporting json to ${filePath}`)
|
|
176
|
+
fs.writeJSONSync(filePath, exportTextData, { spaces: 2 })
|
|
177
|
+
return this
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (['xliff', 'xlf'].includes(this.format)) {
|
|
181
|
+
// create csv for each file
|
|
182
|
+
const outputGroupedByFile = exportTextData.reduce((prev, current) => {
|
|
183
|
+
if (!Object.prototype.hasOwnProperty.call(prev, current.file)) {
|
|
184
|
+
prev[current.file] = []
|
|
185
|
+
}
|
|
186
|
+
prev[current.file].push(current)
|
|
187
|
+
return prev
|
|
188
|
+
}, {})
|
|
189
|
+
|
|
190
|
+
// xliff 2.0
|
|
191
|
+
// const output = `<?xml version="1.0" encoding="UTF-8"?>
|
|
192
|
+
// <xliff xmlns="urn:oasis:names:tc:xliff:document:2.0" version="2.0" srcLang="${this.masterLang}" trgLang="${this.masterLang}">
|
|
193
|
+
// ${Object.entries(outputGroupedByFile).map(([fileName, entries]) => {
|
|
194
|
+
// return ` <file id="${fileName}">
|
|
195
|
+
// ${entries.map(item => {
|
|
196
|
+
// const value = /[<>&"'/]/.test(item.value)
|
|
197
|
+
// ? `<![CDATA[${item.value}]]>`
|
|
198
|
+
// : item.value;
|
|
199
|
+
// return ` <unit id="${item.id}${item.path}">
|
|
200
|
+
// <segment>
|
|
201
|
+
// <source xml:space="preserve">${value}</source>
|
|
202
|
+
// <target xml:space="preserve">${value}</target>
|
|
203
|
+
// </segment>
|
|
204
|
+
// </unit>
|
|
205
|
+
// `;
|
|
206
|
+
// }).filter(Boolean).join('')} </file>
|
|
207
|
+
// `;
|
|
208
|
+
// }).join('')}</xliff>`;
|
|
209
|
+
|
|
210
|
+
// xliff 1.2
|
|
211
|
+
const output = `<?xml version="1.0" encoding="UTF-8"?>
|
|
212
|
+
<xliff version="1.2">
|
|
213
|
+
${Object.entries(outputGroupedByFile).map(([fileName, entries]) => {
|
|
214
|
+
return ` <file original="${fileName}" source-language="${this.masterLang}" target-language="${this.masterLang}"><body>
|
|
215
|
+
${entries.map(item => {
|
|
216
|
+
const value = /[<>&"'/]/.test(item.value)
|
|
217
|
+
? `<![CDATA[${item.value}]]>`
|
|
218
|
+
: item.value
|
|
219
|
+
return ` <trans-unit id="${item.id}${item.path}">
|
|
220
|
+
<source>${value}</source>
|
|
221
|
+
<target>${value}</target>
|
|
222
|
+
</trans-unit>
|
|
223
|
+
`
|
|
224
|
+
}).filter(Boolean).join('')} </body></file>
|
|
225
|
+
`
|
|
226
|
+
}).join('')}</xliff>`
|
|
227
|
+
const filePath = path.join(outputFolder, 'source.xlf')
|
|
228
|
+
this.log(`Exporting xliff to ${filePath}`)
|
|
229
|
+
fs.writeFileSync(filePath, `${output}`)
|
|
230
|
+
return this
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// create csv for each file
|
|
234
|
+
const outputGroupedByFile = exportTextData.reduce((prev, current) => {
|
|
235
|
+
if (!Object.prototype.hasOwnProperty.call(prev, current.file)) {
|
|
236
|
+
prev[current.file] = []
|
|
237
|
+
}
|
|
238
|
+
prev[current.file].push([`${current.file}/${current.id}${current.path}`, current.value])
|
|
239
|
+
return prev
|
|
240
|
+
}, {})
|
|
241
|
+
|
|
242
|
+
const csvOptions = {
|
|
243
|
+
quotedString: true,
|
|
244
|
+
delimiter: this.csvDelimiter || ','
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const fileNames = Object.keys(outputGroupedByFile)
|
|
248
|
+
await async.each(fileNames, (name, done) => {
|
|
249
|
+
csv.stringify(outputGroupedByFile[name], csvOptions, (error, output) => {
|
|
250
|
+
if (error) {
|
|
251
|
+
return done(new Error('Error saving CSV files.'))
|
|
252
|
+
}
|
|
253
|
+
const filePath = path.join(outputFolder, `${name}.csv`)
|
|
254
|
+
this.log(`Exporting csv to ${filePath}`)
|
|
255
|
+
fs.writeFileSync(filePath, `\ufeff${output}`)
|
|
256
|
+
done(null)
|
|
257
|
+
})
|
|
258
|
+
})
|
|
259
|
+
|
|
260
|
+
return this
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Imports a single JSON file or multiple CSV files to replace values in the
|
|
265
|
+
* existing data file items.
|
|
266
|
+
* @returns {Translate}
|
|
267
|
+
*/
|
|
268
|
+
async import () {
|
|
269
|
+
if (this.isTest) {
|
|
270
|
+
this.log('!TEST IMPORT, not changing data.')
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// check that a targetLang has been specified
|
|
274
|
+
if (!this.targetLang) {
|
|
275
|
+
const err = new Error('Target language option is missing. ')
|
|
276
|
+
err.number = 10001
|
|
277
|
+
throw err
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// check input folder exists
|
|
281
|
+
const inputFolder = path.join(this.languagePath, this.targetLang)
|
|
282
|
+
if (!fs.existsSync(inputFolder) || !fs.statSync(inputFolder).isDirectory()) {
|
|
283
|
+
const err = new Error(`Folder does not exist. ${inputFolder}`)
|
|
284
|
+
err.number = 10002
|
|
285
|
+
throw err
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// auto-detect format if not specified
|
|
289
|
+
let format = this.format
|
|
290
|
+
if (!format) {
|
|
291
|
+
const filePaths = globs.sync([`${inputFolder}/*.*`])
|
|
292
|
+
const uniqueFileExtensions = _.uniq(filePaths.map(filePath => path.extname(filePath).slice(1)))
|
|
293
|
+
if (uniqueFileExtensions.length !== 1) {
|
|
294
|
+
throw new Error(`Format autodetection failed, ${uniqueFileExtensions.length} file types found.`)
|
|
295
|
+
}
|
|
296
|
+
format = uniqueFileExtensions[0]
|
|
297
|
+
switch (format) {
|
|
298
|
+
case 'xlf':
|
|
299
|
+
case 'xliff':
|
|
300
|
+
case 'csv':
|
|
301
|
+
case 'json':
|
|
302
|
+
this.log(`Format autodetected as ${format}`)
|
|
303
|
+
break
|
|
304
|
+
default:
|
|
305
|
+
throw new Error(`Format of the language file is not supported: ${format}`)
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if (format === 'xliff') format = 'xlf'
|
|
310
|
+
|
|
311
|
+
// discover import files
|
|
312
|
+
const langFiles = globs.sync([`${inputFolder}/*.${format}`])
|
|
313
|
+
if (langFiles.length === 0) {
|
|
314
|
+
throw new Error(`No languagefiles found to process in folder ${inputFolder}`)
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// copy master language files to target language directory if needed
|
|
318
|
+
if (this.targetLang !== this.masterLang) {
|
|
319
|
+
this.data.copyLanguage(this.masterLang, this.targetLang, this.shouldReplaceExisting)
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// get target language data
|
|
323
|
+
const targetLanguage = this.data.getLanguage(this.targetLang)
|
|
324
|
+
|
|
325
|
+
if (this.targetLang === this.masterLang && !this.shouldReplaceExisting) {
|
|
326
|
+
const err = new Error(`Folder already exists. ${targetLanguage.path}`)
|
|
327
|
+
err.number = 10003
|
|
328
|
+
throw err
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// process import files
|
|
332
|
+
let importData
|
|
333
|
+
switch (format) {
|
|
334
|
+
case 'json':
|
|
335
|
+
importData = fs.readJSONSync(langFiles[0])
|
|
336
|
+
break
|
|
337
|
+
case 'xliff':
|
|
338
|
+
case 'xlf': {
|
|
339
|
+
importData = []
|
|
340
|
+
await async.each(langFiles, (filename, done) => {
|
|
341
|
+
const XMLData = fs.readFileSync(filename)
|
|
342
|
+
const parser = new XMLParser({
|
|
343
|
+
ignoreAttributes: false,
|
|
344
|
+
attributeNamePrefix: ''
|
|
345
|
+
})
|
|
346
|
+
const xml = parser.parse(XMLData)
|
|
347
|
+
// xliff 2.0
|
|
348
|
+
// for (const file of xml.xliff.file) {
|
|
349
|
+
// for (const unit of file.unit) {
|
|
350
|
+
// const [ id, ...path ] = unit.id.split('/');
|
|
351
|
+
// importData.push({
|
|
352
|
+
// file: file.id,
|
|
353
|
+
// id,
|
|
354
|
+
// path: path.filter(Boolean).join('/'),
|
|
355
|
+
// value: unit.segment.target['#text']
|
|
356
|
+
// });
|
|
357
|
+
// }
|
|
358
|
+
// }
|
|
359
|
+
// xliff 1.2
|
|
360
|
+
for (const file of xml.xliff.file) {
|
|
361
|
+
for (const unit of file.body['trans-unit']) {
|
|
362
|
+
const [id, ...path] = unit.id.split('/')
|
|
363
|
+
importData.push({
|
|
364
|
+
file: file.original,
|
|
365
|
+
id,
|
|
366
|
+
path: path.filter(Boolean).join('/'),
|
|
367
|
+
value: unit.source
|
|
368
|
+
})
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
done()
|
|
372
|
+
})
|
|
373
|
+
break
|
|
374
|
+
}
|
|
375
|
+
case 'csv':
|
|
376
|
+
default: {
|
|
377
|
+
importData = []
|
|
378
|
+
const lines = []
|
|
379
|
+
await async.each(langFiles, (filename, done) => {
|
|
380
|
+
const fileBuffer = fs.readFileSync(filename, {
|
|
381
|
+
encoding: null
|
|
382
|
+
})
|
|
383
|
+
const detected = jschardet.detect(fileBuffer)
|
|
384
|
+
let fileContent
|
|
385
|
+
if (iconv.encodingExists(detected.encoding)) {
|
|
386
|
+
fileContent = iconv.decode(fileBuffer, detected.encoding)
|
|
387
|
+
this.log(`Encoding detected as ${detected.encoding} ${filename}`)
|
|
388
|
+
} else {
|
|
389
|
+
fileContent = iconv.decode(fileBuffer, 'utf8')
|
|
390
|
+
this.log(`Encoding not detected used utf-8 ${filename}`)
|
|
391
|
+
}
|
|
392
|
+
let csvDelimiter = this.csvDelimiter
|
|
393
|
+
if (!csvDelimiter) {
|
|
394
|
+
const firstLineMatches = fileContent.match(/^[^,;\t| \n\r]+\/"{0,1}[,;\t| ]{1}/)
|
|
395
|
+
if (firstLineMatches && firstLineMatches.length) {
|
|
396
|
+
const detectedDelimiter = firstLineMatches[0].slice(-1)
|
|
397
|
+
if (detectedDelimiter !== this.csvDelimiter) {
|
|
398
|
+
this.log(`Delimiter detected as ${detectedDelimiter} in ${filename}`)
|
|
399
|
+
csvDelimiter = detectedDelimiter
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
if (!csvDelimiter) {
|
|
404
|
+
const err = new Error(`Could not detect csv delimiter ${targetLanguage.path}`)
|
|
405
|
+
err.number = 10014
|
|
406
|
+
throw err
|
|
407
|
+
}
|
|
408
|
+
const options = {
|
|
409
|
+
delimiter: csvDelimiter
|
|
410
|
+
}
|
|
411
|
+
csv.parse(fileContent, options, (error, output) => {
|
|
412
|
+
if (error) {
|
|
413
|
+
return done(error)
|
|
414
|
+
}
|
|
415
|
+
let hasWarnedTruncated = false
|
|
416
|
+
output.forEach(line => {
|
|
417
|
+
if (line.length < 2) {
|
|
418
|
+
throw new Error(`Too few columns detected: expected 2, found ${line.length} in ${filename}`)
|
|
419
|
+
}
|
|
420
|
+
if (line.length !== 2 && !hasWarnedTruncated) {
|
|
421
|
+
this.log(`Truncating, too many columns detected: expected 2, found extra ${line.length - 2} in ${filename}`)
|
|
422
|
+
hasWarnedTruncated = true
|
|
423
|
+
}
|
|
424
|
+
line.length = 2
|
|
425
|
+
})
|
|
426
|
+
lines.push(...output)
|
|
427
|
+
done(null)
|
|
428
|
+
})
|
|
429
|
+
}).then(() => {
|
|
430
|
+
lines.forEach(line => {
|
|
431
|
+
const [file, id, ...path] = line[0].split('/')
|
|
432
|
+
importData.push({
|
|
433
|
+
file,
|
|
434
|
+
id,
|
|
435
|
+
path: path.filter(Boolean).join('/'),
|
|
436
|
+
value: line[1]
|
|
437
|
+
})
|
|
438
|
+
})
|
|
439
|
+
}, err => {
|
|
440
|
+
throw new Error(`Error processing CSV files: ${err}`)
|
|
441
|
+
})
|
|
442
|
+
break
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// check import validity
|
|
447
|
+
const item = importData[0]
|
|
448
|
+
const isValid = Object.prototype.hasOwnProperty.call(item, 'file') && Object.prototype.hasOwnProperty.call(item, 'id') && Object.prototype.hasOwnProperty.call(item, 'path') && Object.prototype.hasOwnProperty.call(item, 'value')
|
|
449
|
+
if (!isValid) {
|
|
450
|
+
throw new Error('Sorry, the imported File is not valid')
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// maintain output order with original translate tasks
|
|
454
|
+
// TODO: could probably improve this with read order rather than file order
|
|
455
|
+
const typeSortLevel = {
|
|
456
|
+
course: 1,
|
|
457
|
+
contentObjects: 2,
|
|
458
|
+
articles: 3,
|
|
459
|
+
blocks: 4,
|
|
460
|
+
components: 5
|
|
461
|
+
}
|
|
462
|
+
importData.sort((a, b) => {
|
|
463
|
+
const typeSort = ((typeSortLevel[a.file] || 100) - (typeSortLevel[b.file] || 100))
|
|
464
|
+
return typeSort || a.id.length - b.id.length || a.id.localeCompare(b.id)
|
|
465
|
+
})
|
|
466
|
+
|
|
467
|
+
// update data
|
|
468
|
+
importData.forEach(data => {
|
|
469
|
+
const { file, item } = targetLanguage.getFileItemById(data.id)
|
|
470
|
+
const attributePath = data.path.split('/').filter(Boolean)
|
|
471
|
+
const currentValue = _.get(item, attributePath)
|
|
472
|
+
if (currentValue === data.value) {
|
|
473
|
+
// value is unchanged, skip
|
|
474
|
+
return
|
|
475
|
+
}
|
|
476
|
+
this.log(`#${data.id}\t${attributePath.join('.')}`)
|
|
477
|
+
this.log(` '${currentValue}'`)
|
|
478
|
+
this.log(` '${data.value}'`)
|
|
479
|
+
_.set(item, attributePath, data.value)
|
|
480
|
+
file.changed()
|
|
481
|
+
})
|
|
482
|
+
if (!targetLanguage.hasChanged) {
|
|
483
|
+
this.warn('No changed were found, target and import are identical.')
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// save data
|
|
487
|
+
if (!this.isTest) {
|
|
488
|
+
this.data.save()
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
return this
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
export default Translate
|