i18next / i18next/i18next-cli

extract: keys wrapped in parentheses are silently dropped

Open Beginner friendly
#295 1 comment 1 reaction 0 assignees View on GitHub
Dominant language
TypeScript
Stars
238
Forks
37
Avg merge
1h 9m
Merged PRs (30d)
1

Description

## 🐛 Bug Report

`extract` drops any key that is wrapped in parentheses. The key never reaches the output file and the run still ends with `✔ Extraction complete!`, so nothing signals the loss.

This is easy to hit without writing it on purpose: Prettier wraps the longer branch of a ternary in parentheses when the call does not fit on one line, so a plain `t(cond ? 'a' : 'b')` turns into the broken shape on the next format run.

## To Reproduce

`src/App.tsx`:

```tsx
import { useTranslation } from 'react-i18next'

export function App ({ isOpen }: { isOpen: boolean }) {
const { t } = useTranslation()

return (
<>
{t(('plainKey'))}
{t(isOpen ? 'openKey' : ('closedKey'))}

)
}
```

`i18next.config.ts`:

```ts
import { defineConfig } from 'i18next-cli'

export default defineConfig({
locales: ['en'],
extract: {
input: ['src/**/*.tsx'],
output: 'locales/{{language}}/{{namespace}}.json',
},
})
```

```bash
npx i18next-cli extract
```

`locales/en/translation.json`:

```json
{
"openKey": "openKey"
}
```

## Expected behavior

All three keys extracted, the same as without the parentheses:

```json
{
"plainKey": "plainKey",
"openKey": "openKey",
"closedKey": "closedKey"
}
```

## Possible cause and fix

`ExpressionResolver.resolvePossibleStringValuesFromExpression` in `src/extractor/parsers/expression-resolver.ts` handles `StringLiteral`, `ConditionalExpression`, `TemplateLiteral` and `MemberExpression`, but never unwraps swc's `ParenthesisExpression`. Adding that one branch at the top of the method fixes every variant I tried (both ternary branches, `t(('key'))`, a parenthesized `defaultValue` or `context`, `i18nKey={('key')}` in `Trans`, plurals):

```ts
if (expression.type === 'ParenthesisExpression') {
return this.resolvePossibleStringValuesFromExpression(expression.expression, returnEmptyStrings)
}
```

The same node is already unwrapped in `linter.ts`, in `call-expression-handler.ts` (selector bodies, #247) and in `jsx-handler.ts`. With the change the full suite stays green (1320 tests) and eslint is clean. Happy to open a PR with this plus a test if that helps.

## Your Environment

- *runtime version*: node v24.18.0
- *i18next version*: 26.0.10
- *i18next-cli version*: 1.74.0, same on 1.56.11
- *os*: macOS

Contributor guide

Open the contributing guide

Research direction

Start in src/extractor/parsers/expression-resolver.ts at ExpressionResolver.resolvePossibleStringValuesFromExpression, then compare the ParenthesisExpression handling in linter.ts, call-expression-handler.ts, and jsx-handler.ts. Reproduce with src/App.tsx and i18next.config.ts using npx i18next-cli extract, then run the full suite and eslint. Done means parenthesized plain, conditional, and related translation keys are preserved in locales/en/translation.json.

Written by the indexing model from the issue text.

Assessment

Tech stack
react, typescript
Domain
cli, internationalization
Issue type
Bug
Difficulty
2/5
Estimated time
1-3 hours
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
90/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.