Skip to content

Commit 7d5313a

Browse files
author
Nikos Vasileiou
authored
Merge pull request #134 from transifex/i18next
Add i18next backend support
2 parents 381360f + d27fb9c commit 7d5313a

27 files changed

Lines changed: 951 additions & 150 deletions

README.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,11 @@ Transifex Native support for localizing Vue components.
3030
Transifex Native support for localizing ExpressJS applications.
3131
[Read more](https://github.com/transifex/transifex-javascript/tree/master/packages/express)
3232

33+
## Transifex Native for i18next
34+
35+
Transifex Native backend support for i18next
36+
[Read more](https://github.com/transifex/transifex-javascript/tree/master/packages/i18next)
37+
3338
## Transifex Native CLI
3439

3540
Command line tool for extracting phrases from source files and pushing content to Transifex.
@@ -51,7 +56,6 @@ A javascript library for building SDKs for APIs that implement the {json:api}
5156
specification. This is what our API SDK is based on.
5257
[Read more](https://github.com/transifex/transifex-javascript/tree/master/packages/jsonapi)
5358

54-
5559
# License
5660

5761
Licensed under Apache License 2.0, see [LICENSE](LICENSE) file.

packages/api/.eslintrc.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
},
88
"parser": "babel-eslint",
99
"parserOptions": {
10-
"ecmaVersion": 12,
11-
"sourceType": "module"
10+
"ecmaVersion": 12,
11+
"sourceType": "module"
1212
}
1313
}

packages/cli/README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ OPTIONS
113113
--dry-run dry run, do not push to Transifex
114114
--key-generator=source|hash [default: source] use hashed or source based keys
115115
--no-wait disable polling for upload results
116+
--parser=auto|i18next [default: auto] file parser to use
116117
--purge purge content on Transifex
117118
--secret=secret native project secret
118119
--token=token native project public token
@@ -145,6 +146,7 @@ DESCRIPTION
145146
txjs-cli push --with-tags-only="home,error"
146147
txjs-cli push --without-tags-only="custom"
147148
txjs-cli push --token=mytoken --secret=mysecret
149+
txjs-cli push en.json --parser=i18next
148150
TRANSIFEX_TOKEN=mytoken TRANSIFEX_SECRET=mysecret txjs-cli push
149151
```
150152

packages/cli/src/api/extract.js

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ const pug = require('pug');
99
const { parseHTMLTemplateFile } = require('./parsers/angularHTML');
1010
const { babelExtractPhrases } = require('./parsers/babel');
1111
const { extractVuePhrases } = require('./parsers/vue');
12+
const { extractI18NextPhrases } = require('./parsers/i18next');
1213

1314
/**
1415
* Parse file and extract phrases using AST
@@ -20,29 +21,49 @@ const { extractVuePhrases } = require('./parsers/vue');
2021
* @param {String[]} options.filterWithTags
2122
* @param {String[]} options.filterWithoutTags
2223
* @param {Boolean} options.useHashedKeys
24+
* @param {String} options.parser
2325
* @returns {Object}
2426
*/
2527
function extractPhrases(file, relativeFile, options = {}) {
2628
const HASHES = {};
2729
let source = fs.readFileSync(file, 'utf8');
2830

29-
// Handle simple templates that compile to javascript
31+
// i18next JSON
32+
if (options.parser === 'i18next') {
33+
extractI18NextPhrases(HASHES, source, relativeFile, options);
34+
return HASHES;
35+
}
36+
37+
// PUBG templates
3038
if (path.extname(file) === '.pug') {
3139
source = pug.compileClient(source);
32-
} else if (path.extname(file) === '.ejs') {
40+
babelExtractPhrases(HASHES, source, relativeFile, options);
41+
return HASHES;
42+
}
43+
44+
// EJS templates
45+
if (path.extname(file) === '.ejs') {
3346
const template = new ejs.Template(source);
3447
template.generateSource();
3548
source = template.source;
49+
babelExtractPhrases(HASHES, source, relativeFile, options);
50+
return HASHES;
3651
}
3752

53+
// HTML templates
3854
if (path.extname(file) === '.html') {
3955
parseHTMLTemplateFile(HASHES, file, relativeFile, options);
40-
} else if (path.extname(file) === '.vue') {
56+
return HASHES;
57+
}
58+
59+
// Vue templates
60+
if (path.extname(file) === '.vue') {
4161
extractVuePhrases(HASHES, source, relativeFile, options);
42-
} else if (path.extname(file) !== '.html') {
43-
babelExtractPhrases(HASHES, source, relativeFile, options);
62+
return HASHES;
4463
}
4564

65+
// default
66+
babelExtractPhrases(HASHES, source, relativeFile, options);
4667
return HASHES;
4768
}
4869

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
const { implodePlurals } = require('@transifex/native');
2+
const _ = require('lodash');
3+
const { mergePayload } = require('../merge');
4+
const { createPayload, isPayloadValid } = require('./utils');
5+
6+
const PLURAL_MAP = {
7+
_1: 'one',
8+
_2: 'two',
9+
_3: 'few',
10+
_4: 'many',
11+
_5: 'other',
12+
_one: 'one',
13+
_two: 'two',
14+
_few: 'few',
15+
_many: 'many',
16+
_other: 'other',
17+
};
18+
19+
/**
20+
* Parse i18next JSON v3 & v4 files
21+
*
22+
* @param {Object} HASHES A map of keys and content for phrases
23+
* @param {String} source The content we want to parse
24+
* @param {String} relativeFile occurence
25+
* @param {Object} options
26+
* @param {String[]} options.appendTags
27+
* @param {String[]} options.filterWithTags
28+
* @param {String[]} options.filterWithoutTags
29+
* @param {Boolean} options.useHashedKeys
30+
*/
31+
function extractI18NextPhrases(HASHES, source, relativeFile, options) {
32+
let json = {};
33+
try {
34+
json = JSON.parse(source);
35+
} catch (err) {
36+
return;
37+
}
38+
39+
const plurals = {};
40+
41+
_.each(json, (string, key) => {
42+
// nesting
43+
if (!_.isString(string) && !_.isArray(string) && _.isObject(string)) {
44+
_.each(string, (innerString, innerKey) => {
45+
if (!_.isString(innerString)) return;
46+
const partial = createPayload(innerString, {}, relativeFile, options);
47+
if (!isPayloadValid(partial, options)) return;
48+
49+
mergePayload(HASHES, {
50+
[`${key}.${innerKey}`]: {
51+
string: partial.string,
52+
meta: partial.meta,
53+
},
54+
});
55+
});
56+
return;
57+
}
58+
59+
if (!_.isString(string)) return;
60+
61+
let isPlural = false;
62+
_.each(_.keys(PLURAL_MAP), (suffix) => {
63+
if (_.endsWith(key, suffix)) {
64+
isPlural = true;
65+
const pluralKey = key.slice(0, -suffix.length);
66+
plurals[pluralKey] = plurals[pluralKey] || {};
67+
plurals[pluralKey][PLURAL_MAP[suffix]] = string;
68+
}
69+
});
70+
if (isPlural) return;
71+
72+
const partial = createPayload(string, {}, relativeFile, options);
73+
if (!isPayloadValid(partial, options)) return;
74+
75+
mergePayload(HASHES, {
76+
[key]: {
77+
string: partial.string,
78+
meta: partial.meta,
79+
},
80+
});
81+
});
82+
83+
// add plurals
84+
_.each(plurals, (plural, key) => {
85+
const partial = createPayload(implodePlurals(plural, 'count'), {}, relativeFile, options);
86+
if (!isPayloadValid(partial, options)) return;
87+
88+
mergePayload(HASHES, {
89+
[`${key}_txplural`]: {
90+
string: partial.string,
91+
meta: partial.meta,
92+
},
93+
});
94+
});
95+
}
96+
97+
module.exports = {
98+
extractI18NextPhrases,
99+
};

packages/cli/src/commands/push.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ class PushCommand extends Command {
7373
filterWithTags,
7474
filterWithoutTags,
7575
useHashedKeys,
76+
parser: flags.parser,
7677
};
7778

7879
_.each(allFiles, (file) => {
@@ -255,6 +256,7 @@ txjs-cli push --append-tags="master,release:2.5"
255256
txjs-cli push --with-tags-only="home,error"
256257
txjs-cli push --without-tags-only="custom"
257258
txjs-cli push --token=mytoken --secret=mysecret
259+
txjs-cli push en.json --parser=i18next
258260
TRANSIFEX_TOKEN=mytoken TRANSIFEX_SECRET=mysecret txjs-cli push
259261
`;
260262

@@ -307,6 +309,11 @@ PushCommand.flags = {
307309
description: 'CDS host URL',
308310
default: '',
309311
}),
312+
parser: flags.string({
313+
description: 'file parser to use',
314+
default: 'auto',
315+
options: ['auto', 'i18next'],
316+
}),
310317
'key-generator': flags.string({
311318
description: 'use hashed or source based keys',
312319
default: 'source',
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/* globals describe, it */
2+
3+
const { expect } = require('chai');
4+
const { extractPhrases } = require('../../src/api/extract');
5+
6+
describe('extractPhrases with i18next parser', () => {
7+
it('works with json v4', async () => {
8+
expect(await extractPhrases('test/fixtures/i18next_v4.json', 'v4.json', { parser: 'i18next' }))
9+
.to.deep.equal({
10+
'keyDeep.inner': {
11+
string: 'value',
12+
meta: { context: [], tags: [], occurrences: ['v4.json'] },
13+
},
14+
key: {
15+
string: 'value',
16+
meta: { context: [], tags: [], occurrences: ['v4.json'] },
17+
},
18+
keyNesting: {
19+
string: 'reuse $t(keyDeep.inner)',
20+
meta: { context: [], tags: [], occurrences: ['v4.json'] },
21+
},
22+
keyInterpolate: {
23+
string: 'replace this {{value}}',
24+
meta: { context: [], tags: [], occurrences: ['v4.json'] },
25+
},
26+
keyInterpolateUnescaped: {
27+
string: 'replace this {{- value}}',
28+
meta: { context: [], tags: [], occurrences: ['v4.json'] },
29+
},
30+
keyInterpolateWithFormatting: {
31+
string: 'replace this {{value, format}}',
32+
meta: { context: [], tags: [], occurrences: ['v4.json'] },
33+
},
34+
keyContext_male: {
35+
string: 'the male variant',
36+
meta: { context: [], tags: [], occurrences: ['v4.json'] },
37+
},
38+
keyContext_female: {
39+
string: 'the female variant',
40+
meta: { context: [], tags: [], occurrences: ['v4.json'] },
41+
},
42+
keyPluralSimple_txplural: {
43+
string: '{count, plural, one {the singular} other {the plural}}',
44+
meta: { context: [], tags: [], occurrences: ['v4.json'] },
45+
},
46+
keyPluralMultipleEgArabic_zero: {
47+
string: 'the plural form 0',
48+
meta: { context: [], tags: [], occurrences: ['v4.json'] },
49+
},
50+
keyPluralMultipleEgArabic_txplural: {
51+
string: '{count, plural, one {the plural form 1} two {the plural form 2} few {the plural form 3} many {the plural form 4} other {the plural form 5}}',
52+
meta: { context: [], tags: [], occurrences: ['v4.json'] },
53+
},
54+
'keyWithObjectValue.valueA': {
55+
string: 'return this with valueB',
56+
meta: { context: [], tags: [], occurrences: ['v4.json'] },
57+
},
58+
'keyWithObjectValue.valueB': {
59+
string: 'more text',
60+
meta: { context: [], tags: [], occurrences: ['v4.json'] },
61+
},
62+
});
63+
});
64+
});
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
{
2+
"key": "value",
3+
"keyDeep": {
4+
"inner": "value"
5+
},
6+
"keyNesting": "reuse $t(keyDeep.inner)",
7+
"keyInterpolate": "replace this {{value}}",
8+
"keyInterpolateUnescaped": "replace this {{- value}}",
9+
"keyInterpolateWithFormatting": "replace this {{value, format}}",
10+
"keyContext_male": "the male variant",
11+
"keyContext_female": "the female variant",
12+
"keyPluralSimple_one": "the singular",
13+
"keyPluralSimple_other": "the plural",
14+
"keyPluralMultipleEgArabic_zero": "the plural form 0",
15+
"keyPluralMultipleEgArabic_one": "the plural form 1",
16+
"keyPluralMultipleEgArabic_two": "the plural form 2",
17+
"keyPluralMultipleEgArabic_few": "the plural form 3",
18+
"keyPluralMultipleEgArabic_many": "the plural form 4",
19+
"keyPluralMultipleEgArabic_other": "the plural form 5",
20+
"keyWithArrayValue": ["multipe", "things"],
21+
"keyWithObjectValue": { "valueA": "return this with valueB", "valueB": "more text" }
22+
}

packages/i18next/.eslintrc.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"extends": ["airbnb-base"],
3+
"rules": {
4+
"import/no-extraneous-dependencies": "off",
5+
"no-plusplus": "off",
6+
"no-underscore-dangle": "off"
7+
},
8+
"ignorePatterns": ["/**/*.d.ts"]
9+
}

packages/i18next/.npmignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
.nyc_output
2+
tests
3+
.eslintrc.json

0 commit comments

Comments
 (0)