Skip to content

Commit c9c1017

Browse files
authored
Merge pull request #483 from robinborst95/feature/concat-expression
Add support for concat expressions in HBS files
2 parents d67b157 + a326070 commit c9c1017

File tree

7 files changed

+145
-38
lines changed

7 files changed

+145
-38
lines changed

README.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,30 @@ To prevent that from happening you can configure a `whitelist`, which accepts an
4343
array of regular expressions that will be checked when looking for unused
4444
translations.
4545

46+
### `analyzeConcatExpression`
47+
48+
If your template contains translations like this:
49+
```hbs
50+
{{t (concat "actions." (if @isEditing "save" "publish"))}}
51+
```
52+
then ember-intl-analyzer does not detect that `actions.save` and `actions.publish`
53+
are in fact used translations, so they can be incorrectly flagged as missing or
54+
unused. As the `concat` helper can make it harder to read, it's encouraged to
55+
rewrite it to for example:
56+
```hbs
57+
{{if @isEditing (t "actions.save") (t "actions.publish")}}
58+
```
59+
60+
However, if your application relies heavily on this `concat` helper, then rewriting
61+
may not be the best option for you. In that case, you can opt-in to analyze `concat`
62+
expressions too by setting the `analyzeConcatExpression` flag in the configuration file:
63+
64+
```js
65+
export default {
66+
analyzeConcatExpression: true,
67+
};
68+
```
69+
4670
### `externalPaths`
4771

4872
If your application uses translations provided by (external) addons, then those

__snapshots__/test.js.snap

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,22 @@
11
// Jest Snapshot v1, https://goo.gl/fbAQLP
22

