test: unify i18next mocks into centralized helpers#7
Conversation
Consolidate scattered i18next mock implementations across test files into a single source of truth. This reduces duplication and ensures consistent mock behavior. - Create test/i18n-mock.ts with reusable factory functions - Update vitest.setup.ts to use the centralized helpers - Remove redundant mock definitions from 8 test files - Update testing.md documentation 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
…eateReactI18nextMock` and detail global mock provisions.
| The global mock provides: | ||
|
|
||
| - `useTranslation` - returns translation keys with namespace prefix | ||
| - `Trans` component - renders i18nKey and components | ||
| - `useMixedTranslation` (from `@/app/components/plugins/marketplace/hooks`) | ||
| - `useGetLanguage` (from `@/context/i18n`) - returns `'en-US'` | ||
|
|
There was a problem hiding this comment.
🔴 Documentation in mocking.md, testing.md, and component-test.template.tsx claims the global mock provides useMixedTranslation (from @/app/components/plugins/marketplace/hooks) and useGetLanguage (from @/context/i18n), but vitest.setup.ts only mocks react-i18next — no global mock exists for these modules. This misleading documentation led to local mocks being removed from card/index.spec.tsx and plugin-mutation-model/index.spec.tsx without replacement, and will continue to mislead developers into omitting necessary mocks. Fix: either add actual global mocks for these modules, or remove the false claims from the documentation.
Extended reasoning...
What the bug is
Three documentation files updated in this PR claim that the global mock (defined in web/vitest.setup.ts) provides useMixedTranslation and useGetLanguage:
mocking.md(lines 56-62): "useMixedTranslation (from @/app/components/plugins/marketplace/hooks)" and "useGetLanguage (from @/context/i18n) - returns en-US"testing.md(line ~333): Same claimscomponent-test.template.tsx(line ~31): "The global mock provides: useTranslation, Trans, useMixedTranslation, useGetLanguage"
However, vitest.setup.ts only contains vi.mock("react-i18next", ...). There is no vi.mock call for @/app/components/plugins/marketplace/hooks or @/context/i18n anywhere in the global setup.
Step-by-step proof
vitest.setup.tshas exactly twovi.mockcalls: one fornext/imageand one forreact-i18next. Neither mocks@/app/components/plugins/marketplace/hooksor@/context/i18n.web/test/i18n-mock.ts(the new helper) only exportscreateReactI18nextMock,createUseTranslationMock,createTransMock, andcreateTFunction. It contains no reference touseMixedTranslationoruseGetLanguage.card/index.tsxdirectly importsuseMixedTranslationfrom@/app/components/plugins/marketplace/hooks(line 6) anduseGetLanguagefrom@/context/i18n(line 7). Without mocks, these real modules are loaded.marketplace/hooks.tsimports@tanstack/react-query,ahooks, and@/service/baseat module level — heavy dependencies that may cause import failures or side effects in the test environment.- 27 other test files in the project still explicitly mock
@/context/i18nlocally, confirming it is NOT globally mocked.
Why the refutation is partially correct but misses the point
The refutation argues that useGetLanguage works via jotai defaults (returning en-US) and useMixedTranslation delegates to the globally-mocked useTranslation().t when localeFromOuter is undefined. This is true for the happy path — the tests may pass in many scenarios. However:
- The documentation literally says "The global mock provides" these hooks. This is factually false — there is no global mock for these modules. Saying something "works transitively" is fundamentally different from saying it is "provided by the global mock."
- Loading the real
marketplace/hooks.tsmodule pulls in@tanstack/react-query,ahooks, and@/service/baseas transitive imports, which can cause import errors or unexpected side effects in test environments. - When
localeFromOuterIS provided (e.g., in locale-specific tests),useMixedTranslationcallsi18n.getFixedT()on the real i18next instance, which is NOT covered by the global mock.
Impact
The immediate impact is that card/index.spec.tsx and plugin-mutation-model/index.spec.tsx had their local mocks for these modules removed based on the false documentation claim. The ongoing impact is that developers reading the documentation will believe these hooks are globally mocked and omit necessary local mocks, leading to fragile tests or outright failures.
How to fix
Either:
- Add actual global mocks in
vitest.setup.tsfor@/app/components/plugins/marketplace/hooksand@/context/i18n, OR - Remove the false claims from
mocking.md,testing.md, andcomponent-test.template.tsx, and restore the local mocks incard/index.spec.tsxandplugin-mutation-model/index.spec.tsx.
| export function createTFunction(translations: TranslationMap, defaultNs?: string) { | ||
| return (key: string, options?: Record<string, unknown>) => { | ||
| const ns = (options?.ns as string | undefined) ?? defaultNs | ||
| const fullKey = ns ? `${ns}.${key}` : key | ||
|
|
||
| // Check custom translations with namespace first | ||
| if (translations[fullKey] !== undefined) | ||
| return translations[fullKey] | ||
|
|
||
| // Check custom translations without namespace | ||
| if (translations[key] !== undefined) | ||
| return translations[key] | ||
|
|
||
| // Serialize params (excluding ns) for test assertions | ||
| const params = { ...options } | ||
| delete params.ns | ||
| const suffix = Object.keys(params).length > 0 ? `:${JSON.stringify(params)}` : '' | ||
| return `${fullKey}${suffix}` | ||
| } |
There was a problem hiding this comment.
🟡 The new createTFunction in web/test/i18n-mock.ts does not handle the returnObjects option that the old global mock in vitest.setup.ts supported. When t(key, { returnObjects: true }) is called, the new mock returns a string like key:{"returnObjects":true} instead of an array ["key-feature-1", "key-feature-2"]. No existing tests currently break because all 5 files using returnObjects define their own local mocks, but this is a capability regression in the centralized mock that could cause failures if those local mocks are later removed to follow the pattern this PR establishes.
Extended reasoning...
What the bug is
The old global mock in vitest.setup.ts explicitly handled the returnObjects option:
if (options?.returnObjects)
return [`${key}-feature-1`, `${key}-feature-2`]The new createTFunction in web/test/i18n-mock.ts (lines 10-28) does not check for returnObjects at all. Instead, it falls through to the generic parameter serialization logic, which produces a string like key:{"returnObjects":true} rather than an array.
Step-by-step proof
Consider a component that calls t("billing.features", { returnObjects: true }) and expects a string[] back (e.g., web/app/components/billing/pricing/plans/self-hosted-plan-item/list/index.tsx casts the result as string[]).
- With the old mock:
options.returnObjectsis truthy, so it returns["billing.features-feature-1", "billing.features-feature-2"]- an array, matching what production code expects. - With the new
createTFunction: no custom translation matches,returnObjectsis not in the translations map, so it hits the fallback path.paramsbecomes{"returnObjects":true}(after deletingns), producing the string"billing.features:{\"returnObjects\":true}". This is a string, not an array.
Why existing tests don't break
All 5 test files currently using returnObjects define their own local vi.mock("react-i18next", ...) that overrides the global mock. Since local vi.mock() calls override the global mock from vitest.setup.ts, none of these tests are affected by this change right now.
Impact
The impact is limited but real as a contract regression. The PR establishes a pattern of removing local i18n mocks in favor of the centralized helper. If developers follow this pattern and remove the local mocks from the 5 affected files, those tests will break because the global mock no longer returns arrays for returnObjects: true. The TranslationMap interface already supports string | string[] values, so adding this handling would be straightforward.
How to fix
Add a returnObjects check to createTFunction before the parameter serialization fallback:
if (options?.returnObjects)
return [`${key}-feature-1`, `${key}-feature-2`]This matches the behavior of the old global mock.
|
|
||
| /** | ||
| * Create a t function with optional custom translations | ||
| * Checks translations[key] first, then translations[ns.key], then returns ns.key as fallback |
There was a problem hiding this comment.
🟡 JSDoc comment on line 8 describes the wrong lookup order. It says "Checks translations[key] first, then translations[ns.key]" but the code does the opposite: line 16 checks translations[fullKey] (ns.key) first, then line 20 checks translations[key]. The JSDoc should be updated to say "Checks translations[ns.key] first, then translations[key]".
Extended reasoning...
Bug Analysis
The createTFunction in web/test/i18n-mock.ts has a JSDoc comment on line 8 that describes the translation lookup order incorrectly.
What the JSDoc says (line 8):
Checks translations[key] first, then translations[ns.key], then returns ns.key as fallback
What the code actually does:
- Line 16: Checks
translations[fullKey]wherefullKey = ns.key— this istranslations[ns.key]FIRST - Line 20: Checks
translations[key]— this istranslations[key]SECOND - Line 27: Returns
fullKey(ns.key) as fallback
The inline comments in the code even confirm the correct order: line 15 says "Check custom translations with namespace first" and line 19 says "Check custom translations without namespace".
Step-by-step proof with a concrete example:
Suppose translations = { "common.save": "Save", "save": "Save Button" } and the function is called with key="save", options={ns: "common"}:
fullKeyis computed as"common.save"- Line 16: checks
translations["common.save"]→ found, returns"Save" - Line 20 is never reached
The JSDoc says translations[key] (i.e., translations["save"]) would be checked first, but the code checks translations[fullKey] (i.e., translations["common.save"]) first.
Impact:
This is a documentation-only issue in a test helper file. It has no runtime impact on application behavior. However, it could mislead developers who read the JSDoc to understand the lookup order when writing custom translation overrides in tests.
Fix:
Update line 8 to:
Checks translations[ns.key] first, then translations[key], then returns ns.key as fallback
Benchmark PR from agentic-review-benchmarks#7