Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: rework the nodesToString function to output expected element tags #234

Merged
merged 7 commits into from
Jul 8, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 41 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,10 +123,15 @@ module.exports = {
fallbackKey: function(ns, value) {
return value;
},

// https://react.i18next.com/latest/trans-component#usage-with-simple-html-elements-like-less-than-br-greater-than-and-others-v10.4.0
supportBasicHtmlNodes: true, // Enables keeping the name of simple nodes (e.g. <br/>) in translations instead of indexed keys.
keepBasicHtmlNodesFor: ['br', 'strong', 'i', 'p'], // Which nodes are allowed to be kept in translations during defaultValue generation of <Trans>.

// https://github.com/acornjs/acorn/tree/master/acorn#interface
acorn: {
ecmaVersion: 2020,
sourceType: 'module', // defaults to 'module'
// Check out https://github.com/acornjs/acorn/tree/master/acorn#interface for additional options
}
},
lngs: ['en','de'],
Expand All @@ -148,7 +153,9 @@ module.exports = {
interpolation: {
prefix: '{{',
suffix: '}}'
}
},
metadata: {},
allowDynamicKeys: false,
},
transform: function customTransform(file, enc, done) {
"use strict";
Expand Down Expand Up @@ -498,18 +505,28 @@ Below are the configuration options with their default values:
sort: false,
attr: {
list: ['data-i18n'],
extensions: ['.html', '.htm']
extensions: ['.html', '.htm'],
},
func: {
list: ['i18next.t', 'i18n.t'],
extensions: ['.js', '.jsx']
extensions: ['.js', '.jsx'],
},
trans: {
component: 'Trans',
i18nKey: 'i18nKey',
defaultsKey: 'defaults',
extensions: ['.js', '.jsx'],
fallbackKey: false
fallbackKey: false,

// https://react.i18next.com/latest/trans-component#usage-with-simple-html-elements-like-less-than-br-greater-than-and-others-v10.4.0
supportBasicHtmlNodes: true, // Enables keeping the name of simple nodes (e.g. <br/>) in translations instead of indexed keys.
keepBasicHtmlNodesFor: ['br', 'strong', 'i', 'p'], // Which nodes are allowed to be kept in translations during defaultValue generation of <Trans>.

// https://github.com/acornjs/acorn/tree/master/acorn#interface
acorn: {
ecmaVersion: 2020,
sourceType: 'module', // defaults to 'module'
},
},
lngs: ['en'],
ns: ['translation'],
Expand All @@ -520,7 +537,7 @@ Below are the configuration options with their default values:
loadPath: 'i18n/{{lng}}/{{ns}}.json',
savePath: 'i18n/{{lng}}/{{ns}}.json',
jsonIndent: 2,
lineEnding: '\n'
lineEnding: '\n',
},
nsSeparator: ':',
keySeparator: '.',
Expand All @@ -529,8 +546,10 @@ Below are the configuration options with their default values:
contextDefaultValues: [],
interpolation: {
prefix: '{{',
suffix: '}}'
}
suffix: '}}',
},
metadata: {},
allowDynamicKeys: false,
}
```

Expand Down Expand Up @@ -606,7 +625,17 @@ If an `Object` is supplied, you can specify a list of extensions, or override th
i18nKey: 'i18nKey',
defaultsKey: 'defaults',
extensions: ['.js', '.jsx'],
fallbackKey: false
fallbackKey: false,

// https://react.i18next.com/latest/trans-component#usage-with-simple-html-elements-like-less-than-br-greater-than-and-others-v10.4.0
supportBasicHtmlNodes: true, // Enables keeping the name of simple nodes (e.g. <br/>) in translations instead of indexed keys.
keepBasicHtmlNodesFor: ['br', 'strong', 'i', 'p'], // Which nodes are allowed to be kept in translations during defaultValue generation of <Trans>.

// https://github.com/acornjs/acorn/tree/master/acorn#interface
acorn: {
ecmaVersion: 2020,
sourceType: 'module', // defaults to 'module'
},
}
}
```
Expand Down Expand Up @@ -819,9 +848,6 @@ interpolation options
}
```

## Integration Guide
Checkout [Integration Guide](https://github.com/i18next/i18next-scanner/wiki/Integration-Guide) to learn how to integrate with [React](https://github.com/i18next/i18next-scanner/wiki/Integration-Guide#react), [Gettext Style I18n](https://github.com/i18next/i18next-scanner/wiki/Integration-Guide#gettext-style-i18n), and [Handlebars](https://github.com/i18next/i18next-scanner/wiki/Integration-Guide#handlebars).

#### metadata

Type: `Object` Default: `{}`
Expand Down Expand Up @@ -881,6 +907,9 @@ Example Usage:
done();
```