3+
exports[`Test Fixtures concat-expression 1`] = `
4+
"[1/4] 🔍 Finding JS and HBS files...
5+
[2/4] 🔍 Searching for translations keys in JS and HBS files...
6+
[3/4] ⚙️ Checking for unused translations...
7+
[4/4] ⚙️ Checking for missing translations...
8+
9+
👏 No unused translations were found!
10+
11+
⚠️ Found 2 missing translations!
12+
13+
- prefix.simple-but-missing (used in app/templates/application.hbs)
14+
- prefix.key-that-should-exist-but-is-missing.value (used in app/templates/application.hbs)
15+
"
16+
`;
17+
18+
exports[`Test Fixtures concat-expression 2`] = `Map {}`;
19+
320
exports[`Test Fixtures decorators 1`] = `
421
"[1/4] 🔍 Finding JS and HBS files...
522
[2/4] 🔍 Searching for translations keys in JS and HBS files...
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{{t (concat "prefix" "." "simple")}}
2+
{{t (concat "prefix" "." "simple-but-missing")}}
3+
{{t (concat "prefix." (if true "with-if-first" "with-if-second"))}}
4+
{{t (concat "prefix.some-action" (if this.isCompact "-short"))}}
5+
{{t (concat "prefix." (if this.isEditing "edit" "new") ".label")}}
6+
{{t (if true "foo" (concat "prefix." (if this.isEditing "edit" "new") ".nested"))}}
7+
{{t (concat "prefix." this.dynamicKey ".not-missing")}}
8+
{{t (concat "prefix." (if true "a1" (if false "b1" (concat "c" (if false "1" "2")))) ".value")}}
9+
{{t (concat "prefix." (if true this.dynamicKey "key-that-should-exist") ".value")}}
10+
{{t (concat "prefix." (if true this.dynamicKey "key-that-should-exist-but-is-missing") ".value")}}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
prefix:
2+
simple: Simple concatenation
3+
with-if-first: First condition
4+
with-if-second: Second condition
5+
some-action: Action that can be long
6+
some-action-short: Action
7+
edit:
8+
label: Label on edit branch
9+
nested: Nested concat on edit branch
10+
new:
11+
label: Label on new branch
12+
nested: Nested concat on new branch
13+
a1:
14+
value: Value
15+
b1:
16+
value: Value
17+
c1:
18+
value: Value
19+
c2:
20+
value: Value
21+
key-that-should-exist:
22+
value: value
23+
foo: Foo
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
{{t "hbs-translation"}}
2+
{{t (concat "concat-" "expression.is-skipped")}}

index.js

Lines changed: 67 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -26,14 +26,16 @@ async function run(rootDir, options = {}) {
2626
const step = num => chalk.dim(`[${num}/${NUM_STEPS}]`);
2727

2828
let config = options.config || readConfig(rootDir);
29+
let analyzeConcatExpression = options.analyzeConcatExpression || config.analyzeConcatExpression;
30+
let analyzeOptions = { analyzeConcatExpression };
2931

3032
log(`${step(1)} 🔍 Finding JS and HBS files...`);
3133
let appFiles = await findAppFiles(rootDir);
3234
let inRepoFiles = await findInRepoFiles(rootDir);
3335
let files = [...appFiles, ...inRepoFiles];
3436

3537
log(`${step(2)} 🔍 Searching for translations keys in JS and HBS files...`);
36-
let usedTranslationKeys = await analyzeFiles(rootDir, files);
38+
let usedTranslationKeys = await analyzeFiles(rootDir, files, analyzeOptions);
3739

3840
log(`${step(3)} ⚙️ Checking for unused translations...`);
3941

@@ -161,11 +163,11 @@ function joinPaths(inputPathOrPaths, outputPaths) {
161163
}
162164
}
163165

164-
async function analyzeFiles(cwd, files) {
166+
async function analyzeFiles(cwd, files, options) {
165167
let allTranslationKeys = new Map();
166168

167169
for (let file of files) {
168-
let translationKeys = await analyzeFile(cwd, file);
170+
let translationKeys = await analyzeFile(cwd, file, options);
169171

170172
for (let key of translationKeys) {
171173
if (allTranslationKeys.has(key)) {
@@ -179,17 +181,17 @@ async function analyzeFiles(cwd, files) {
179181
return allTranslationKeys;
180182
}
181183

182-
async function analyzeFile(cwd, file) {
184+
async function analyzeFile(cwd, file, options) {
183185
let content = fs.readFileSync(`${cwd}/${file}`, 'utf8');
184186
let extension = path.extname(file).toLowerCase();
185187

186188
if (extension === '.js') {
187-
return analyzeJsFile(content);
189+
return analyzeJsFile(content, options);
188190
} else if (extension === '.hbs') {
189-
return analyzeHbsFile(content);
191+
return analyzeHbsFile(content, options);
190192
} else if (extension === '.emblem') {
191193
let hbs = Emblem.compile(content, { quiet: true });
192-
return analyzeHbsFile(hbs);
194+
return analyzeHbsFile(hbs, options);
193195
} else {
194196
throw new Error(`Unknown extension: ${extension} (${file})`);
195197
}
@@ -232,50 +234,77 @@ async function analyzeJsFile(content) {
232234
return translationKeys;
233235
}
234236

235-
async function analyzeHbsFile(content) {
237+
async function analyzeHbsFile(content, { analyzeConcatExpression = false }) {
236238
let translationKeys = new Set();
237239

238240
// parse the HBS file
239241
let ast = Glimmer.preprocess(content);
240242

243+
function findKeysInIfExpression(node) {
244+
let keysInFirstParam = findKeysInNode(node.params[1]);
245+
let keysInSecondParam = node.params.length > 2 ? findKeysInNode(node.params[2]) : [''];
246+
247+
return [...keysInFirstParam, ...keysInSecondParam];
248+
}
249+
250+
function findKeysInConcatExpression(node) {
251+
let potentialKeys = [''];
252+
253+
for (let param of node.params) {
254+
let keysInParam = findKeysInNode(param);
255+
256+
if (keysInParam.length === 0) return [];
257+
258+
potentialKeys = potentialKeys.reduce((newPotentialKeys, potentialKey) => {
259+
for (let key of keysInParam) {
260+
newPotentialKeys.push(potentialKey + key);
261+
}
262+
263+
return newPotentialKeys;
264+
}, []);
265+
}
266+
267+
return potentialKeys;
268+
}
269+
270+
function findKeysInNode(node) {
271+
if (!node) return [];
272+
273+
if (node.type === 'StringLiteral') {
274+
return [node.value];
275+
} else if (node.type === 'SubExpression' && node.path.original === 'if') {
276+
return findKeysInIfExpression(node);
277+
} else if (
278+
analyzeConcatExpression &&
279+
node.type === 'SubExpression' &&
280+
node.path.original === 'concat'
281+
) {
282+
return findKeysInConcatExpression(node);
283+
}
284+
285+
return [];
286+
}
287+
288+
function processNode(node) {
289+
if (node.path.type !== 'PathExpression') return;
290+
if (node.path.original !== 't') return;
291+
if (node.params.length === 0) return;
292+
293+
for (let key of findKeysInNode(node.params[0])) {
294+
translationKeys.add(key);
295+
}
296+
}
297+
241298
// find translation keys in the syntax tree
242299
Glimmer.traverse(ast, {
243300
// handle {{t "foo"}} case
244301
MustacheStatement(node) {
245-
if (node.path.type !== 'PathExpression') return;
246-
if (node.path.original !== 't') return;
247-
if (node.params.length === 0) return;
248-
249-
let firstParam = node.params[0];
250-
if (firstParam.type === 'StringLiteral') {
251-
translationKeys.add(firstParam.value);
252-
} else if (firstParam.type === 'SubExpression' && firstParam.path.original === 'if') {
253-
if (firstParam.params[1].type === 'StringLiteral') {
254-
translationKeys.add(firstParam.params[1].value);
255-
}
256-
if (firstParam.params[2].type === 'StringLiteral') {
257-
translationKeys.add(firstParam.params[2].value);
258-
}
259-
}
302+
processNode(node);
260303
},
261304

262305
// handle {{some-component foo=(t "bar")}} case
263306
SubExpression(node) {
264-
if (node.path.type !== 'PathExpression') return;
265-
if (node.path.original !== 't') return;
266-
if (node.params.length === 0) return;
267-
268-
let firstParam = node.params[0];
269-
if (firstParam.type === 'StringLiteral') {
270-
translationKeys.add(firstParam.value);
271-
} else if (firstParam.type === 'SubExpression' && firstParam.path.original === 'if') {
272-
if (firstParam.params[1].type === 'StringLiteral') {
273-
translationKeys.add(firstParam.params[1].value);
274-
}
275-
if (firstParam.params[2].type === 'StringLiteral') {
276-
translationKeys.add(firstParam.params[2].value);
277-
}
278-
}
307+
processNode(node);
279308
},
280309
});
281310

test.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,10 @@ describe('Test Fixtures', () => {
1212
'unused-translations',
1313
'in-repo-translations',
1414
'external-addon-translations',
15+
'concat-expression',
1516
];
1617
let fixturesWithFix = ['remove-unused-translations', 'remove-unused-translations-nested'];
18+
let fixturesWithConcat = ['concat-expression'];
1719
let fixturesWithConfig = {
1820
'external-addon-translations': {
1921
externalPaths: ['@*/*', 'external-addon'],
@@ -41,6 +43,7 @@ describe('Test Fixtures', () => {
4143
color: false,
4244
writeToFile,
4345
config: fixturesWithConfig[fixture],
46+
analyzeConcatExpression: fixturesWithConcat.includes(fixture),
4447
});
4548

4649
let expectedReturnValue = fixturesWithErrors.includes(fixture) ? 1 : 0;

0 commit comments

Comments
 (0)