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

every translation in jsx incorrectly flagged as unused #34

Open
kitsunekyo opened this issue Feb 9, 2023 · 7 comments
Open

every translation in jsx incorrectly flagged as unused #34

kitsunekyo opened this issue Feb 9, 2023 · 7 comments

Comments

@kitsunekyo
Copy link

kitsunekyo commented Feb 9, 2023

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
Copy link

DonatienD commented Feb 22, 2023

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
Copy link

TheNemus commented Mar 1, 2023

Same to me but with .tsx

@halcyonshift
Copy link

halcyonshift commented Mar 26, 2023

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

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

@beamery-tomht
Copy link

beamery-tomht commented Jun 8, 2023

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...

  • It assumes only t('key' or i18next='key' are the only ways keys are consumed (as we only use this in our codebase, it optionally looks for either a closing bracket or comma after the key string)
  • We only use single quotes so didn't add support for double quotes or backticks
  • it doesn't support nested keys, we use a flat key structure
  • when keys are substrings, it marks them as false positive
  • can't handle dynamic keys still

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
Copy link

Having the exactly same issue in a React+TS project

@fredrivett
Copy link

fredrivett commented Apr 16, 2024

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
Copy link

mhyassin commented May 2, 2024

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 })

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

8 participants