Skip to content

Commit

Permalink
Update parser to support multiple fallback keys (#251)
Browse files Browse the repository at this point in the history
* Remove deprecated and incompatible `acorn-dynamic-import`

Signed-off-by: Scott González <[email protected]>

* Properly escape function name regex

Signed-off-by: Scott González <[email protected]>

* Support fallback keys

Signed-off-by: Scott González <[email protected]>

* test: update test fixtures

---------

Signed-off-by: Scott González <[email protected]>
Co-authored-by: cheton <[email protected]>
  • Loading branch information
scottgonzalez and cheton authored Aug 21, 2023
1 parent 108d443 commit 9f9d300
Show file tree
Hide file tree
Showing 5 changed files with 105 additions and 59 deletions.
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,6 @@
],
"dependencies": {
"acorn": "^8.0.4",
"acorn-dynamic-import": "^4.0.0",
"acorn-jsx": "^5.3.1",
"acorn-stage3": "^4.0.0",
"acorn-walk": "^8.0.0",
Expand Down
5 changes: 0 additions & 5 deletions src/acorn-jsx-walk.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,12 @@
// Originally from: https://github.com/sderosiaux/acorn-jsx-walk
import { simple as walk, base } from 'acorn-walk';
import { DynamicImportKey } from 'acorn-dynamic-import';

Object.assign(base, {
FieldDefinition(node, state, callback) {
if (node.value !== null) {
callback(node.value, state);
}
},

// Workaround with `acorn` and `acorn-dynamic-import`
// https://github.com/kristoferbaxter/dynamic-walk/blob/master/workaround.js
[DynamicImportKey]: () => {},
});

// Extends acorn walk with JSX elements
Expand Down
138 changes: 85 additions & 53 deletions src/parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,23 @@ class Parser {
.replace(regex.ns, ns);
}

fixStringAfterRegExpAsArray(strToFix) {
let fixedString = _.trim(strToFix);
const firstChar = fixedString[0];
const lastChar = fixedString[fixedString.length - 1];

if (firstChar === '[' && lastChar === ']') {
return fixedString
.substring(1, fixedString.length - 1)
.split(',')
.map((part) => {
return this.fixStringAfterRegExp(part, true);
});
}

return [this.fixStringAfterRegExp(strToFix, true)];
}

fixStringAfterRegExp(strToFix) {
const options = this.options;
let fixedString = _.trim(strToFix); // Remove leading and trailing whitespace
Expand Down Expand Up @@ -442,25 +459,38 @@ class Parser {
}

const matchFuncs = funcs
.map(func => ('(?:' + func + ')'))
.join('|')
.replace(/\./g, '\\.');
.map(func => ('(?:' + _.escapeRegExp(func) + ')'))
.join('|');
// `\s` matches a single whitespace character, which includes spaces, tabs, form feeds, line feeds and other unicode spaces.
const matchSpecialCharacters = '[\\r\\n\\s]*';
const stringGroup =
matchSpecialCharacters + '(' +
const string =
// backtick (``)
'`(?:[^`\\\\]|\\\\(?:.|$))*`' +
'|' +
// double quotes ("")
'"(?:[^"\\\\]|\\\\(?:.|$))*"' +
'|' +
// single quote ('')
'\'(?:[^\'\\\\]|\\\\(?:.|$))*\'' +
'\'(?:[^\'\\\\]|\\\\(?:.|$))*\'';
const stringGroup =
matchSpecialCharacters + '(' +
string +
')' + matchSpecialCharacters;
const stringNoGroup =
matchSpecialCharacters + '(?:' +
string +
')' + matchSpecialCharacters;
const keys = '(' +
stringNoGroup +
'|' +
'\\[' +
stringNoGroup +
'(?:[\\,]' + stringNoGroup + ')?' +
'\\]' +
')';
const pattern = '(?:(?:^\\s*)|[^a-zA-Z0-9_])' +
'(?:' + matchFuncs + ')' +
'\\(' + stringGroup +
'\\(' + keys +
'(?:[\\,]' + stringGroup + ')?' +
'[\\,\\)]';
const re = new RegExp(pattern, 'gim');
Expand All @@ -470,62 +500,64 @@ class Parser {
const options = {};
const full = r[0];

let key = this.fixStringAfterRegExp(r[1], true);
if (!key) {
continue;
}

if (r[2] !== undefined) {
const defaultValue = this.fixStringAfterRegExp(r[2], false);
if (!defaultValue) {
let keys = this.fixStringAfterRegExpAsArray(r[1]);
for (const key of keys) {
if (!key) {
continue;
}
options.defaultValue = defaultValue;
}

const endsWithComma = (full[full.length - 1] === ',');
if (endsWithComma) {
const { propsFilter } = { ...opts };
if (r[2] !== undefined) {
const defaultValue = this.fixStringAfterRegExp(r[2], false);
if (!defaultValue) {
continue;
}
options.defaultValue = defaultValue;
}

const endsWithComma = (full[full.length - 1] === ',');
if (endsWithComma) {
const { propsFilter } = { ...opts };

let code = matchBalancedParentheses(content.substr(re.lastIndex));

let code = matchBalancedParentheses(content.substr(re.lastIndex));
if (typeof propsFilter === 'function') {
code = propsFilter(code);
}

if (typeof propsFilter === 'function') {
code = propsFilter(code);
try {
const syntax = code.trim() !== '' ? parse('(' + code + ')') : {};

const props = _.get(syntax, 'body[0].expression.properties') || [];
// http://i18next.com/docs/options/
const supportedOptions = [
'defaultValue',
'defaultValue_plural',
'count',
'context',
'ns',
'keySeparator',
'nsSeparator',
'metadata',
];

props.forEach((prop) => {
if (_.includes(supportedOptions, prop.key.name)) {
options[prop.key.name] = this.optionsBuilder(prop);
}
});
} catch (err) {
this.error(`Unable to parse code "${code}"`);
this.error(err);
}
}

try {
const syntax = code.trim() !== '' ? parse('(' + code + ')') : {};

const props = _.get(syntax, 'body[0].expression.properties') || [];
// http://i18next.com/docs/options/
const supportedOptions = [
'defaultValue',
'defaultValue_plural',
'count',
'context',
'ns',
'keySeparator',
'nsSeparator',
'metadata',
];

props.forEach((prop) => {
if (_.includes(supportedOptions, prop.key.name)) {
options[prop.key.name] = this.optionsBuilder(prop);
}
});
} catch (err) {
this.error(`Unable to parse code "${code}"`);
this.error(err);
if (customHandler) {
customHandler(key, options);
continue;
}
}

if (customHandler) {
customHandler(key, options);
continue;
this.set(key, options);
}

this.set(key, options);
}

return this;
Expand Down
4 changes: 4 additions & 0 deletions test/fixtures/fallback.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
i18next.t(['error.404', 'error.unspecific']);

const err_500 = 500;
i18next.t([`error.${err_500}`, 'error.unspecific']);
16 changes: 16 additions & 0 deletions test/parser.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1222,3 +1222,19 @@ test('allowDynamicKeys', () => {
}
});
});

test('Should support fallback keys', () => {
const parser = new Parser();
const content = fs.readFileSync(path.resolve(__dirname, 'fixtures/fallback.js'), 'utf-8');
parser.parseFuncFromString(content, {});
expect(parser.get()).toEqual({
en: {
translation: {
'error': {
'unspecific': '', // Something wen wrong.
'404': '', // The page was not found.
},
}
}
});
});

0 comments on commit 9f9d300

Please sign in to comment.