google-spreadsheet-translation-sync 1.3.4 → 1.3.8
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/Gruntfile.js +1 -1
- package/README.md +1 -0
- package/package.json +4 -3
- package/src/handler.js +2 -1
- package/src/handlers/json_structure.js +107 -0
- package/src/handlers/properties.js +1 -1
- package/src/handlers/yaml.js +2 -18
- package/src/util/constraints.js +2 -1
- package/src/util/file-utils.js +1 -0
- package/src/util/structure-utils.js +27 -0
- package/test/test.js +119 -3
package/Gruntfile.js
CHANGED
|
@@ -75,7 +75,7 @@ module.exports = function(grunt) {
|
|
|
75
75
|
shell: {
|
|
76
76
|
publish_npm: {
|
|
77
77
|
command: [
|
|
78
|
-
'npmrc public', // well, this is at the moment hardcoded to my personal public registry name
|
|
78
|
+
// 'npmrc public', // well, this is at the moment hardcoded to my personal public registry name
|
|
79
79
|
'npm publish'
|
|
80
80
|
].join('&&')
|
|
81
81
|
},
|
package/README.md
CHANGED
|
@@ -86,6 +86,7 @@ If `namespaces` or `fileBaseName` is used, this is the separating character. For
|
|
|
86
86
|
Type: `Enum`
|
|
87
87
|
Possible Values:
|
|
88
88
|
* `locale_json` (translations are stored in simple key/value json files)
|
|
89
|
+
* `json_structure` (translations are stored in a json tree, where the keys are split by the . character)
|
|
89
90
|
* `gettext` (utilizes [node gettext-parser](https://github.com/smhg/gettext-parser) for the work with po and mo files)
|
|
90
91
|
* `properties` (utilizes [propertie-reader](https://github.com/steveukx/properties) for java property files)
|
|
91
92
|
* `yml` (utilizes [js-yaml](https://github.com/nodeca/js-yaml) for symfony yaml translation files)
|
package/package.json
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "google-spreadsheet-translation-sync",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.8",
|
|
4
4
|
"description": "A plugin to read and write i18n translationsfrom and to google spreadsheets ",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
7
|
-
"test": "mocha --reporter spec"
|
|
7
|
+
"test": "mocha --reporter spec",
|
|
8
|
+
"build": "grunt build"
|
|
8
9
|
},
|
|
9
10
|
"repository": {
|
|
10
11
|
"type": "git",
|
|
@@ -48,7 +49,7 @@
|
|
|
48
49
|
"google-spreadsheet": "^3.0.11",
|
|
49
50
|
"js-yaml": "^4.1.0",
|
|
50
51
|
"mkdirp": "^0.5.5",
|
|
51
|
-
"properties-reader": "
|
|
52
|
+
"properties-reader": "^2.2.0",
|
|
52
53
|
"shelljs": "^0.7.8",
|
|
53
54
|
"tmp": "^0.1.0"
|
|
54
55
|
}
|
package/src/handler.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
-
|
|
4
3
|
/**
|
|
5
4
|
*
|
|
6
5
|
* @param translationFormat
|
|
@@ -13,6 +12,8 @@ module.exports.getHandler = function (translationFormat) {
|
|
|
13
12
|
switch (translationFormat) {
|
|
14
13
|
default:
|
|
15
14
|
throw new Error('No handler available for the translation format ' + translationFormat);
|
|
15
|
+
case TRANSLATION_FORMATS.JSON_STRUCTURE:
|
|
16
|
+
return require('./handlers/json_structure');
|
|
16
17
|
case TRANSLATION_FORMATS.LOCALE_JSON:
|
|
17
18
|
return require('./handlers/locale_json');
|
|
18
19
|
case TRANSLATION_FORMATS.GETTEXT:
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const withoutError = require('../helpers').withoutError
|
|
5
|
+
const constraints = require('../util/constraints');
|
|
6
|
+
const {resolveStructureToTree} = require("../util/structure-utils");
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
module.exports.loadTranslationFile = function (filePath, callback) {
|
|
10
|
+
|
|
11
|
+
fs.access(filePath, function (err) {
|
|
12
|
+
|
|
13
|
+
if (err) {
|
|
14
|
+
callback({}); // we return the empty json
|
|
15
|
+
} else {
|
|
16
|
+
|
|
17
|
+
fs.readFile(filePath, function (err, data) {
|
|
18
|
+
|
|
19
|
+
const existingTranslations = err ? {} : JSON.parse(data);
|
|
20
|
+
const cleanResult = {};
|
|
21
|
+
|
|
22
|
+
resolveStructureToTree(cleanResult, existingTranslations);
|
|
23
|
+
|
|
24
|
+
callback(cleanResult);
|
|
25
|
+
})
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* A wrapper to return a flat key: value object structure, used for uploading
|
|
32
|
+
* @param {string} filePath
|
|
33
|
+
* @param {function} callback
|
|
34
|
+
*/
|
|
35
|
+
module.exports.getTranslationKeys = function (filePath, callback) {
|
|
36
|
+
const result = {};
|
|
37
|
+
|
|
38
|
+
const keyFlattener = function(structure, prefix) {
|
|
39
|
+
prefix = prefix || '';
|
|
40
|
+
|
|
41
|
+
Object.keys(structure).forEach((key) => {
|
|
42
|
+
|
|
43
|
+
const subStructure = structure[key];
|
|
44
|
+
|
|
45
|
+
if (typeof subStructure === 'object' && subStructure !== null && subStructure !== undefined) {
|
|
46
|
+
keyFlattener(subStructure, prefix + key + '.');
|
|
47
|
+
} else {
|
|
48
|
+
result[prefix + key] = subStructure;
|
|
49
|
+
}
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
this.loadTranslationFile(filePath, (parsedObject) => {
|
|
55
|
+
keyFlattener(parsedObject);
|
|
56
|
+
callback(result);
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
module.exports.updateTranslations = function (translationData, translationRootFolder, options, callback) {
|
|
61
|
+
const path = require('path');
|
|
62
|
+
const async = require('async');
|
|
63
|
+
const mod = this;
|
|
64
|
+
const fileUtils = require('../util/file-utils');
|
|
65
|
+
|
|
66
|
+
if (! fs.existsSync(translationRootFolder)) {
|
|
67
|
+
throw new Error('The folder ' + translationRootFolder + ' does not exist');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async.each(Object.keys(translationData), function(locale, done) {
|
|
71
|
+
|
|
72
|
+
// is it a comment or a real translation?
|
|
73
|
+
if (locale.substr(0, constraints.commentCollumnName.length) !== constraints.commentCollumnName) {
|
|
74
|
+
|
|
75
|
+
async.each(Object.keys(translationData[locale]), function (namespace, done2) {
|
|
76
|
+
|
|
77
|
+
const localeFileName = fileUtils.buildTranslationFileName(constraints.TRANSLATION_FORMATS.JSON_STRUCTURE, namespace, locale, options);
|
|
78
|
+
const file = path.resolve(translationRootFolder + '/' + localeFileName);
|
|
79
|
+
|
|
80
|
+
mod.loadTranslationFile(file, function (translations) {
|
|
81
|
+
const potentiallyUpdatedTranslations = translationData[locale][namespace];
|
|
82
|
+
|
|
83
|
+
resolveStructureToTree(translations, potentiallyUpdatedTranslations);
|
|
84
|
+
|
|
85
|
+
// now we write
|
|
86
|
+
fs.writeFile(file, JSON.stringify(translations, null, 4), function (err) {
|
|
87
|
+
if (withoutError(err)) {
|
|
88
|
+
// console.info('Updated translations of %o', localeFileName);
|
|
89
|
+
}
|
|
90
|
+
done2();
|
|
91
|
+
})
|
|
92
|
+
})
|
|
93
|
+
}, function () {
|
|
94
|
+
done();
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
} else {
|
|
98
|
+
done();
|
|
99
|
+
}
|
|
100
|
+
}, function (err) {
|
|
101
|
+
if (withoutError(err, callback)) {
|
|
102
|
+
callback(null);
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
}
|
|
@@ -46,7 +46,7 @@ module.exports.updateTranslations = function (translationData, translationRootFo
|
|
|
46
46
|
fs.writeFileSync(file, '');
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
const props = PropertiesReader(file, {
|
|
49
|
+
const props = PropertiesReader(file, 'utf-8', { writer: { saveSections: false } });
|
|
50
50
|
const potentiallyUpdatedTranslations = translationData[locale][namespace];
|
|
51
51
|
|
|
52
52
|
Object.keys(potentiallyUpdatedTranslations).forEach(function (key) {
|
package/src/handlers/yaml.js
CHANGED
|
@@ -2,6 +2,7 @@ const fs = require("fs");
|
|
|
2
2
|
const yaml = require('js-yaml');
|
|
3
3
|
const constraints = require("../util/constraints");
|
|
4
4
|
const {withoutError} = require("../helpers");
|
|
5
|
+
const {resolveStructureToTree} = require("../util/structure-utils");
|
|
5
6
|
|
|
6
7
|
module.exports.loadTranslationFile = function (filePath, callback) {
|
|
7
8
|
if (fs.existsSync(filePath)) {
|
|
@@ -65,24 +66,7 @@ module.exports.updateTranslations = function (translationData, translationRootFo
|
|
|
65
66
|
mod.loadTranslationFile(file, function (translations) {
|
|
66
67
|
const potentiallyUpdatedTranslations = translationData[locale][namespace];
|
|
67
68
|
|
|
68
|
-
|
|
69
|
-
const parts = key.split('.');
|
|
70
|
-
let tree = translations;
|
|
71
|
-
|
|
72
|
-
while (parts.length > 0) {
|
|
73
|
-
const part = parts.shift();
|
|
74
|
-
|
|
75
|
-
if (parts.length === 0) {
|
|
76
|
-
tree[part] = potentiallyUpdatedTranslations[key];
|
|
77
|
-
} else {
|
|
78
|
-
if (!tree[part]) {
|
|
79
|
-
tree[part] = {}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
tree = tree[part];
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
});
|
|
69
|
+
resolveStructureToTree(translations, potentiallyUpdatedTranslations);
|
|
86
70
|
|
|
87
71
|
// now we write
|
|
88
72
|
fs.writeFile(file, yaml.dump(translations, {indent: 4}), function (err) {
|
package/src/util/constraints.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const formats = {
|
|
2
|
-
LOCALE_JSON: 'locale_json',
|
|
2
|
+
LOCALE_JSON: 'locale_json', // just a plain array - lorem.ipsum keys will be {'lorem.ipsum': 'whatever'}
|
|
3
|
+
JSON_STRUCTURE: 'json_structure', // lorem.ipsum keys will be converted into a json structure: {lorem: {ipsum: 'whatever'}}
|
|
3
4
|
GETTEXT: 'gettext',
|
|
4
5
|
PROPERTIES: 'properties',
|
|
5
6
|
YAML: 'yml'
|
package/src/util/file-utils.js
CHANGED
|
@@ -19,6 +19,7 @@ module.exports.buildTranslationFileName = function (format, namespace, locale, o
|
|
|
19
19
|
throw new Error('Unknown extension for translation format ' + format);
|
|
20
20
|
break;
|
|
21
21
|
case constraints.TRANSLATION_FORMATS.LOCALE_JSON:
|
|
22
|
+
case constraints.TRANSLATION_FORMATS.JSON_STRUCTURE:
|
|
22
23
|
extension = 'json';
|
|
23
24
|
break;
|
|
24
25
|
case constraints.TRANSLATION_FORMATS.GETTEXT:
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
module.exports.resolveStructureToTree = (originalTree, potentialTreeUpdates) => {
|
|
2
|
+
|
|
3
|
+
Object.keys(potentialTreeUpdates).forEach((key) => {
|
|
4
|
+
const parts = key.split('.');
|
|
5
|
+
let tree = originalTree;
|
|
6
|
+
|
|
7
|
+
// remove the full key, if present
|
|
8
|
+
if (originalTree[key]) {
|
|
9
|
+
delete originalTree[key];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// add it again in a clean fassion
|
|
13
|
+
while (parts.length > 0) {
|
|
14
|
+
const part = parts.shift();
|
|
15
|
+
|
|
16
|
+
if (parts.length === 0) {
|
|
17
|
+
tree[part] = potentialTreeUpdates[key];
|
|
18
|
+
} else {
|
|
19
|
+
if (!tree[part]) {
|
|
20
|
+
tree[part] = {}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
tree = tree[part];
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
})
|
|
27
|
+
}
|
package/test/test.js
CHANGED
|
@@ -21,6 +21,10 @@ const timeout = 20000;
|
|
|
21
21
|
var tmpFile = tmp.fileSync({postfix: '.csv'});
|
|
22
22
|
|
|
23
23
|
const csv = require('fast-csv');
|
|
24
|
+
const fileUtils = require("../src/util/file-utils");
|
|
25
|
+
const yamlHandler = require("../src/handlers/yaml");
|
|
26
|
+
const rimraf = require("rimraf");
|
|
27
|
+
const yaml = require("js-yaml");
|
|
24
28
|
const testFile = tmpFile.name;
|
|
25
29
|
const csvData = [
|
|
26
30
|
['key', 'default', 'de', 'it', 'fr', 'pl', 'hu'],
|
|
@@ -53,8 +57,8 @@ function ensureFolder (folder) {
|
|
|
53
57
|
}
|
|
54
58
|
|
|
55
59
|
|
|
56
|
-
// const testFor = 'all' // 'connect', 'upload', 'import', 'properties', 'locale_json', 'gettext', 'yml'
|
|
57
|
-
// const testFor = '
|
|
60
|
+
// const testFor = 'all' // 'connect', 'upload', 'import', 'properties', 'locale_json', 'gettext', 'yml', 'json_structure'
|
|
61
|
+
// const testFor = 'json_structure';
|
|
58
62
|
const testFor = 'all';
|
|
59
63
|
|
|
60
64
|
const tests = [
|
|
@@ -154,7 +158,7 @@ const tests = [
|
|
|
154
158
|
file: propertiesFile,
|
|
155
159
|
namespace: namespace,
|
|
156
160
|
locale: key,
|
|
157
|
-
reader: PropertiesReader(propertiesFile, {
|
|
161
|
+
reader: PropertiesReader(propertiesFile, 'utf-8', { writer: { saveSections: false } })
|
|
158
162
|
}
|
|
159
163
|
|
|
160
164
|
csvData.forEach(function (lines, i) {
|
|
@@ -514,6 +518,118 @@ const tests = [
|
|
|
514
518
|
});
|
|
515
519
|
}
|
|
516
520
|
},
|
|
521
|
+
|
|
522
|
+
{
|
|
523
|
+
name: 'should upload changes in the json_structure test project',
|
|
524
|
+
run: ['upload', 'json_structure'],
|
|
525
|
+
fnc: function (done) {
|
|
526
|
+
this.timeout(timeout);
|
|
527
|
+
|
|
528
|
+
const baseName = 'messages';
|
|
529
|
+
|
|
530
|
+
const options = {
|
|
531
|
+
translationFormat: 'json_structure',
|
|
532
|
+
fileBaseName: baseName,
|
|
533
|
+
keyId: csvData[0][0],
|
|
534
|
+
gid: 0,
|
|
535
|
+
namespaceSeparator: '.',
|
|
536
|
+
spreadsheetId: testSheetId,
|
|
537
|
+
credentials: accessData
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
const files = [];
|
|
541
|
+
|
|
542
|
+
const tempFolder = tmp.dirSync({prefix: 'trans-dyn-to-update'});
|
|
543
|
+
const handler = require('../src/handlers/json_structure');
|
|
544
|
+
const fileUtils = require('../src/util/file-utils');
|
|
545
|
+
|
|
546
|
+
const data = {};
|
|
547
|
+
|
|
548
|
+
csvData[0].forEach( function (key, index) {
|
|
549
|
+
|
|
550
|
+
if (index > 0) {
|
|
551
|
+
data[key] = {default: {}}
|
|
552
|
+
csvData.forEach(function (lines, i) {
|
|
553
|
+
if (i > 0 && lines[index]) {
|
|
554
|
+
data[key].default[lines[0]] = lines[index];
|
|
555
|
+
}
|
|
556
|
+
});
|
|
557
|
+
files.push(tempFolder.name + '/' + fileUtils.buildTranslationFileName(options.translationFormat, null, key, options));
|
|
558
|
+
}
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
handler.updateTranslations(data, tempFolder.name, options, () => {
|
|
562
|
+
|
|
563
|
+
app.exportToSpreadsheet(files, options, function (err) {
|
|
564
|
+
const rimraf = require("rimraf");
|
|
565
|
+
expect(err).to.be.null;
|
|
566
|
+
|
|
567
|
+
if (!err) {
|
|
568
|
+
connector(options.spreadsheetId, options.gid, accessData, function (err, sheet) {
|
|
569
|
+
expect(err).to.be.null;
|
|
570
|
+
expect(sheet).to.be.an('object');
|
|
571
|
+
|
|
572
|
+
sheet.getRows({
|
|
573
|
+
offset: 0,
|
|
574
|
+
limit: csvData.length - 1
|
|
575
|
+
}).then(function (rows) {
|
|
576
|
+
expect(rows).to.have.lengthOf(csvData.length - 1);
|
|
577
|
+
expect(rows[0][options.keyId]).to.equal(csvData[1][0]);
|
|
578
|
+
expect(rows[0].pl).to.equal(csvData[1][5]);
|
|
579
|
+
expect(rows[0].default).to.equal(csvData[1][1]);
|
|
580
|
+
expect(rows[0].hu).to.equal('Elfogadom'); // this was not part of the upload and should not be overwrittem
|
|
581
|
+
expect(rows[1].default).to.equal(csvData[2][1]);
|
|
582
|
+
expect(rows[1].de).to.equal(csvData[2][2]);
|
|
583
|
+
expect(rows[1].key).to.equal(csvData[2][0]);
|
|
584
|
+
rimraf.sync(tempFolder.name);
|
|
585
|
+
done()
|
|
586
|
+
})
|
|
587
|
+
})
|
|
588
|
+
} else {
|
|
589
|
+
rimraf.sync(tempFolder.name);
|
|
590
|
+
done()
|
|
591
|
+
}
|
|
592
|
+
})
|
|
593
|
+
});
|
|
594
|
+
}
|
|
595
|
+
},
|
|
596
|
+
|
|
597
|
+
{
|
|
598
|
+
name: 'should import updated json_structure keys from the test project',
|
|
599
|
+
run: ['import', 'json_structure'],
|
|
600
|
+
fnc: function (done) {
|
|
601
|
+
this.timeout(timeout);
|
|
602
|
+
|
|
603
|
+
const baseName = 'messages';
|
|
604
|
+
|
|
605
|
+
const options = {
|
|
606
|
+
translationFormat: 'json_structure',
|
|
607
|
+
fileBaseName: baseName,
|
|
608
|
+
namespaceSeparator: '.',
|
|
609
|
+
spreadsheetId: testSheetId_yaml,
|
|
610
|
+
credentials: accessData
|
|
611
|
+
};
|
|
612
|
+
|
|
613
|
+
const translationRoot = ensureFolder(path.resolve('./test/translations/' + options.translationFormat + '/'));
|
|
614
|
+
const testFile = path.resolve(translationRoot + '/' + baseName + '.en.json');
|
|
615
|
+
|
|
616
|
+
app.importFromSpreadsheet(translationRoot, options, function (err) {
|
|
617
|
+
expect(err).to.be.null
|
|
618
|
+
|
|
619
|
+
if (!err) {
|
|
620
|
+
const fs = require('fs');
|
|
621
|
+
|
|
622
|
+
expect(fs.existsSync(testFile)).to.equal(true);
|
|
623
|
+
|
|
624
|
+
const translations = require(testFile);
|
|
625
|
+
|
|
626
|
+
expect(translations.add_address).to.equal("Add new address");
|
|
627
|
+
expect(translations.additional.news).to.equal("Additional News");
|
|
628
|
+
}
|
|
629
|
+
done();
|
|
630
|
+
});
|
|
631
|
+
}
|
|
632
|
+
},
|
|
517
633
|
];
|
|
518
634
|
|
|
519
635
|
|