## Integration Guide
Checkout [Integration Guide](https://github.com/i18next/i18next-scanner/wiki/Integration-Guide) to learn how to integrate with [React](https://github.com/i18next/i18next-scanner/wiki/Integration-Guide#react), [Gettext Style I18n](https://github.com/i18next/i18next-scanner/wiki/Integration-Guide#gettext-style-i18n), and [Handlebars](https://github.com/i18next/i18next-scanner/wiki/Integration-Guide#handlebars).

## License

MIT
4 changes: 2 additions & 2 deletions bin/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@

const path = require('path');
const program = require('commander');
const ensureArray = require('ensure-array');
const { ensureArray } = require('ensure-type');
const sort = require('gulp-sort');
const vfs = require('vinyl-fs');
const scanner = require('../lib').default;
const scanner = require('../lib');
const pkg = require('../package.json');

program
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "i18next-scanner",
"version": "3.3.0",
"version": "4.0.0",
"description": "Scan your code, extract translation keys/values, and merge them into i18n resource files.",
"homepage": "https://github.com/i18next/i18next-scanner",
"author": "Cheton Wu <[email protected]>",
Expand Down Expand Up @@ -57,7 +57,7 @@
"clone-deep": "^4.0.0",
"commander": "^9.0.0",
"deepmerge": "^4.0.0",
"ensure-array": "^1.0.0",
"ensure-type": "^1.5.0",
"eol": "^0.9.1",
"esprima-next": "^5.7.0",
"gulp-sort": "^2.0.0",
Expand Down
16 changes: 9 additions & 7 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/* eslint-disable no-buffer-constructor */
/* eslint-disable import/no-import-module-exports */
import fs from 'fs';
import path from 'path';
import eol from 'eol';
Expand Down Expand Up @@ -93,7 +93,7 @@ const flush = (parser, customFlush) => {
contents = Buffer.from(text);
} catch (e) {
// Fallback to "new Buffer(string[, encoding])" which is deprecated since Node.js v6.0.0
contents = new Buffer(text);
contents = new Buffer(text); // eslint-disable-line no-buffer-constructor
}

this.push(new VirtualFile({
Expand Down Expand Up @@ -121,9 +121,11 @@ const createStream = (options, customTransform, customFlush) => {
return stream;
};

export default (...args) => createStream(...args);
// Convenience API
module.exports = (...args) => module.exports.createStream(...args);

export {
createStream,
Parser,
};
// Basic API
module.exports.createStream = createStream;

// Parser
module.exports.Parser = Parser;
66 changes: 52 additions & 14 deletions src/nodes-to-string.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { ensureArray, ensureBoolean, ensureString } from 'ensure-type';
import _get from 'lodash/get';

const isJSXText = (node) => {
Expand Down Expand Up @@ -32,16 +33,26 @@ const isObjectExpression = (node) => {
return node.type === 'ObjectExpression';
};

const nodesToString = (nodes, code) => {
let memo = '';
let nodeIndex = 0;
nodes.forEach((node, i) => {
if (isJSXText(node) || isStringLiteral(node)) {
const value = (node.value)
.replace(/^[\r\n]+\s*/g, '') // remove leading spaces containing a leading newline character
.replace(/[\r\n]+\s*$/g, '') // remove trailing spaces containing a leading newline character
.replace(/[\r\n]+\s*/g, ' '); // replace spaces containing a leading newline character with a single space character
const trimValue = value => ensureString(value)
.replace(/^[\r\n]+\s*/g, '') // remove leading spaces containing a leading newline character
.replace(/[\r\n]+\s*$/g, '') // remove trailing spaces containing a leading newline character
.replace(/[\r\n]+\s*/g, ' '); // replace spaces containing a leading newline character with a single space character

const nodesToString = (nodes, options) => {
const supportBasicHtmlNodes = ensureBoolean(options?.supportBasicHtmlNodes);
const keepBasicHtmlNodesFor = ensureArray(options?.keepBasicHtmlNodesFor);
const filteredNodes = ensureArray(nodes)
.filter(node => {
if (isJSXText(node)) {
return trimValue(node.value);
}
return true;
});

let memo = '';
filteredNodes.forEach((node, nodeIndex) => {
if (isJSXText(node)) {
const value = trimValue(node.value);
if (!value) {
return;
}
Expand All @@ -55,17 +66,44 @@ const nodesToString = (nodes, code) => {
} if (isStringLiteral(expression)) {
memo += expression.value;
} else if (isObjectExpression(expression) && (_get(expression, 'properties[0].type') === 'Property')) {
memo += `<${nodeIndex}>{{${expression.properties[0].key.name}}}</${nodeIndex}>`;
memo += `{{${expression.properties[0].key.name}}}`;
} else {
console.error(`Unsupported JSX expression. Only static values or {{interpolation}} blocks are supported. Got ${expression.type}:`);
console.error(code.slice(node.start, node.end));
console.error(ensureString(options?.code).slice(node.start, node.end));
console.error(node.expression);
}
} else if (node.children) {
memo += `<${nodeIndex}>${nodesToString(node.children, code)}</${nodeIndex}>`;
}
const nodeType = node.openingElement?.name?.name;
const selfClosing = node.openingElement?.selfClosing;
const attributeCount = ensureArray(node.openingElement?.attributes).length;
const filteredChildNodes = ensureArray(node.children)
.filter(childNode => {
if (isJSXText(childNode)) {
return trimValue(childNode.value);
}
return true;
});
const childCount = filteredChildNodes.length;
const firstChildNode = filteredChildNodes[0];
const shouldKeepChild = supportBasicHtmlNodes && keepBasicHtmlNodesFor.indexOf(node.openingElement?.name?.name) > -1;

++nodeIndex;
if (selfClosing && shouldKeepChild && (attributeCount === 0)) {
// actual e.g. lorem <br/> ipsum
// expected e.g. lorem <br/> ipsum
memo += `<${nodeType}/>`;
} else if ((childCount === 0 && !shouldKeepChild) || (childCount === 0 && attributeCount !== 0)) {
// actual e.g. lorem <hr className="test" /> ipsum
// expected e.g. lorem <0></0> ipsum
memo += `<${nodeIndex}></${nodeIndex}>`;
} else if (shouldKeepChild && (attributeCount === 0) && (childCount === 1) && (isJSXText(firstChildNode) || isStringLiteral(firstChildNode?.expression))) {
// actual e.g. dolor <strong>bold</strong> amet
// expected e.g. dolor <strong>bold</strong> amet
memo += `<${nodeType}>${nodesToString(node.children, options)}</${nodeType}>`;
} else {
// regular case mapping the inner children
memo += `<${nodeIndex}>${nodesToString(node.children, options)}</${nodeIndex}>`;
}
}
});

return memo;
Expand Down
20 changes: 17 additions & 3 deletions src/parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import acornStage3 from 'acorn-stage3';
import chalk from 'chalk';
import cloneDeep from 'clone-deep';
import deepMerge from 'deepmerge';
import ensureArray from 'ensure-array';
import { ensureArray } from 'ensure-type';
import { parse } from 'esprima-next';
import _ from 'lodash';
import parse5 from 'parse5';
Expand Down Expand Up @@ -43,11 +43,13 @@ const defaults = {
defaultsKey: 'defaults',
extensions: ['.js', '.jsx'],
fallbackKey: false,
supportBasicHtmlNodes: true, // Enables keeping the name of simple nodes (e.g. <br/>) in translations instead of indexed keys.
keepBasicHtmlNodesFor: ['br', 'strong', 'i', 'p'], // Which nodes are allowed to be kept in translations during defaultValue generation of <Trans>.
acorn: {
ecmaVersion: 2020, // defaults to 2020
sourceType: 'module', // defaults to 'module'
// Check out https://github.com/acornjs/acorn/tree/master/acorn#interface for additional options
}
},
},

lngs: ['en'], // array of supported languages
Expand Down Expand Up @@ -171,6 +173,12 @@ const normalizeOptions = (options) => {
if (_.isUndefined(_.get(options, 'trans.acorn'))) {
_.set(options, 'trans.acorn', defaults.trans.acorn);
}
if (_.isUndefined(_.get(options, 'trans.supportBasicHtmlNodes'))) {
_.set(options, 'trans.supportBasicHtmlNodes', defaults.trans.supportBasicHtmlNodes);
}
if (_.isUndefined(_.get(options, 'trans.keepBasicHtmlNodesFor'))) {
_.set(options, 'trans.keepBasicHtmlNodesFor', defaults.trans.keepBasicHtmlNodesFor);
}
}

// Resource
Expand Down Expand Up @@ -538,6 +546,8 @@ class Parser {
defaultsKey = this.options.trans.defaultsKey, // string
fallbackKey, // boolean|function
acorn: acornOptions = this.options.trans.acorn, // object
supportBasicHtmlNodes = this.options.trans.supportBasicHtmlNodes, // boolean
keepBasicHtmlNodesFor = this.options.trans.keepBasicHtmlNodesFor, // array
} = { ...opts };

const parseJSXElement = (node, code) => {
Expand Down Expand Up @@ -634,7 +644,11 @@ class Parser {
const tOptions = attr.tOptions;
const options = {
...tOptions,
defaultValue: defaultsString || nodesToString(node.children, code),
defaultValue: defaultsString || nodesToString(node.children, {
code,
supportBasicHtmlNodes,
keepBasicHtmlNodesFor,
}),
fallbackKey: fallbackKey || this.options.trans.fallbackKey
};

Expand Down
Loading