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

Add the ability to analyse nested translation folders #704

Merged
merged 1 commit into from
Feb 7, 2025
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
57 changes: 32 additions & 25 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
ember-intl-analyzer
==============================================================================
# ember-intl-analyzer

Find unused translations in your Ember.js projects

Expand All @@ -9,37 +8,31 @@ Find unused translations in your Ember.js projects
> We offer consulting, training, and team augmentation for Ember.js – check out
> our [website](https://mainmatter.com/ember-consulting/) to learn more!
Usage
------------------------------------------------------------------------------
## Usage

```bash
npx ember-intl-analyzer
```


Configuration
------------------------------------------------------------------------------
## Configuration

ember-intl-analyzer can be configured by creating a `config/ember-intl-analyzer.js`
file in your app:

```js
export default {
whitelist: [
/^countries\./,
/^currency\./,
/^validations\.errors\./,
/^[^.]+\.warnings\.[^.]+$/,
],
whitelist: [/^countries\./, /^currency\./, /^validations\.errors\./, /^[^.]+\.warnings\.[^.]+$/],
};
```

### `whitelist`

If you use dynamic translations keys like this:

```js
this.intl.t(`countries.${code}`)
this.intl.t(`countries.${code}`);
```

then ember-intl-analyzer can not easily understand what translation keys are
being used here. In that case it will ignore the dynamic translation key and
show the corresponding translations as unused.
Expand All @@ -64,15 +57,18 @@ export default {
### `analyzeConcatExpression`

If your template contains translations like this:

```hbs
{{t (concat "actions." (if @isEditing "save" "publish"))}}
{{t (concat 'actions.' (if @isEditing 'save' 'publish'))}}
```

then ember-intl-analyzer does not detect that `actions.save` and `actions.publish`
are in fact used translations, so they can be incorrectly flagged as missing or
unused. As the `concat` helper can make it harder to read, it's encouraged to
rewrite it to for example:

```hbs
{{if @isEditing (t "actions.save") (t "actions.publish")}}
{{if @isEditing (t 'actions.save') (t 'actions.publish')}}
```

However, if your application relies heavily on this `concat` helper, then rewriting
Expand Down Expand Up @@ -100,9 +96,11 @@ export default {
This example will try to find translation files in `node_modules/my-addon/translations`.
Patterns supported by [`globby`](https://www.npmjs.com/package/globby) are also
possible here, e.g. this:

```js
externalPaths: ['@*/*']
externalPaths: ['@*/*'];
```

will look up translations in scoped addons like `node_modules/@company/scoped-addon/translations`.

### `translationFiles`
Expand All @@ -122,13 +120,24 @@ This example will try to find all `en.yaml` files in the different `translations
folders, but any patterns supported by [`globby`](https://www.npmjs.com/package/globby) are also
possible here.

### `wrapTranslationsWithNamespace`

If you are nesting your translations with `ember-intl`s [`wrapTranslationsWithNamespace`](https://ember-intl.github.io/ember-intl/docs/advanced/configuration#wraptranslationswithnamespace) you will need to set the corresponding property within your `ember-intl-analyzer` config file.

```js
export default {
wrapTranslationsWithNamespace: true,
};
```

### `babelParserPlugins` `extensions`

If your application uses doesn't parse correctly because it requires a specific babel plugin you can specifiy them in the config file under the key `babelParserPlugins` a list on plugins can be found [here](https://babeljs.io/docs/en/babel-parser#plugins).

For example if you would like typescript support you can specify the `typescript` plugin, although please note if the plugin introduces a new file extension you will also need to specifiy that in the `extensions` property. See the examples below.

Typescript example

```js
export default {
babelParserPlugins: ['typescript'],
Expand All @@ -137,6 +146,7 @@ export default {
```

Jsx example

```js
export default {
babelParserPlugins: ['jsx'],
Expand All @@ -145,6 +155,7 @@ export default {
```

Gts example

```js
export default {
babelParserPlugins: ['typescript'],
Expand All @@ -153,6 +164,7 @@ export default {
```

### `--fix`

If your application has a lot of unused translations you can run the command with
the `--fix` to remove them. Remember to double check your translations as dynamic
translations need to be whitelisted or they will be removed!
Expand All @@ -172,23 +184,18 @@ export default {
};
```

Caveats
------------------------------------------------------------------------------
## Caveats

There are a number of things that we do not support yet. Have a look at the
[Issues](https://github.com/Mainmatter/ember-intl-analyzer/issues) before using
this project.


Related
------------------------------------------------------------------------------
## Related

- [ember-intl](https://github.com/ember-intl/ember-intl) – Internationalization
addon for Ember.js


License
------------------------------------------------------------------------------
## License

This projects is developed by and © [Mainmatter GmbH](http://mainmatter.com)
and contributors. It is released under the [MIT License](LICENSE.md).
14 changes: 14 additions & 0 deletions __snapshots__/test.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,20 @@ exports[`Test Fixtures missing-translations 1`] = `

exports[`Test Fixtures missing-translations 2`] = `Map {}`;

exports[`Test Fixtures nested-keys 1`] = `
"[1/4] 🔍 Finding JS and HBS files...
[2/4] 🔍 Searching for translations keys in JS and HBS files...
[3/4] ⚙️ Checking for unused translations...
[4/4] ⚙️ Checking for missing translations...
👏 No unused translations were found!
👏 No missing translations were found!
"
`;

exports[`Test Fixtures nested-keys 2`] = `Map {}`;

exports[`Test Fixtures no-issues 1`] = `
"[1/4] 🔍 Finding JS and HBS files...
[2/4] 🔍 Searching for translations keys in JS and HBS files...
Expand Down
9 changes: 9 additions & 0 deletions fixtures/nested-keys/app/controllers/application.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import Controller from '@ember/controller';
import { tracked } from '@glimmer/tracking';

export default class ApplicationController extends Controller {
@tracked foo;
foo() {
return this.intl.t('foo.bar.hello');
}
}
2 changes: 2 additions & 0 deletions fixtures/nested-keys/app/templates/application.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
{{t 'foo.bar.hello'}}
{{t 'foo.bar.bing.pot'}}
3 changes: 3 additions & 0 deletions fixtures/nested-keys/translations/foo/bar/de-at.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
hello: Hallo
bing:
pot: wow!
3 changes: 3 additions & 0 deletions fixtures/nested-keys/translations/foo/bar/en.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
hello: Hello
bing:
pot: wow!
39 changes: 28 additions & 11 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ async function run(rootDir, options = {}) {
let userPlugins = config.babelParserPlugins || [];
let userExtensions = config.extensions || [];
let customHelpers = config.helpers || [];
let wrapTranslationsWithNamespace = config.wrapTranslationsWithNamespace || false;
let includeGtsExtension = userExtensions.includes('.gts');

userExtensions = userExtensions.map(extension =>
Expand All @@ -59,10 +60,15 @@ async function run(rootDir, options = {}) {

let ownTranslationFiles = await findOwnTranslationFiles(rootDir, config);
let externalTranslationFiles = await findExternalTranslationFiles(rootDir, config);
let existingOwnTranslationKeys = await analyzeTranslationFiles(rootDir, ownTranslationFiles);
let existingOwnTranslationKeys = await analyzeTranslationFiles(
rootDir,
ownTranslationFiles,
wrapTranslationsWithNamespace
);
let existingExternalTranslationKeys = await analyzeTranslationFiles(
rootDir,
externalTranslationFiles
externalTranslationFiles,
wrapTranslationsWithNamespace
);
let existingTranslationKeys = mergeMaps(
existingOwnTranslationKeys,
Expand Down Expand Up @@ -417,21 +423,32 @@ async function analyzeHbsFile(content, { analyzeConcatExpression = false, helper
return translationKeys;
}

async function analyzeTranslationFiles(cwd, files) {
let existingTranslationKeys = new Map();
function extractNestedPath(filepath) {
const regex = /translations\/(.+)\/(?:[a-z]{2}\.(?:ya?ml|json)|.*)/;
return filepath.replace(regex, '$1').replace(/\//g, '.');
}

async function analyzeTranslationFiles(cwd, files, wrapTranslationsWithNamespace) {
let existingTranslationKeys = new Map();
for (let file of files) {
let prefix = '';
if (wrapTranslationsWithNamespace) {
prefix = extractNestedPath(file) + '.';
}
let content = fs.readFileSync(`${cwd}/${file}`, 'utf8');
let translations = YAML.parse(content); // json is valid yaml
forEachTranslation(translations, key => {
if (!existingTranslationKeys.has(key)) {
existingTranslationKeys.set(key, new Set());
}
forEachTranslation(
translations,
key => {
if (!existingTranslationKeys.has(key)) {
existingTranslationKeys.set(key, new Set());
}

existingTranslationKeys.get(key).add(file);
});
existingTranslationKeys.get(key).add(file);
},
prefix
);
}

return existingTranslationKeys;
}

Expand Down
3 changes: 3 additions & 0 deletions test.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ describe('Test Fixtures', () => {
'custom-t-helpers': {
helpers: ['t-error'],
},
'nested-keys': {
wrapTranslationsWithNamespace: true,
},
};

beforeEach(() => {
Expand Down