google-spreadsheet-translation-sync 1.3.19 → 1.4.1
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.md +7 -0
- package/index.js +5 -0
- package/package.json +1 -1
- package/src/export-to-spreadsheet.js +1 -1
- package/src/handler.js +2 -0
- package/src/handlers/wordpress.js +173 -0
- package/src/import-from-spreadsheet.js +26 -1
- package/src/util/constraints.js +1 -0
- package/test/test.js +113 -2
package/README.md
CHANGED
|
@@ -65,6 +65,12 @@ Default value: 'default'
|
|
|
65
65
|
|
|
66
66
|
If no locale is provided or could be parsed, the defaultLocaleName is used instead.
|
|
67
67
|
|
|
68
|
+
#### options.defaultFallback
|
|
69
|
+
Type: `Boolean`
|
|
70
|
+
Default value: `false`
|
|
71
|
+
|
|
72
|
+
Fills empty collumns automatically with the value from the defaultLocale - usefull if always complete translations have to be provided per file.
|
|
73
|
+
|
|
68
74
|
#### options.fileBaseName
|
|
69
75
|
Type: `String`
|
|
70
76
|
Default value: (empty string)
|
|
@@ -95,6 +101,7 @@ Possible Values:
|
|
|
95
101
|
* `locale_json` (translations are stored in simple key/value json files)
|
|
96
102
|
* `json_structure` (translations are stored in a json tree, where the keys are split by the . character)
|
|
97
103
|
* `gettext` (utilizes [node gettext-parser](https://github.com/smhg/gettext-parser) for the work with po and mo files)
|
|
104
|
+
* `wordpress` (utilizes [node gettext-parser](https://github.com/smhg/gettext-parser) for the work with po and mo files) and adds additionally the new wordpress 6.5+ `.l10n.php` structure for faster parsing
|
|
98
105
|
* `properties` (utilizes [propertie-reader](https://github.com/steveukx/properties) for java property files)
|
|
99
106
|
* `yml` (utilizes [js-yaml](https://github.com/nodeca/js-yaml) for symfony yaml translation files)
|
|
100
107
|
Default value: `locale_json`
|
package/index.js
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* The options type
|
|
5
|
+
* @typedef {{translationFormat: string, mode: string, spreadsheetId: string, gid : string, credentials: {}, keyId: string, fileBaseName: string, namespaces: boolean, defaultLocaleName: string, defaultFallback: boolean}} OptionsObject
|
|
6
|
+
*/
|
|
7
|
+
|
|
3
8
|
/**
|
|
4
9
|
* Adds commas to a number
|
|
5
10
|
*/
|
package/package.json
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* @param {[]} translationFiles - an array of files
|
|
8
|
-
* @param {
|
|
8
|
+
* @param {OptionsObject} options
|
|
9
9
|
* @param {function} callback
|
|
10
10
|
*/
|
|
11
11
|
module.exports = function (translationFiles, options, callback) {
|
package/src/handler.js
CHANGED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const withoutError = require('../helpers').withoutError
|
|
5
|
+
const gettextParser = require("gettext-parser");
|
|
6
|
+
const constraints = require('../util/constraints');
|
|
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
|
+
if (err) {
|
|
20
|
+
callback({});
|
|
21
|
+
} else {
|
|
22
|
+
callback(gettextParser.po.parse(data));
|
|
23
|
+
}
|
|
24
|
+
})
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* A wrapper to return a flat key: value object structure, used for uploading
|
|
31
|
+
* @param filePath
|
|
32
|
+
* @param callback
|
|
33
|
+
*/
|
|
34
|
+
module.exports.getTranslationKeys = function (filePath, callback) {
|
|
35
|
+
|
|
36
|
+
const translations = {};
|
|
37
|
+
|
|
38
|
+
this.loadTranslationFile(filePath, function (parsedObj) {
|
|
39
|
+
if (parsedObj.translations) {
|
|
40
|
+
Object.keys(parsedObj.translations['']).forEach(function (key) {
|
|
41
|
+
translations[key] = parsedObj.translations[''][key].msgstr;
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
callback(translations);
|
|
46
|
+
})
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
module.exports.updateTranslations = function (translationData, translationRootFolder, options, callback) {
|
|
51
|
+
const path = require('path');
|
|
52
|
+
const async = require('async');
|
|
53
|
+
const mod = this;
|
|
54
|
+
const fileUtils = require('../util/file-utils');
|
|
55
|
+
|
|
56
|
+
if (! fs.existsSync(translationRootFolder)) {
|
|
57
|
+
throw new Error('The folder ' + translationRootFolder + ' does not exist');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async.each(Object.keys(translationData), function(locale, done) {
|
|
61
|
+
|
|
62
|
+
// is it a comment or a real translation?
|
|
63
|
+
if (locale.substr(0, constraints.commentCollumnName.length) !== constraints.commentCollumnName) {
|
|
64
|
+
|
|
65
|
+
async.each(Object.keys(translationData[locale]), function (namespace, done2) {
|
|
66
|
+
|
|
67
|
+
const localeFileName = fileUtils.buildTranslationFileName(constraints.TRANSLATION_FORMATS.GETTEXT, namespace, locale, options);
|
|
68
|
+
const file = path.resolve(translationRootFolder + '/' + localeFileName);
|
|
69
|
+
const moFile = path.resolve(translationRootFolder + '/' + localeFileName.replace('.po', '.mo'));
|
|
70
|
+
const phpFile = path.resolve(translationRootFolder + '/' + localeFileName.replace('.po', '.l10n.php'));
|
|
71
|
+
|
|
72
|
+
mod.loadTranslationFile(file, function (parsedObj) {
|
|
73
|
+
|
|
74
|
+
// do we have a file?
|
|
75
|
+
if (!parsedObj.translations) {
|
|
76
|
+
parsedObj = {
|
|
77
|
+
"charset": "UTF-8",
|
|
78
|
+
|
|
79
|
+
"headers": {
|
|
80
|
+
"content-type": "text/plain; charset=UTF-8",
|
|
81
|
+
"plural-forms": "nplurals=2; plural=(n!=1);",
|
|
82
|
+
"X-Generator": "node-google-spreadsheet-translation-sync",
|
|
83
|
+
"Project-Id-Version": options.fileBaseName,
|
|
84
|
+
"Language": locale
|
|
85
|
+
},
|
|
86
|
+
|
|
87
|
+
"translations": {
|
|
88
|
+
"": {}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const potentiallyUpdatedTranslations = translationData[locale][namespace];
|
|
94
|
+
|
|
95
|
+
if (potentiallyUpdatedTranslations) {
|
|
96
|
+
// update our object
|
|
97
|
+
Object.keys(potentiallyUpdatedTranslations).forEach(function (key, index) {
|
|
98
|
+
if (!parsedObj.translations[''][key]) {
|
|
99
|
+
parsedObj.translations[''][key] = {
|
|
100
|
+
"msgid": key,
|
|
101
|
+
"msgstr": [],
|
|
102
|
+
"comments": {}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
parsedObj.translations[''][key].msgstr[0] = potentiallyUpdatedTranslations[key];
|
|
107
|
+
|
|
108
|
+
// do we have a comment?
|
|
109
|
+
if (translationData[constraints.commentCollumnName] && translationData[constraints.commentCollumnName][key]) {
|
|
110
|
+
if (!parsedObj.translations[''][key].comments) {
|
|
111
|
+
parsedObj.translations[''][key].comments = {};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
parsedObj.translations[''][key].comments.translator = translationData[constraints.commentCollumnName][key];
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// now we write
|
|
119
|
+
|
|
120
|
+
const output = gettextParser.po.compile(parsedObj, {sort: true});
|
|
121
|
+
fs.writeFileSync(file, output);
|
|
122
|
+
|
|
123
|
+
const mo = gettextParser.mo.compile(parsedObj);
|
|
124
|
+
fs.writeFileSync(moFile, mo);
|
|
125
|
+
|
|
126
|
+
// write the additional .i10n.php file
|
|
127
|
+
|
|
128
|
+
if (fs.existsSync(phpFile)) {
|
|
129
|
+
fs.unlinkSync(phpFile);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
let phpContent = '<?php\n';
|
|
133
|
+
phpContent += 'return [\n';
|
|
134
|
+
|
|
135
|
+
Object.keys(parsedObj.headers).forEach((header) => {
|
|
136
|
+
phpContent += ` "${header.toLocaleLowerCase()}" => ${JSON.stringify(parsedObj.headers[header])},\n`;
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
phpContent += ` "messages" => [\n`;
|
|
140
|
+
|
|
141
|
+
Object.keys(parsedObj.translations['']).forEach((key) => {
|
|
142
|
+
if (key) {
|
|
143
|
+
phpContent += ` "${key}" => ${JSON.stringify(parsedObj.translations[''][key].msgstr[0])},\n`;
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
phpContent += ' ]\n';
|
|
148
|
+
phpContent += '];\n';
|
|
149
|
+
|
|
150
|
+
fs.writeFileSync(phpFile, phpContent);
|
|
151
|
+
|
|
152
|
+
// console.info('Updated translations of %o', localeFileName);
|
|
153
|
+
} else {
|
|
154
|
+
// console.info('Ignored unchanged %o', localeFileName);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
done2();
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
}, function () {
|
|
161
|
+
done();
|
|
162
|
+
})
|
|
163
|
+
} else {
|
|
164
|
+
done();
|
|
165
|
+
}
|
|
166
|
+
}, function (err) {
|
|
167
|
+
if (withoutError(err, callback)) {
|
|
168
|
+
callback(null);
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
}
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
'use strict'
|
|
2
|
-
|
|
2
|
+
/**
|
|
3
|
+
*
|
|
4
|
+
* @param translationRootFolder
|
|
5
|
+
* @param {OptionsObject} options
|
|
6
|
+
* @param callback
|
|
7
|
+
*/
|
|
3
8
|
module.exports = function (translationRootFolder, options, callback) {
|
|
4
9
|
|
|
5
10
|
const connector = require('./connector');
|
|
@@ -74,6 +79,26 @@ module.exports = function (translationRootFolder, options, callback) {
|
|
|
74
79
|
}
|
|
75
80
|
}
|
|
76
81
|
|
|
82
|
+
if (options.defaultFallback) {
|
|
83
|
+
|
|
84
|
+
const defaultLocale = options.defaultLocaleName || 'default';
|
|
85
|
+
|
|
86
|
+
Object.keys(translationData).forEach(locale => {
|
|
87
|
+
// use the default locale as reference
|
|
88
|
+
Object.keys(translationData[defaultLocale]).forEach(namespace => {
|
|
89
|
+
if (!translationData[locale][namespace]) {
|
|
90
|
+
translationData[locale][namespace] = {};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
Object.keys(translationData[defaultLocale][namespace]).forEach(key => {
|
|
94
|
+
if (translationData[locale][namespace][key] === undefined) {
|
|
95
|
+
translationData[locale][namespace][key] = translationData[defaultLocale][namespace][key];
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
77
102
|
// now we get the handler
|
|
78
103
|
const h = require('./handler');
|
|
79
104
|
const TRANSLATION_FORMATS = require('./util/constraints').TRANSLATION_FORMATS;
|
package/src/util/constraints.js
CHANGED
|
@@ -2,6 +2,7 @@ const formats = {
|
|
|
2
2
|
LOCALE_JSON: 'locale_json', // just a plain array - lorem.ipsum keys will be {'lorem.ipsum': 'whatever'}
|
|
3
3
|
JSON_STRUCTURE: 'json_structure', // lorem.ipsum keys will be converted into a json structure: {lorem: {ipsum: 'whatever'}}
|
|
4
4
|
GETTEXT: 'gettext',
|
|
5
|
+
WORDPRESS: 'wordpress',
|
|
5
6
|
PROPERTIES: 'properties',
|
|
6
7
|
YAML: 'yml'
|
|
7
8
|
};
|
package/test/test.js
CHANGED
|
@@ -53,8 +53,8 @@ function ensureFolder (folder) {
|
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
|
|
56
|
-
// const testFor = 'all' // 'connect', 'upload', 'import', 'properties', 'locale_json', 'gettext', 'yml', 'json_structure'
|
|
57
|
-
// const testFor = '
|
|
56
|
+
// const testFor = 'all' // 'connect', 'upload', 'import', 'properties', 'locale_json', 'gettext', 'yml', 'json_structure', 'wordpress'
|
|
57
|
+
// const testFor = 'gettext';
|
|
58
58
|
const testFor = 'all';
|
|
59
59
|
|
|
60
60
|
const tests = [
|
|
@@ -370,6 +370,74 @@ const tests = [
|
|
|
370
370
|
}
|
|
371
371
|
},
|
|
372
372
|
|
|
373
|
+
{
|
|
374
|
+
name: 'should import updated wordpress keys in the test project',
|
|
375
|
+
run: ['import', 'wordpress'],
|
|
376
|
+
fnc: function (done) {
|
|
377
|
+
this.timeout(timeout);
|
|
378
|
+
|
|
379
|
+
const baseName = 'karmapa-chenno';
|
|
380
|
+
|
|
381
|
+
const options = {
|
|
382
|
+
translationFormat: 'wordpress',
|
|
383
|
+
fileBaseName: baseName,
|
|
384
|
+
spreadsheetId: testSheetId_gettext,
|
|
385
|
+
credentials: accessData
|
|
386
|
+
};
|
|
387
|
+
|
|
388
|
+
const translationRoot = ensureFolder(path.resolve('./test/translations/' + options.translationFormat + '/'));
|
|
389
|
+
const testFile = path.resolve(translationRoot + '/' + baseName + '-en.po');
|
|
390
|
+
const testFilePhp = path.resolve(translationRoot + '/' + baseName + '-en.l10n.php');
|
|
391
|
+
const testFilePl = path.resolve(translationRoot + '/' + baseName + '-pl.po');
|
|
392
|
+
|
|
393
|
+
app.importFromSpreadsheet(translationRoot, options, function (err) {
|
|
394
|
+
expect(err).to.be.null
|
|
395
|
+
|
|
396
|
+
if (!err) {
|
|
397
|
+
const fs = require('fs');
|
|
398
|
+
|
|
399
|
+
expect(fs.existsSync(testFile)).to.equal(true);
|
|
400
|
+
|
|
401
|
+
const po = require('gettext-parser').po.parse(fs.readFileSync(testFile));
|
|
402
|
+
const poPl = require('gettext-parser').po.parse(fs.readFileSync(testFilePl));
|
|
403
|
+
const translations = po.translations[''];
|
|
404
|
+
const translationsPl = poPl.translations[''];
|
|
405
|
+
expect(translations.add_address.msgstr[0]).to.equal("Add new address");
|
|
406
|
+
expect(translationsPl['additional.news'].msgstr[0]).to.equal('czecz 2242');
|
|
407
|
+
expect(translationsPl.add_address).to.be.undefined;
|
|
408
|
+
|
|
409
|
+
const phpContent = fs.readFileSync(testFilePhp).toString();
|
|
410
|
+
|
|
411
|
+
expect(phpContent).to.equal('<?php\n' +
|
|
412
|
+
'return [\n' +
|
|
413
|
+
' "content-type" => "text/plain; charset=utf-8",\n' +
|
|
414
|
+
' "plural-forms" => "nplurals=2; plural=(n!=1);",\n' +
|
|
415
|
+
' "x-generator" => "node-google-spreadsheet-translation-sync",\n' +
|
|
416
|
+
' "project-id-version" => "karmapa-chenno",\n' +
|
|
417
|
+
' "language" => "en",\n' +
|
|
418
|
+
' "messages" => [\n' +
|
|
419
|
+
' "add_address" => "Add new address",\n' +
|
|
420
|
+
' "add_as_favourite" => "Add to Quick Contacts",\n' +
|
|
421
|
+
' "add_center" => "Add Center",\n' +
|
|
422
|
+
' "add_contact" => "Add contact",\n' +
|
|
423
|
+
' "add_phone_number" => "Add new phone",\n' +
|
|
424
|
+
' "additional.news" => "Additional News",\n' +
|
|
425
|
+
' "administration" => "Administration",\n' +
|
|
426
|
+
' "administration_info" => "This page allows you to access functions connected to Members, Centers, Roles and Groups.",\n' +
|
|
427
|
+
' "all" => "All",\n' +
|
|
428
|
+
' "all_centers" => "All Centers",\n' +
|
|
429
|
+
' "all_contacts" => "All contacts",\n' +
|
|
430
|
+
' "some.key" => "a Key 370",\n' +
|
|
431
|
+
' ]\n' +
|
|
432
|
+
'];\n');
|
|
433
|
+
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
done();
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
},
|
|
440
|
+
|
|
373
441
|
{
|
|
374
442
|
name: 'should import updated gettext keys in the test project',
|
|
375
443
|
run: ['import', 'gettext'],
|
|
@@ -403,6 +471,49 @@ const tests = [
|
|
|
403
471
|
const translationsPl = poPl.translations[''];
|
|
404
472
|
expect(translations.add_address.msgstr[0]).to.equal("Add new address");
|
|
405
473
|
expect(translationsPl['additional.news'].msgstr[0]).to.equal('czecz 2242');
|
|
474
|
+
expect(translationsPl.add_address).to.be.undefined;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
done();
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
},
|
|
481
|
+
|
|
482
|
+
{
|
|
483
|
+
name: 'should fill empty columns with the default',
|
|
484
|
+
run: ['import', 'gettext'],
|
|
485
|
+
fnc: function (done) {
|
|
486
|
+
this.timeout(timeout);
|
|
487
|
+
|
|
488
|
+
const baseName = 'om-mani-peme';
|
|
489
|
+
|
|
490
|
+
const options = {
|
|
491
|
+
translationFormat: 'gettext',
|
|
492
|
+
fileBaseName: baseName,
|
|
493
|
+
spreadsheetId: testSheetId_gettext,
|
|
494
|
+
credentials: accessData,
|
|
495
|
+
defaultLocaleName: 'en',
|
|
496
|
+
defaultFallback: true
|
|
497
|
+
};
|
|
498
|
+
|
|
499
|
+
const translationRoot = ensureFolder(path.resolve('./test/translations/' + options.translationFormat + '/'));
|
|
500
|
+
const testFile = path.resolve(translationRoot + '/' + baseName + '-en.po');
|
|
501
|
+
const testFileDe = path.resolve(translationRoot + '/' + baseName + '-de.po');
|
|
502
|
+
|
|
503
|
+
app.importFromSpreadsheet(translationRoot, options, function (err) {
|
|
504
|
+
expect(err).to.be.null
|
|
505
|
+
|
|
506
|
+
if (!err) {
|
|
507
|
+
const fs = require('fs');
|
|
508
|
+
|
|
509
|
+
expect(fs.existsSync(testFile)).to.equal(true);
|
|
510
|
+
|
|
511
|
+
const po = require('gettext-parser').po.parse(fs.readFileSync(testFile));
|
|
512
|
+
const poDe = require('gettext-parser').po.parse(fs.readFileSync(testFileDe));
|
|
513
|
+
const translations = po.translations[''];
|
|
514
|
+
const translationsDe = poDe.translations[''];
|
|
515
|
+
expect(translations['additional.news'].msgstr[0]).to.equal("Additional News");
|
|
516
|
+
expect(translationsDe['additional.news']?.msgstr[0]).to.equal('Additional News');
|
|
406
517
|
}
|
|
407
518
|
|
|
408
519
|
done();
|