mxmvshnvsk / i18n-unused

The static analyze tool for finding, marking and removing unused and missing i18n translations in your JavaScript project
MIT License
127 stars 21 forks source link

every translation in jsx incorrectly flagged as unused #34

Open kitsunekyo opened 1 year ago

kitsunekyo commented 1 year ago

i18next-react project setup:

// ./i18n-unused.config.cjs
module.exports = {
  localesPath: 'src/analysis/_locales',
  srcPath: 'src',
};

translation files

./src/analysis/_locales/de.json
./src/analysis/_locales/en.json

(reduced for brevity)

{
  "tab": {
    "progress": "Progress",
  }
}

example file that uses translations

// ./src/analysis/views/TestView.tsx
import { useTranslation } from 'react-i18next';

export const TestView = () => {
  const { t } = useTranslation('analysis');
  return <div>{t('tab.progress')}</div>;
};

i have installed i18n-unused as dev dependency and run yarn i18n-unused display-unused this prints almost all translations as unused. what i noticed is that it works with translations that are in javascript (like useEffect, or other pure javascript contexts), but it does not seem to work in jsx at all.

DonatienD commented 1 year ago

Experiencing the same issue in my project. This is also true when you implement the collectUnusedTranslations script (see below as implemented on my side).

const { collectUnusedTranslations } = require('i18n-unused');
const { sync: globSync } = require('glob');

const dictionaries = globSync('./src/i18n/dictionaries/*.json', {realpath: true});
const sources = globSync('./src/**/*.+(tsx|ts|jsx|js|es6)', {realpath: true});

const handleTranslations = async () => {
  const unusedTranslations = await collectUnusedTranslations(
    dictionaries,
    sources,
    {}
  );
  return unusedTranslations;
}

handleTranslations().then(result => { console.log('unused:', result) });
TheNemus commented 1 year ago

Same to me but with .tsx

halcyonshift commented 1 year ago

I had this issue and mostly fixed it with the translationKeyMatcher config:

translationKeyMatcher: /['"](translation|prefixes|like|tab|here)\.([\w\.]+)['"]/gi

beamery-tomht commented 1 year ago

Seems like the default value for translationKeyMatcher, which is /(?:[$ .](_|t|tc|i18nKey))\(.*?\)/gi, isn't working - it gives no results

It's looking for $, space or . characters preceding the t function which isn't always present.

I tried the one provided in the comment above - but, it struggles with several cases:

When there isn't a closing bracket on the same line, they won't get recognised

t('myKey', {
  someToken: 123
})

If the key used is dynamic, they aren't recognised

t(somePredicate ? 'keyTrue' : 'keyFalse')

If keys are substrings of each other, they won't get flagged

For example, given:

{
  "keyA": "x",
  "keyAB": "x",
}

And code of:

t('keyAB')

The tool won't flag that keyA is unused because it's a substring of keyAB.

If other functions match the same structure, they are also captured

e.g.

matchViewport('someString') // matcher sees `t('someString')` here and marks it as a used key

I've added a custom translationKeyMatcher that is sort of working....

translationKeyMatcher: /(?:[ ={:]t\(|i18nKey=)'\w+'[,)]?/gi,

Some gotchas to note as this is very basic...

I've also had to refactor some of my keys to support these gotchas:

-  t(predicate ? 'key1' : 'key2')
+  predicate ? t('key1') : t('key2') 

Making sure my keys aren't substrings

- "myControl": "value",
+ "myControlLabel": "value",
  "myControlTooltip": "tooltip",
manuelpoelzl commented 6 months ago

Having the exactly same issue in a React+TS project

fredrivett commented 5 months ago

after a good chat with my friend chatGPT, we (they) came up with this:

translationKeyMatcher:
  // this regex was generated by chatGPT, and has the following features:
  // * only match for `t()` and `tToDocLanguage()` calls
  // * preceded by a space, opening parenthesis, or opening curly brace
  // * also match for `i18nKey` prop in JSX
  // * captures double quotes, single quotes, and backticks
  // * works with optional chaining e.g. `t?.()`
  // * works with multiline strings e.g. `t(\n"key")`
  /(?<=[\s{(])(?:t|tToDocLanguage)\??\.?\(\s*["'`]?([\s\S]+?)["'`]?\s*\)|i18nKey="([\s\S]+?)"/g,

we have a custom function used in the codebase, tToDocLanguage which we wanted to match on too, but if you don't you can remove the |tToDocLanguage part.

works really well for us, only downside is it doesn't match on dynamic strings (e.g. t(`foo.${bar}`)), so we just have a list of those in our excludeKey array.

here's some test code to verify it works as expected for your use case:

const regex = /(?<=[\s{(])(?:t|tToDocLanguage)\??\.?\(\s*["'`]?([\s\S]+?)["'`]?\s*\)|i18nKey="([\s\S]+?)"/g;
const text = `
  t("1.basic.doublequotes.string")
  t?.("2.basic.doublequotes.string.with.optional.chaining")
  t('3.basic.singlequotes.string')
  t?.('4.basic.singlequotes.string.with.optional.chaining')
  t(\`5.basic.backtick.string\`)
  t?.(\`6.basic.backtick.string.with.optional.chaining\`)
  const multilineString = \`abc \${t(
    "7.multiline.string",
  )}\`;
  tToDocLanguage("8.custom.func.beginning.with.t")
  <Element i18nKey="9.i18nKey.string">
`;

const matches = [...text.matchAll(regex)];
const keys = matches.map(match => match[1] || match[2]);

// output each key on a new line
keys.forEach(key => console.log(key));

// outputs:
//   '1.basic.doublequotes.string'
//   '2.basic.doublequotes.string.with.optional.chaining'
//   '3.basic.singlequotes.string'
//   '4.basic.singlequotes.string.with.optional.chaining'
//   '5.basic.backtick.string'
//   '6.basic.backtick.string.with.optional.chaining'
//   '7.multiline.string",'
//   '8.custom.func.beginning.with.t'
//   '9.i18nKey.string'

hope that helps some folks!

mhyassin commented 4 months ago

I've adjusted the regex a bit to also include t methods where you use variables,

/t\(\s*["'`]?([\s\S]+?)["'`]?\s*(?:\)|,)|i18nKey="([\s\S]+?)"/gi

Now this would be matched correctly as well

t(`translation.key`, { variable: x })