Update dependency i18next to v26 #554

Open
Renovate wants to merge 1 commit from renovate/i18next-26.x into main
Collaborator

This PR contains the following updates:

Package Type Update Change
i18next (source) dependencies major ^25.3.2 → ^26.0.0

Release Notes

i18next/i18next (i18next)

v26.5.11

Compare Source

  • fix: a nested false, 0 or '' is now returned when a flat key with the same dotted name also exists. With { a: { b: false }, 'a.b': 'flat' }, getResource('en', 'translation', 'a.b') and t('a.b') returned 'flat': the nested result was checked for truthiness, so these present values fell through to the flat-key lookup. Without such a flat key they were already returned. A nested null, which is not a valid lookup by default (returnNull: false), still falls back to the flat key. Thanks @​lprnmns (#​2461).

v26.5.10

Compare Source

  • fix: when none of the detected languages is in supportedLngs, the best match no longer picks a different language whose code only starts with the same letters. The last fuzzy check took any supported code starting with the language part, so with supportedLngs: ['en', 'fil-PH'] a Finnish user (fi-FI) got Filipino instead of the fallback language, and ha (Hausa) picked haw-US (Hawaiian). Other regions of the same language still match as before (de-CH → de-DE, pt-PT → pt-BR, fil → fil-PH). Compatibility note: a two-letter code no longer matches a supported three-letter code that extends it, like ar → arz-EG, sw → swh-KE or no → nob-NO; list the two-letter code in supportedLngs (or map it with fallbackLng) if you relied on that. Thanks @​Pharex38 (#​2460).

v26.5.9

Compare Source

  • fix: nested lookups no longer resolve to methods inherited from the prototype, like constructor, toString or map. Only the top level was guarded, so {{constructor}} was treated as missing, but {{user.constructor}} rendered the function source (with a class instance, the full source code of the class), and {{user.toString}}, {{items.map}} or a class method like {{user.greet}} did the same. The same lookup is used for translation keys when ignoreJSONStructure is on (the default): a nested key named like an inherited method that is missing in the current language (t('roles.constructor') with roles.constructor only in the fallback language) returned function Object() { [native code] } instead of falling back, and exists('roles.valueOf') returned true. These keys are now missing, so the fallback language, missingInterpolationHandler, saveMissing and the missing key handler apply as for any other missing key. Getters defined on a class ({{user.fullName}} with get fullName()) still work. Nested lookups also no longer walk into the prototype via __proto__, so {{user.__proto__.constructor}} no longer prints the class source either. Thanks @​Pharex38 (#​2459).

v26.5.8

Compare Source

  • fix: arrays returned with joinArrays now get the same resolved details as strings. The joined value was extended with lastKey in the place of the resolved lookup, so with postProcessPassResolved a post processor got no usedLng, usedNS or exactUsedKey (t('steps', { joinArrays: ' / ' }) reported undefined/undefined:undefined). This affected the subliminal post processor used by the locize InContext editor, which could not map a joined array to its key. The nesting recursion guard did not get the parent key either, so arrays that nest each other (listA: ['a', '$t(listB)'], listB: ['b', '$t(listA)']) threw Maximum call stack size exceeded. They now stop at the guard, as strings do. An i18nFormat plugin with handleAsObject and parse also no longer throws Cannot read properties of undefined (reading 'usedNS') on joined arrays. Thanks @​nk-batman21 (#​2458).

v26.5.7

Compare Source

  • fix: with the default cacheInBuiltFormats: true, built-in formats now also work when the t() options contain a BigInt or a circular object. The cache key was built with JSON.stringify over all options, including the interpolation values, which throws on those, so every built-in format in that call returned the raw value: t('num', { val: 12345678901234567890n }) with 'Total: {{val, number}}' returned 'Total: 12345678901234567890' instead of 'Total: 12,345,678,901,234,567,890', and passing an object with a back-reference (like an ORM entity) left all numbers, dates and lists of that call unformatted. When the key cannot be built, the value is now formatted without the cache, as with cacheInBuiltFormats: false. This also applies to formatters added with addCached. Other calls still use the cache as before. Thanks @​nk-batman21 (#​2457).

v26.5.6

Compare Source

  • fix: cloneInstance({ forkResourceStore: true }) now copies the resources at every level. It copied them only down to the namespace objects, so nested keys and arrays stayed shared with the original instance: addResource() with a nested key (fork.addResource('en', 'translation', 'greeting.hello', 'Hi')) or a deep addResourceBundle() on the fork also changed the original instance and every other fork. Flat keys were already isolated. Forking a large store takes a bit longer: with 300k keys in nested groups, about 11 ms instead of 3 ms per fork. Clones without forkResourceStore still share the store as before. Thanks @​nk-batman21 (#​2456, follow-up to #​2270).

v26.5.5

Compare Source

  • fix: with the default skipOnVariables: true, a {{placeholder}} inside a value inserted with an unescaped {{- var}} now stays literal, as it already did for {{var}}. The escape pass ran after the unescaped values were written and scanned them again, so t('{{- a}}', { a: '{{b}}', b: 'c' }) returned 'c' instead of '{{b}}'. With pre-built HTML that carries user text, like t('You may contact {{name}} at {{- emailHTML}}.', { name, emailHTML }), that text could pull another variable of the same t() call into the output. Results with skipOnVariables: false, and values without placeholders, are unchanged. Compatibility note: a {{- var}} value whose placeholders were meant to be filled from the same t() call now keeps them literally; skipOnVariables: false restores the old behavior. Thanks @​nk-batman21 (#​2455, reported in #​1595).

v26.5.4

Compare Source

  • fix: with the default skipOnVariables: true, a {{placeholder}} inside an interpolated value now stays literal also when the same placeholder appears later in the translation. Each value was written with String.replace, which replaces the first copy of the placeholder text, and that could be the copy inside an earlier value: t('{{name}} has {{count}} items', { name: '{{count}}', count: 5 }) returned '5 has {{count}} items' instead of '{{count}} has 5 items'. The value is now written at the position of the matched placeholder. Results with skipOnVariables: false are unchanged. Thanks @​nk-batman21 (#​2454, reported in #​1595).
  • fix: with useRawValueToEscape: true, a non-string value in an unescaped {{- var}} placeholder or with escapeValue: false no longer throws val.replace is not a function; it is converted to a string like any other value.

v26.5.3

Compare Source

  • fix(types): with the selector API, a plural key now requires count also when the call passes an explicit ns. t(($) => $.item) already failed without count, but t(($) => $.item, { ns: 'items' }) compiled, although at runtime it looks up item instead of item_one / item_other and returns the key. It now fails the same way, also with ordinal: true, defaultValue, an ns array, a key prefix and i18n.t. Calls with count, context, returnObjects: true and arrays of selectors, as well as return types, are unchanged. Thanks @​nyarthan (#​2452).

v26.5.2

Compare Source

  • fix(types): on TypeScript 7, a t() call with a missing, misspelled or wrongly typed option now reports that option again (Property 'album' is missing, Type 'string' is not assignable to type 'number'), as TypeScript 5 and 6 do. TypeScript 7 elaborated the error against the default-value signature instead and ended in '{ count: 3; albm: "Summer"; }' is not assignable to type 'string': on a failed call TypeScript explains the last of the equally matching signatures, and TypeScript 7 sorts the 3-element [key, defaultValue, options?] last. It is now written as two signatures, [key, defaultValue] and [key, defaultValue, options]. This affects the default key checks with i18next {{name}} messages as well as parseInterpolation: 'icu' and 'mf2'. Which calls compile, their return types and the error positions are unchanged on TypeScript 5.9, 6 and 7.

v26.5.1

Compare Source

  • fix: a nested false, 0 or '' is now returned when a flat key with the same dotted name also exists. With { a: { b: false }, 'a.b': 'flat' }, getResource('en', 'translation', 'a.b') and t('a.b') returned 'flat': the nested result was checked for truthiness, so these present values fell through to the flat-key lookup. Without such a flat key they were already returned. A nested null, which is not a valid lookup by default (returnNull: false), still falls back to the flat key. Thanks @​lprnmns (#​2461).

v26.5.0

Compare Source

  • feat(types): parseInterpolation: 'icu' and parseInterpolation: 'mf2' type t() options from the message's ICU MessageFormat arguments (i18next-icu) or MessageFormat 2 variables (i18next-mf2), so ICU and MF2 users no longer need parseInterpolation: false and lose argument checking. Missing, misspelled and wrongly typed arguments fail, and so does leaving out the options for a message with arguments (t('greeting')); that error names the missing arguments. Opt-in, default true is unchanged.
    • ICU: {name} takes string | number, {n, number}, plural and selectordinal take number, {d, date} and {t, time} take Date, select takes its case keys or any string (for other), and arguments nested in cases are required too. # and tags are not arguments, apostrophe quoting is read like intl-messageformat does.
    • MF2: {$name} takes string | number, a variable annotated with :number, :integer, :currency, :percent or :unit (inline or through .input / .local) takes number, :datetime, :date and :time take Date, :string takes string, option and markup variables (minimumFractionDigits=$digits, {#link href=$url}) are required too, .local variables are not arguments, and |quoted literals| and \{ escapes are skipped.
    • interpolationFormatTypeMap overrides a type by ICU format or MF2 function name, e.g. date: Date | number or a custom uppercase: string.
    • Applies to t(), getFixedT(), i18n.t, key prefixes, namespaces, the selector API and react-i18next's Trans values. Messages need literal types, as the resources file from i18next-cli's types command has them.
    • Type-check cost with 1,000 messages and one call each: 1.35s instead of 0.86s with false (TypeScript 6), 0.5s instead of 0.26s with TypeScript 7. Messages that no call uses cost nothing.
  • fix(types): with the default (non-strict) key checks, an options object with a missing or misspelled interpolation variable now reports the missing variable (Property 'name' is missing) instead of '{ nmae: string }' is not assignable to type 'string' from the default-value overload.

v26.4.7

Compare Source

  • perf: cloneInstance() no longer inherits preload. loadResources() adds every preload language to each load, and clones copied preload from the parent, so a clone's init() and every changeLanguage() walked all preloaded languages × all namespaces through queueLoad() / hasResourceBundle(), although the parent had already loaded them into the shared store. With i18next-http-middleware (one clone + changeLanguage() per request), 25 preloaded languages and 20 namespaces, that was most of the per-request CPU: the reproduction from the issue went from ~90 µs to ~10 µs per request, the same as with a single language. A clone now only loads what it uses itself: its changeLanguage() no longer waits for parent preload languages that are still loading (the parent's own init() still does), and clone.loadNamespaces() loads the new namespace for the clone's language and fallbacks instead of every preloaded language. clone.loadLanguages() works as before, the parent instance is unchanged, and passing preload to cloneInstance() keeps the old behavior. Thanks @​theMostCuriousHomunculus for the detailed report (#​2451).

v26.4.6

Compare Source

  • fix: a fallback language whose formatted code is already in the hierarchy is no longer added a second time. toResolveHierarchy() checked the raw fallbackLng value against the list, but the list holds formatted codes, so any fallback written differently from its formatted form slipped through: lng: 'de-CH' with fallbackLng: 'de-ch' gave languages ['de-CH', 'de', 'de-CH'], and the same happened with fallbackLng: 'EN' + cleanCode, lowerCaseLng, and Intl aliases (iw-IL → he-IL). Missing keys were looked up in the same language twice. Only exact duplicates are removed; the order is unchanged. Thanks @​lprnmns (#​2450).

v26.4.5

Compare Source

  • fix: hasLoadedNamespace(ns, { lng }) no longer stays false forever when the passed in lng is loaded under another code, or not at all. It checked the load state of the raw lng, but loadLanguages() stores a language under its resolved code (de-ch → de-CH, de-CH → de with load: 'languageOnly') and skips a lng outside supportedLngs entirely (only the fallback gets loaded), so that state never got set. react-i18next's useTranslation(ns, { lng }) then never became ready: with useSuspense it kept throwing an already resolved promise and React retried forever (100% CPU, SSR never finished), without it the load effect re-rendered in a loop. It now checks the code the language is actually loaded under, and for a lng outside supportedLngs only waits for the fallback, which is also what t() resolves to. Calls without lng and the precheck (react-i18next's bindI18n: 'languageChanging') are unchanged (react-i18next#1934).

v26.4.4

Compare Source

  • fix(security): the nesting regexp no longer backtracks exponentially (ReDoS) on an unclosed $t(. Its repeated group had overlapping alternatives ([^()"']+ inside (?:...)*?, and inside parentheses [^()] also matched quotes), so a string containing $t( without a closing ) made the engine try every way to split the text: about 2x the runtime per extra character, and a ~25 character value hung t() for minutes. With the default config this was reachable through any interpolated value (t('greet', { name: '$t(aaaaaaaaaaaaaaaaaaaaaaaa' })): escaping does not touch $, t or (, and the skipOnVariables check runs the regexp on the interpolated result, so nest: false did not help either. Every alternative now starts with a different character (plain char, ", ' or (), so there is only one way to match and the cost is linear. Quoted strings and one level of parentheses in nesting options keep working (#​2336). The only difference: a lone " or ' inside unquoted parentheses within $t(...) is no longer matched. Affects 25.3.3 to 26.4.3. See advisory GHSA-5jr9-983r-c8x4, CVSS 7.5 (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H). Thanks to Anisetti Chaitanya Eshwar Prasad for the responsible disclosure.
  • fix(security): nest() now resolves $t(...) in a single pass and no longer nests text that came out of a resolved value. Every nested lookup reset the shared nesting regexp, so the loop started over from the beginning of the string after each match. That had two effects. First, a $t(...) inside a variable that the nested lookup deliberately kept literal (default skipOnVariables: true) was resolved anyway by the outer loop: with outer: 'A $t(inner) B' and inner: 'Hi {{name}}', t('outer', { name: '$t(secret)' }) rendered the secret translation. It now stays A Hi $t(secret) B, the same as without the outer nesting; with skipOnVariables: false it is still resolved. Second, the cost was quadratic when an unclosed $t( preceded many nestings, which matters when an app passes untrusted input as the key itself (t(userInput)): 20,000 nestings took ~1.4 s, now ~25 ms. The resolved value is now sliced into the result instead of going through String.replace, so $&, $`, $' and $$ stay literal as before (26.4.2).

v26.4.3

Compare Source

  • perf: ResourceStore.hasLanguageSomeTranslations() no longer enumerates every key of a large bundle on each call. It ran Object.keys() on the whole bundle just to learn whether it had at least one key, and init() / changeLanguage() reach it three times per instance (once while picking the language, twice via setResolvedLanguage()). With large flat bundles and one i18next instance per SSR request reusing the same resources, that showed up as measurable CPU work before any t() call. Bundle objects already found non-empty are now remembered in a WeakSet shared across instances, so later checks are O(1): 2,000 inits with a 6,800-key bundle went from ~750 ms to ~20 ms, the same as a one-key bundle. Only the non-empty result is cached. No API removes single keys from a bundle, so a non-empty bundle stays non-empty, and empty bundles are re-checked every time, so keys added later (backend loads, addResource, addResourceBundle, direct or silent changes) count right away. The only edge case: a bundle emptied by hand with delete on every key while it is still in the store keeps counting as having translations; use removeResourceBundle() for that. Thanks @​dmchoi77 for the detailed report (#​2449).

v26.4.2

Compare Source

  • fix: $&, $`, $' and $$ inside a nested value ($t(key)) now stay literal. nest() handed the resolved value straight to String.replace as the replacement argument, so those sequences were read as replacement patterns: $& re-inserted the $t(...) match, $` / $' inserted the text before / after it, and $$ collapsed to $. Through t() the $& case was worse than a wrong string: the nested lookup resets the shared nesting regexp, so the re-inserted $t(...) was matched again on every pass and t() never returned — also under the default escapeValue: true when the value arrives via a variable forwarded through nesting options ($t(key, { "name": "{{name}}" }) with a name containing $&). The value is now $-escaped at the String.replace call, the same guard interpolate() already has, and a non-string value returned by a formatter in the nesting chain ($t(key, myFormat)) is stringified before that. Nested values are still not HTML-escaped (#​854). Thanks @​mahirhir (#​2447).

v26.4.1

Compare Source

  • fix(types): the selector-form keyPrefix overload of getFixedT() is now available under enableSelector: 'strict'. Its constraint was gated on true | 'optimize' only, so under 'strict' it collapsed to never, the overload dropped out, and the returned t silently lost its keyPrefix scope (t(($) => $.deep) failed with Property 'deep' does not exist on type '{}'). The same call already typechecked under true and 'optimize'. Thanks @​hovelopin (#​2446).

v26.4.0

Compare Source

  • perf: cache toResolveHierarchy results per (code, fallbackCode) pair. The hierarchy resolver runs on every t() call and calls Intl.getCanonicalLocales multiple times, which showed up prominently when profiling render-heavy UIs (e.g. virtualized data grids); with the cache the per-call cost drops from ~886 ns to ~41 ns. The cache is invalidated automatically when options.fallbackLng changes (reassignment or in-place array mutation); if you mutate other resolution-relevant options at runtime (load, lowerCaseLng, cleanCode, nonExplicitSupportedLngs), call i18next.services.languageUtils.clearCache() afterwards. Function-valued fallbackLng and per-call array/object fallbackLng options are never cached, so dynamic fallbacks keep working as before. Thanks @​equaterina (#​2444).
  • chore: update all devDependencies (Babel stays on 7.x until @rollup/plugin-babel supports 8, eslint on 9.x for neostandard). Removed the unused coveralls package (CI uses the Coveralls GitHub Action) and replaced sinon with nise + vitest.spyOn in the v1 compatibility tests, which resolves all open npm audit findings (0 vulnerabilities) and should close the dependabot alerts on the lockfile.

v26.3.6

Compare Source

  • fix: allow TypeScript 7 in the optional typescript peer dependency range (^5 || ^6 || ^7). With typescript@7.0.2 in a project, npm install failed with an ERESOLVE peer conflict. The published types are TS7-compatible as-is: every test/typescript suite produces identical results under 6.0 and 7.0.2. Reported in react-i18next#1927, thanks @​andikapradanaarif.

v26.3.5

Compare Source

  • fix: $t() nesting options blocks that span multiple lines are now parsed. nest() decided where the nested key ends by testing match[1] with /{.*}/, whose dot does not cross line breaks — so a $t(key, { ... }) options object containing a newline was treated as having no options, mis-split as formatters, and the nested lookup ran without its options (placeholders stayed unresolved). The nesting regexp itself already matches newlines inside $t(...); adding the s (dotAll) flag makes multiline options behave like the single-line form. Thanks @​spokodev (#​2440).
  • fix: getUsedParamsDetails (the returnDetails: true path) no longer mutates the passed replace object. It wrote count straight onto options.replace so the returned usedParams would include it — a caller reusing one replace object across t() calls then carried a stale count into later interpolations (e.g. a previous call's count: 5 rendered instead of the current call's value). The details are now built from a copy; usedParams still includes count. Thanks @​spokodev (#​2441).
  • fix: with the default skipOnVariables: true + escapeValue: true, a {{placeholder}} carried inside an interpolated value now stays literal even when the value contains escapable characters. The skip logic advanced the regex lastIndex by the raw value length, but the escaped text written into the string is longer, so lastIndex landed inside the inserted value and a trailing {{placeholder}} in it got interpolated — leaking another in-scope variable that should have stayed literal (values without escapable characters were already skipped correctly). The advance now uses the escaped length that is actually written, and the regex-safe $-doubling is applied only at the String.replace call so it can't distort the length arithmetic. Thanks @​spokodev (#​2442).

v26.3.4

Compare Source

  • fix(security): deepExtend (used by addResourceBundle(..., deep, overwrite)) no longer recurses into inherited properties. It checked key existence with the in operator, which walks the prototype chain, so a source key matching an inherited built-in (e.g. hasOwnProperty, toString) caused recursion into the shared Object.prototype function and, with overwrite: true, could overwrite e.g. Object.prototype.hasOwnProperty.call with a non-callable value — corrupting a shared built-in process-wide (DoS). Existence is now checked with Object.prototype.hasOwnProperty.call, so such keys are copied as plain own data instead. This complements the existing __proto__/constructor guard and is also strictly more correct for an own-property merge. Only affects applications that pass attacker-controlled data with deep: true and overwrite: true; no standard backend/integration does this. Distinct from CVE-2026-48713 / CVE-2026-48714 (different packages, setPath mechanism). See advisory GHSA-6jcc-5g8w-32mx, CVSS 5.9 (CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:U/C:N/I:L/A:H). Thanks to zx (Jace) @​manus-use for the responsible disclosure.

v26.3.3

Compare Source

  • fix(types): selector t($ => $.arr, { returnObjects: true, context }) on a JSON array of heterogeneous objects now preserves each element's full shape (e.g. { transKey1: string; transKey2: string }[]) instead of collapsing to a union of partial element types. Two type-level causes: (1) FilterKeys evaluated the whole array element type at once, so keyof (A | B) only saw the keys common to every element — it now distributes over the object union and filters each element independently; (2) when TypeScript merges mismatched array element types it injects phantom optional undefined keys (e.g. transKey1_withContext?: undefined on elements that don't define it), which the context-detection helpers mistook for real context variants — they now skip keys typed as undefined. Also adds a dedicated context + returnObjects: true selector overload using const Fn + ReturnType<Fn>, so Target is no longer collapsed to unknown via ApplyTarget. Resolves Problem 1 of #​2398 (Problem 2 was already fixed on master). Thanks @​sauravgupta-dotcom (#​2438). Fixes #​2398.

v26.3.2

Compare Source

  • fix: chained formatters with a parenthesised option that contains the format separator (e.g. join(separator: ', ')) now work at any position in the chain, not just first. Previously the comma-in-parens reassembly only repaired formats[0], so {{v, uppercase, join(separator: ', ')}} split the join(...) option on the inner comma and never rejoined it, producing corrupt output. Replaced the first-position-only repair with a position-independent pass that re-joins fragments until each open paren closes. Thanks @​spokodev (#​2437).

v26.3.1

Compare Source

  • fix(types): t() with a keyPrefix no longer pollutes its return type with sibling keys' values. A regression in 26.3.0 — the [Res] extends [never] guards added to KeysBuilderWithReturnObjects / KeysBuilderWithoutReturnObjects turned the builders into deferred conditional types, so KeyPrefix<Ns> stopped resolving to a literal union and keyPrefix inference widened to the whole namespace. Symptom: useTranslation(ns, { keyPrefix: 'a.b' }) then t('title') would resolve to '<a.b>.title' | '<other.path>.title' | ... instead of just the scoped value. Affected every react-i18next user using keyPrefix. Restored to the eager 26.2.0 form. The same-namespace conflict handling from #​2434 still works via _DropConflictKeys at the merge layer (in options.d.ts). Thanks @​aaronrosenthal (#​2436).

v26.3.0

Compare Source

  • feat(types): introduce ResourceNamespaceMap — a separate mergeable augmentation surface for namespace resource types, designed for monorepos where multiple packages each want to contribute their own namespaces. Previously, every package had to coordinate on a single CustomTypeOptions.resources declaration (or fall back to typing dependency namespaces as any) because resources is a single property of an interface and TypeScript reports TS2717 when two declarations of the same property disagree. The new interface merges naturally across declare module 'i18next' blocks, so each package can ship its own i18next.d.ts independently. Per-property merge handles same-namespace contributions from multiple packages, and same-key/different-literal conflicts are silently dropped to avoid poisoning t() overload resolution. Fully backwards-compatible — existing CustomTypeOptions.resources augmentations continue to work, and both surfaces can coexist. Scalar options (defaultNS, returnNull, enableSelector, etc.) still belong on CustomTypeOptions. Thanks @​sh3xu (#​2434). Fixes #​2409.

v26.2.0

Compare Source

  • feat(types): new parseInterpolation TypeOption (default true). When set to false in CustomTypeOptions, the type-level extractor stops parsing translation strings for {{variable}} patterns. Required by i18next-icu users — the default extractor mistakes ICU MessageFormat nested-brace plurals like {count, plural, one {{count} row} other {{count} rows}} for an interpolation block and demands a phantom variable name. The flag is type-only; runtime interpolation is governed by InterpolationOptions and is unaffected. Fixes i18next-icu#85.
  • fix(types): expose enableSelector on InitOptions so i18next.init({ enableSelector: 'strict' }) typechecks without a module augmentation. The runtime already reads opts?.enableSelector from init options; this lands the matching type declaration next to the other selector-resolution knobs. Accepts false | true | 'optimize' | 'strict'. Thanks @​Faithfinder (#​2431)

v26.1.0

Compare Source

  • feat: enableSelector: 'strict' (TypeOptions + runtime option). Opt-in mode that drops the flattened-primary form from NsResource at the type level — every namespace (primary included) is exposed only under its own key on $, uniformly across single- and multi-ns hooks. At runtime, a leading selector path segment matching the scope's namespace list is always rewritten as a namespace prefix, including the primary. Eliminates the silent-miss surface area where t($ => $.primary.foo) typechecks but doesn't resolve under the default mode (see #​2429). Backward-compatible: default enableSelector: false | true | 'optimize' behavior is unchanged. Note: strict mode is incompatible with the #​2405 pattern (keys whose names match sibling namespaces) — those users should stay on default mode.

v26.0.10

Compare Source

  • feat: getFixedT accepts a fourth optional fixedOpts argument carrying scopeNs — the full namespace list the bound t was created for. The selector API uses scopeNs to detect when a path's first segment is a namespace prefix, without changing resolution scope. Resolution still uses the bound ns (a single primary string in the typical react-i18next setup), so plain t('key') lookups stay isolated to the primary namespace exactly as before — only t($ => $.secondaryNs.foo) selectors now route correctly under useTranslation([nsA, nsB]). Fixes the runtime side of #​2429 for the react-i18next default-nsMode case. The 4th argument is opt-in: existing 3-arg getFixedT(lng, ns, keyPrefix) callers see no behavior change.

v26.0.9

Compare Source

  • fix(types): unformatted interpolation values are now typed as string | number (was string). i18next stringifies values at runtime, so requiring callers to wrap numbers in String(...) for plain {{var}} placeholders was unnecessary friction — and could mask the real problem when a non-string value was passed alongside multiple interpolation slots (the t() overload resolution would fall through to the 3-arg form and report a confusing "not assignable to string" error against the options object). Typed format specifiers like {{x, number}}, {{x, currency}}, {{x, datetime}}, etc. keep their precise types; this only relaxes the no-format default. The count variable remains number-only

v26.0.8

Compare Source

  • fix(types): restore the pre-v25.10.4 ExistsFunction shape so plain arrow functions can again be assigned to ExistsFunction-typed variables (TypeScript cannot infer type predicates through multi-overload assignment). Direct i18next.exists(key) calls still narrow key to SelectorKey — the predicate is now declared inline on i18n.exists. Custom wrappers that want the narrowing can type themselves as typeof i18next.exists 2425

v26.0.7

Compare Source

  • fix: when a plural lookup misses, the missingKey debug log now shows the actual plural-resolved key (e.g. foo.bar_many for Polish count: 14) instead of the base key — making it obvious which plural category was expected and missing 2423
  • chore: drop @babel/runtime runtime dependency. The build no longer generates any @babel/runtime imports, so the package is unused by consumers. Rollup now uses babelHelpers: 'bundled' so any helpers that are ever needed in the future will be inlined rather than imported externally 2424
  • chore: stop emitting dist/esm/i18next.bundled.js. It was byte-identical to dist/esm/i18next.js because no helpers were being imported 2424

v26.0.6

Compare Source

Security release — all issues found via an internal audit.

  • security: warn when a translation string combines escapeValue: false with interpolated variables inside a $t(key, { ... "{{var}}" ... }) nesting-options block. In that narrow combination, attacker-controlled string values containing " can break out of the JSON options literal and inject additional nesting options (e.g. redirect lng/ns). The default escapeValue: true configuration is unaffected because HTML-escaping neutralises the quote before JSON.parse. See the security note in the Nesting docs for the full pattern and mitigations
  • security: apply regexEscape to unescapePrefix / unescapeSuffix on par with the other interpolation delimiters. Prevents ReDoS (catastrophic-backtracking) when a misconfigured delimiter contains regex metacharacters, and fixes silent breakage of the {{- var}} syntax when the delimiter contains characters like (, [, .
  • security: strip CR/LF/NUL and other C0/C1 control characters from string log arguments to prevent log forging via user-controlled translation keys, language codes, namespaces, or interpolation variable names (CWE-117)
  • chore: ignore .env* and *.pem/*.key files in .gitignore

v26.0.5

Compare Source

  • fix: cloneInstance().changeLanguage() no longer fails to update language state when the target language is not yet loaded — a race between init()'s deferred load() and the user's changeLanguage() could overwrite isLanguageChangingTo, causing setLngProps to be skipped 2422

v26.0.4

Compare Source

  • fix(types): inline formatting options like {{price, currency(EUR)}} are now correctly resolved to their base format type (e.g. number for currency) instead of falling back to string 2378

v26.0.3

Compare Source

  • fix(types): addResourceBundle now accepts an optional 6th options parameter ({ silent?: boolean; skipCopy?: boolean }) matching the runtime API 2419

v26.0.2

Compare Source

  • fix(types): t("key", {} as TOptions) no longer produces a type error — the context constraint now bypasses strict checking when context is unknown (e.g. from TOptions) 2418

v26.0.1

Compare Source

  • feat: getFixedT accepts a fourth optional fixedOpts argument carrying scopeNs — the full namespace list the bound t was created for. The selector API uses scopeNs to detect when a path's first segment is a namespace prefix, without changing resolution scope. Resolution still uses the bound ns (a single primary string in the typical react-i18next setup), so plain t('key') lookups stay isolated to the primary namespace exactly as before — only t($ => $.secondaryNs.foo) selectors now route correctly under useTranslation([nsA, nsB]). Fixes the runtime side of #​2429 for the react-i18next default-nsMode case. The 4th argument is opt-in: existing 3-arg getFixedT(lng, ns, keyPrefix) callers see no behavior change.

v26.0.0

Compare Source

This is a major breaking release:

Breaking Changes
  • Remove deprecated initImmediate option — the backward-compatibility mapping from initImmediate to initAsync (introduced in v24) has been removed. Use initAsync instead.
  • Remove legacy interpolation.format function — the old monolithic format function (interpolation: { format: (value, format, lng) => ... }) is no longer supported. The built-in Formatter (or a custom Formatter module via .use()) is now always used. Migrate to the new formatting approach using i18next.services.formatter.add() or .addCached() for custom formatters.
  • Remove console support notice — the console support notice introduced in v25.8.0 has been removed, along with the showSupportNotice option and all related internal suppression logic (globalThis.__i18next_supportNoticeShown, I18NEXT_NO_SUPPORT_NOTICE env var). See our blog post for the full story.
  • Remove simplifyPluralSuffix option — this option was unused by the core PluralResolver (which relies entirely on Intl.PluralRules). It only had an effect in the old v1/v2/v3 compatibility layer. The v4 test compatibility layer now defaults to true internally.
  • Remove deprecated @babel/polyfill from devDependencies.
Improvements
  • Code modernization across all source files:
    • Replace indexOf() > -1 / indexOf() < 0 with .includes() (~40+ occurrences)
    • Replace indexOf() === 0 with .startsWith() where appropriate
    • Replace var with const, '' + object with String(object), .substring() with .slice()
    • Replace .apply(observer, [event, ...args]) with direct call observer(event, ...args)
    • Remove unnecessary .call(this, ...) in BackendConnector retry logic
    • Fix array-callback-return in LanguageUtils getBestMatchFromCodes
    • Clean up all stale eslint-disable comments from source files
  • EventEmitter: add once() method for one-time event subscriptions
  • Memory leak fix: move module-level checkedLoadedFor cache to Translator instance, preventing cross-instance state leakage
  • TypeScript: fix BackendModule generic parameter naming inconsistency between CJS and ESM type definitions
  • TypeScript: add once() method to i18n and ResourceStore type interfaces
  • ESLint 9: migrate from ESLint 8 (airbnb-base) to ESLint 9 flat config with neostandard
  • Vitest 4: upgrade from vitest 3 to vitest 4, migrate workspace files to test.projects config

Configuration

📅 Schedule: (UTC)

  • Branch creation
    • At any time (no schedule defined)
  • Automerge
    • At any time (no schedule defined)

🚦 Automerge: Disabled by config. Please merge this manually once you are satisfied.

♻ Rebasing: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 Ignore: Close this PR and you won't be reminded about this update again.


  • If you want to rebase/retry this PR, check this box

This PR has been generated by Mend Renovate CLI.

This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [i18next](https://www.i18next.com) ([source](https://github.com/i18next/i18next)) | dependencies | major | [`^25.3.2` → `^26.0.0`](https://renovatebot.com/diffs/npm/i18next/25.10.10/26.5.11) | --- ### Release Notes <details> <summary>i18next/i18next (i18next)</summary> ### [`v26.5.11`](https://github.com/i18next/i18next/blob/HEAD/CHANGELOG.md#26511) [Compare Source](https://github.com/i18next/i18next/compare/v26.5.10...v26.5.11) - fix: a nested `false`, `0` or `''` is now returned when a flat key with the same dotted name also exists. With `{ a: { b: false }, 'a.b': 'flat' }`, `getResource('en', 'translation', 'a.b')` and `t('a.b')` returned `'flat'`: the nested result was checked for truthiness, so these present values fell through to the flat-key lookup. Without such a flat key they were already returned. A nested `null`, which is not a valid lookup by default (`returnNull: false`), still falls back to the flat key. Thanks [@&#8203;lprnmns](https://github.com/lprnmns) ([#&#8203;2461](https://github.com/i18next/i18next/pull/2461)). ### [`v26.5.10`](https://github.com/i18next/i18next/blob/HEAD/CHANGELOG.md#26510) [Compare Source](https://github.com/i18next/i18next/compare/v26.5.9...v26.5.10) - fix: when none of the detected languages is in `supportedLngs`, the best match no longer picks a different language whose code only starts with the same letters. The last fuzzy check took any supported code starting with the language part, so with `supportedLngs: ['en', 'fil-PH']` a Finnish user (`fi-FI`) got Filipino instead of the fallback language, and `ha` (Hausa) picked `haw-US` (Hawaiian). Other regions of the same language still match as before (`de-CH` → `de-DE`, `pt-PT` → `pt-BR`, `fil` → `fil-PH`). Compatibility note: a two-letter code no longer matches a supported three-letter code that extends it, like `ar` → `arz-EG`, `sw` → `swh-KE` or `no` → `nob-NO`; list the two-letter code in `supportedLngs` (or map it with `fallbackLng`) if you relied on that. Thanks [@&#8203;Pharex38](https://github.com/Pharex38) ([#&#8203;2460](https://github.com/i18next/i18next/pull/2460)). ### [`v26.5.9`](https://github.com/i18next/i18next/blob/HEAD/CHANGELOG.md#2659) [Compare Source](https://github.com/i18next/i18next/compare/v26.5.8...v26.5.9) - fix: nested lookups no longer resolve to methods inherited from the prototype, like `constructor`, `toString` or `map`. Only the top level was guarded, so `{{constructor}}` was treated as missing, but `{{user.constructor}}` rendered the function source (with a class instance, the full source code of the class), and `{{user.toString}}`, `{{items.map}}` or a class method like `{{user.greet}}` did the same. The same lookup is used for translation keys when `ignoreJSONStructure` is on (the default): a nested key named like an inherited method that is missing in the current language (`t('roles.constructor')` with `roles.constructor` only in the fallback language) returned `function Object() { [native code] }` instead of falling back, and `exists('roles.valueOf')` returned `true`. These keys are now missing, so the fallback language, `missingInterpolationHandler`, `saveMissing` and the missing key handler apply as for any other missing key. Getters defined on a class (`{{user.fullName}}` with `get fullName()`) still work. Nested lookups also no longer walk into the prototype via `__proto__`, so `{{user.__proto__.constructor}}` no longer prints the class source either. Thanks [@&#8203;Pharex38](https://github.com/Pharex38) ([#&#8203;2459](https://github.com/i18next/i18next/pull/2459)). ### [`v26.5.8`](https://github.com/i18next/i18next/blob/HEAD/CHANGELOG.md#2658) [Compare Source](https://github.com/i18next/i18next/compare/v26.5.7...v26.5.8) - fix: arrays returned with `joinArrays` now get the same resolved details as strings. The joined value was extended with `lastKey` in the place of the resolved lookup, so with `postProcessPassResolved` a post processor got no `usedLng`, `usedNS` or `exactUsedKey` (`t('steps', { joinArrays: ' / ' })` reported `undefined/undefined:undefined`). This affected the subliminal post processor used by the locize InContext editor, which could not map a joined array to its key. The nesting recursion guard did not get the parent key either, so arrays that nest each other (`listA: ['a', '$t(listB)']`, `listB: ['b', '$t(listA)']`) threw `Maximum call stack size exceeded`. They now stop at the guard, as strings do. An `i18nFormat` plugin with `handleAsObject` and `parse` also no longer throws `Cannot read properties of undefined (reading 'usedNS')` on joined arrays. Thanks [@&#8203;nk-batman21](https://github.com/nk-batman21) ([#&#8203;2458](https://github.com/i18next/i18next/pull/2458)). ### [`v26.5.7`](https://github.com/i18next/i18next/blob/HEAD/CHANGELOG.md#2657) [Compare Source](https://github.com/i18next/i18next/compare/v26.5.6...v26.5.7) - fix: with the default `cacheInBuiltFormats: true`, built-in formats now also work when the `t()` options contain a BigInt or a circular object. The cache key was built with `JSON.stringify` over all options, including the interpolation values, which throws on those, so every built-in format in that call returned the raw value: `t('num', { val: 12345678901234567890n })` with `'Total: {{val, number}}'` returned `'Total: 12345678901234567890'` instead of `'Total: 12,345,678,901,234,567,890'`, and passing an object with a back-reference (like an ORM entity) left all numbers, dates and lists of that call unformatted. When the key cannot be built, the value is now formatted without the cache, as with `cacheInBuiltFormats: false`. This also applies to formatters added with `addCached`. Other calls still use the cache as before. Thanks [@&#8203;nk-batman21](https://github.com/nk-batman21) ([#&#8203;2457](https://github.com/i18next/i18next/pull/2457)). ### [`v26.5.6`](https://github.com/i18next/i18next/blob/HEAD/CHANGELOG.md#2656) [Compare Source](https://github.com/i18next/i18next/compare/v26.5.5...v26.5.6) - fix: `cloneInstance({ forkResourceStore: true })` now copies the resources at every level. It copied them only down to the namespace objects, so nested keys and arrays stayed shared with the original instance: `addResource()` with a nested key (`fork.addResource('en', 'translation', 'greeting.hello', 'Hi')`) or a deep `addResourceBundle()` on the fork also changed the original instance and every other fork. Flat keys were already isolated. Forking a large store takes a bit longer: with 300k keys in nested groups, about 11 ms instead of 3 ms per fork. Clones without `forkResourceStore` still share the store as before. Thanks [@&#8203;nk-batman21](https://github.com/nk-batman21) ([#&#8203;2456](https://github.com/i18next/i18next/pull/2456), follow-up to [#&#8203;2270](https://github.com/i18next/i18next/issues/2270)). ### [`v26.5.5`](https://github.com/i18next/i18next/blob/HEAD/CHANGELOG.md#2655) [Compare Source](https://github.com/i18next/i18next/compare/v26.5.4...v26.5.5) - fix: with the default `skipOnVariables: true`, a `{{placeholder}}` inside a value inserted with an unescaped `{{- var}}` now stays literal, as it already did for `{{var}}`. The escape pass ran after the unescaped values were written and scanned them again, so `t('{{- a}}', { a: '{{b}}', b: 'c' })` returned `'c'` instead of `'{{b}}'`. With pre-built HTML that carries user text, like `t('You may contact {{name}} at {{- emailHTML}}.', { name, emailHTML })`, that text could pull another variable of the same `t()` call into the output. Results with `skipOnVariables: false`, and values without placeholders, are unchanged. Compatibility note: a `{{- var}}` value whose placeholders were meant to be filled from the same `t()` call now keeps them literally; `skipOnVariables: false` restores the old behavior. Thanks [@&#8203;nk-batman21](https://github.com/nk-batman21) ([#&#8203;2455](https://github.com/i18next/i18next/pull/2455), reported in [#&#8203;1595](https://github.com/i18next/i18next/issues/1595#issuecomment-814611747)). ### [`v26.5.4`](https://github.com/i18next/i18next/blob/HEAD/CHANGELOG.md#2654) [Compare Source](https://github.com/i18next/i18next/compare/v26.5.3...v26.5.4) - fix: with the default `skipOnVariables: true`, a `{{placeholder}}` inside an interpolated value now stays literal also when the same placeholder appears later in the translation. Each value was written with `String.replace`, which replaces the first copy of the placeholder text, and that could be the copy inside an earlier value: `t('{{name}} has {{count}} items', { name: '{{count}}', count: 5 })` returned `'5 has {{count}} items'` instead of `'{{count}} has 5 items'`. The value is now written at the position of the matched placeholder. Results with `skipOnVariables: false` are unchanged. Thanks [@&#8203;nk-batman21](https://github.com/nk-batman21) ([#&#8203;2454](https://github.com/i18next/i18next/pull/2454), reported in [#&#8203;1595](https://github.com/i18next/i18next/issues/1595#issuecomment-814609503)). - fix: with `useRawValueToEscape: true`, a non-string value in an unescaped `{{- var}}` placeholder or with `escapeValue: false` no longer throws `val.replace is not a function`; it is converted to a string like any other value. ### [`v26.5.3`](https://github.com/i18next/i18next/blob/HEAD/CHANGELOG.md#2653) [Compare Source](https://github.com/i18next/i18next/compare/v26.5.2...v26.5.3) - fix(types): with the selector API, a plural key now requires `count` also when the call passes an explicit `ns`. `t(($) => $.item)` already failed without `count`, but `t(($) => $.item, { ns: 'items' })` compiled, although at runtime it looks up `item` instead of `item_one` / `item_other` and returns the key. It now fails the same way, also with `ordinal: true`, `defaultValue`, an `ns` array, a key prefix and `i18n.t`. Calls with `count`, `context`, `returnObjects: true` and arrays of selectors, as well as return types, are unchanged. Thanks [@&#8203;nyarthan](https://github.com/nyarthan) ([#&#8203;2452](https://github.com/i18next/i18next/pull/2452)). ### [`v26.5.2`](https://github.com/i18next/i18next/blob/HEAD/CHANGELOG.md#2652) [Compare Source](https://github.com/i18next/i18next/compare/v26.5.1...v26.5.2) - fix(types): on TypeScript 7, a `t()` call with a missing, misspelled or wrongly typed option now reports that option again (`Property 'album' is missing`, `Type 'string' is not assignable to type 'number'`), as TypeScript 5 and 6 do. TypeScript 7 elaborated the error against the default-value signature instead and ended in `'{ count: 3; albm: "Summer"; }' is not assignable to type 'string'`: on a failed call TypeScript explains the last of the equally matching signatures, and TypeScript 7 sorts the 3-element `[key, defaultValue, options?]` last. It is now written as two signatures, `[key, defaultValue]` and `[key, defaultValue, options]`. This affects the default key checks with i18next `{{name}}` messages as well as `parseInterpolation: 'icu'` and `'mf2'`. Which calls compile, their return types and the error positions are unchanged on TypeScript 5.9, 6 and 7. ### [`v26.5.1`](https://github.com/i18next/i18next/blob/HEAD/CHANGELOG.md#26511) [Compare Source](https://github.com/i18next/i18next/compare/v26.5.0...v26.5.1) - fix: a nested `false`, `0` or `''` is now returned when a flat key with the same dotted name also exists. With `{ a: { b: false }, 'a.b': 'flat' }`, `getResource('en', 'translation', 'a.b')` and `t('a.b')` returned `'flat'`: the nested result was checked for truthiness, so these present values fell through to the flat-key lookup. Without such a flat key they were already returned. A nested `null`, which is not a valid lookup by default (`returnNull: false`), still falls back to the flat key. Thanks [@&#8203;lprnmns](https://github.com/lprnmns) ([#&#8203;2461](https://github.com/i18next/i18next/pull/2461)). ### [`v26.5.0`](https://github.com/i18next/i18next/blob/HEAD/CHANGELOG.md#2650) [Compare Source](https://github.com/i18next/i18next/compare/v26.4.7...v26.5.0) - feat(types): `parseInterpolation: 'icu'` and `parseInterpolation: 'mf2'` type `t()` options from the message's ICU MessageFormat arguments (i18next-icu) or MessageFormat 2 variables (i18next-mf2), so ICU and MF2 users no longer need `parseInterpolation: false` and lose argument checking. Missing, misspelled and wrongly typed arguments fail, and so does leaving out the options for a message with arguments (`t('greeting')`); that error names the missing arguments. Opt-in, default `true` is unchanged. - ICU: `{name}` takes `string | number`, `{n, number}`, `plural` and `selectordinal` take `number`, `{d, date}` and `{t, time}` take `Date`, `select` takes its case keys or any string (for `other`), and arguments nested in cases are required too. `#` and tags are not arguments, apostrophe quoting is read like intl-messageformat does. - MF2: `{$name}` takes `string | number`, a variable annotated with `:number`, `:integer`, `:currency`, `:percent` or `:unit` (inline or through `.input` / `.local`) takes `number`, `:datetime`, `:date` and `:time` take `Date`, `:string` takes `string`, option and markup variables (`minimumFractionDigits=$digits`, `{#link href=$url}`) are required too, `.local` variables are not arguments, and `|quoted literals|` and `\{` escapes are skipped. - `interpolationFormatTypeMap` overrides a type by ICU format or MF2 function name, e.g. `date: Date | number` or a custom `uppercase: string`. - Applies to `t()`, `getFixedT()`, `i18n.t`, key prefixes, namespaces, the selector API and react-i18next's `Trans` `values`. Messages need literal types, as the resources file from i18next-cli's `types` command has them. - Type-check cost with 1,000 messages and one call each: 1.35s instead of 0.86s with `false` (TypeScript 6), 0.5s instead of 0.26s with TypeScript 7. Messages that no call uses cost nothing. - fix(types): with the default (non-strict) key checks, an options object with a missing or misspelled interpolation variable now reports the missing variable (`Property 'name' is missing`) instead of `'{ nmae: string }' is not assignable to type 'string'` from the default-value overload. ### [`v26.4.7`](https://github.com/i18next/i18next/blob/HEAD/CHANGELOG.md#2647) [Compare Source](https://github.com/i18next/i18next/compare/v26.4.6...v26.4.7) - perf: `cloneInstance()` no longer inherits `preload`. `loadResources()` adds every `preload` language to each load, and clones copied `preload` from the parent, so a clone's `init()` and every `changeLanguage()` walked all preloaded languages × all namespaces through `queueLoad()` / `hasResourceBundle()`, although the parent had already loaded them into the shared store. With i18next-http-middleware (one clone + `changeLanguage()` per request), 25 preloaded languages and 20 namespaces, that was most of the per-request CPU: the reproduction from the issue went from \~90 µs to \~10 µs per request, the same as with a single language. A clone now only loads what it uses itself: its `changeLanguage()` no longer waits for parent preload languages that are still loading (the parent's own `init()` still does), and `clone.loadNamespaces()` loads the new namespace for the clone's language and fallbacks instead of every preloaded language. `clone.loadLanguages()` works as before, the parent instance is unchanged, and passing `preload` to `cloneInstance()` keeps the old behavior. Thanks [@&#8203;theMostCuriousHomunculus](https://github.com/theMostCuriousHomunculus) for the detailed report ([#&#8203;2451](https://github.com/i18next/i18next/issues/2451)). ### [`v26.4.6`](https://github.com/i18next/i18next/blob/HEAD/CHANGELOG.md#2646) [Compare Source](https://github.com/i18next/i18next/compare/v26.4.5...v26.4.6) - fix: a fallback language whose formatted code is already in the hierarchy is no longer added a second time. `toResolveHierarchy()` checked the raw `fallbackLng` value against the list, but the list holds formatted codes, so any fallback written differently from its formatted form slipped through: `lng: 'de-CH'` with `fallbackLng: 'de-ch'` gave `languages` `['de-CH', 'de', 'de-CH']`, and the same happened with `fallbackLng: 'EN'` + `cleanCode`, `lowerCaseLng`, and Intl aliases (`iw-IL` → `he-IL`). Missing keys were looked up in the same language twice. Only exact duplicates are removed; the order is unchanged. Thanks [@&#8203;lprnmns](https://github.com/lprnmns) ([#&#8203;2450](https://github.com/i18next/i18next/pull/2450)). ### [`v26.4.5`](https://github.com/i18next/i18next/blob/HEAD/CHANGELOG.md#2645) [Compare Source](https://github.com/i18next/i18next/compare/v26.4.4...v26.4.5) - fix: `hasLoadedNamespace(ns, { lng })` no longer stays `false` forever when the passed in `lng` is loaded under another code, or not at all. It checked the load state of the raw `lng`, but `loadLanguages()` stores a language under its resolved code (`de-ch` → `de-CH`, `de-CH` → `de` with `load: 'languageOnly'`) and skips a `lng` outside `supportedLngs` entirely (only the fallback gets loaded), so that state never got set. react-i18next's `useTranslation(ns, { lng })` then never became ready: with `useSuspense` it kept throwing an already resolved promise and React retried forever (100% CPU, SSR never finished), without it the load effect re-rendered in a loop. It now checks the code the language is actually loaded under, and for a `lng` outside `supportedLngs` only waits for the fallback, which is also what `t()` resolves to. Calls without `lng` and the `precheck` (react-i18next's `bindI18n: 'languageChanging'`) are unchanged ([react-i18next#1934](https://github.com/i18next/react-i18next/issues/1934)). ### [`v26.4.4`](https://github.com/i18next/i18next/blob/HEAD/CHANGELOG.md#2644) [Compare Source](https://github.com/i18next/i18next/compare/v26.4.3...v26.4.4) - fix(security): the nesting regexp no longer backtracks exponentially (ReDoS) on an unclosed `$t(`. Its repeated group had overlapping alternatives (`[^()"']+` inside `(?:...)*?`, and inside parentheses `[^()]` also matched quotes), so a string containing `$t(` without a closing `)` made the engine try every way to split the text: about 2x the runtime per extra character, and a \~25 character value hung `t()` for minutes. With the default config this was reachable through any interpolated value (`t('greet', { name: '$t(aaaaaaaaaaaaaaaaaaaaaaaa' })`): escaping does not touch `$`, `t` or `(`, and the `skipOnVariables` check runs the regexp on the interpolated result, so `nest: false` did not help either. Every alternative now starts with a different character (plain char, `"`, `'` or `(`), so there is only one way to match and the cost is linear. Quoted strings and one level of parentheses in nesting options keep working ([#&#8203;2336](https://github.com/i18next/i18next/issues/2336)). The only difference: a lone `"` or `'` inside unquoted parentheses within `$t(...)` is no longer matched. Affects 25.3.3 to 26.4.3. See advisory [GHSA-5jr9-983r-c8x4](https://github.com/i18next/i18next/security/advisories/GHSA-5jr9-983r-c8x4), CVSS 7.5 (`CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H`). Thanks to Anisetti Chaitanya Eshwar Prasad for the responsible disclosure. - fix(security): `nest()` now resolves `$t(...)` in a single pass and no longer nests text that came out of a resolved value. Every nested lookup reset the shared nesting regexp, so the loop started over from the beginning of the string after each match. That had two effects. First, a `$t(...)` inside a variable that the nested lookup deliberately kept literal (default `skipOnVariables: true`) was resolved anyway by the outer loop: with `outer: 'A $t(inner) B'` and `inner: 'Hi {{name}}'`, `t('outer', { name: '$t(secret)' })` rendered the `secret` translation. It now stays `A Hi $t(secret) B`, the same as without the outer nesting; with `skipOnVariables: false` it is still resolved. Second, the cost was quadratic when an unclosed `$t(` preceded many nestings, which matters when an app passes untrusted input as the key itself (`t(userInput)`): 20,000 nestings took \~1.4 s, now \~25 ms. The resolved value is now sliced into the result instead of going through `String.replace`, so `$&`, `` $` ``, `$'` and `$$` stay literal as before (26.4.2). ### [`v26.4.3`](https://github.com/i18next/i18next/blob/HEAD/CHANGELOG.md#2643) [Compare Source](https://github.com/i18next/i18next/compare/v26.4.2...v26.4.3) - perf: `ResourceStore.hasLanguageSomeTranslations()` no longer enumerates every key of a large bundle on each call. It ran `Object.keys()` on the whole bundle just to learn whether it had at least one key, and `init()` / `changeLanguage()` reach it three times per instance (once while picking the language, twice via `setResolvedLanguage()`). With large flat bundles and one i18next instance per SSR request reusing the same resources, that showed up as measurable CPU work before any `t()` call. Bundle objects already found non-empty are now remembered in a `WeakSet` shared across instances, so later checks are O(1): 2,000 inits with a 6,800-key bundle went from \~750 ms to \~20 ms, the same as a one-key bundle. Only the non-empty result is cached. No API removes single keys from a bundle, so a non-empty bundle stays non-empty, and empty bundles are re-checked every time, so keys added later (backend loads, `addResource`, `addResourceBundle`, direct or silent changes) count right away. The only edge case: a bundle emptied by hand with `delete` on every key while it is still in the store keeps counting as having translations; use `removeResourceBundle()` for that. Thanks [@&#8203;dmchoi77](https://github.com/dmchoi77) for the detailed report ([#&#8203;2449](https://github.com/i18next/i18next/issues/2449)). ### [`v26.4.2`](https://github.com/i18next/i18next/blob/HEAD/CHANGELOG.md#2642) [Compare Source](https://github.com/i18next/i18next/compare/v26.4.1...v26.4.2) - fix: `$&`, `` $` ``, `$'` and `$$` inside a nested value (`$t(key)`) now stay literal. `nest()` handed the resolved value straight to `String.replace` as the replacement argument, so those sequences were read as replacement patterns: `$&` re-inserted the `$t(...)` match, `` $` `` / `$'` inserted the text before / after it, and `$$` collapsed to `$`. Through `t()` the `$&` case was worse than a wrong string: the nested lookup resets the shared nesting regexp, so the re-inserted `$t(...)` was matched again on every pass and `t()` never returned — also under the default `escapeValue: true` when the value arrives via a variable forwarded through nesting options (`$t(key, { "name": "{{name}}" })` with a name containing `$&`). The value is now `$`-escaped at the `String.replace` call, the same guard `interpolate()` already has, and a non-string value returned by a formatter in the nesting chain (`$t(key, myFormat)`) is stringified before that. Nested values are still not HTML-escaped ([#&#8203;854](https://github.com/i18next/i18next/issues/854)). Thanks [@&#8203;mahirhir](https://github.com/mahirhir) ([#&#8203;2447](https://github.com/i18next/i18next/pull/2447)). ### [`v26.4.1`](https://github.com/i18next/i18next/blob/HEAD/CHANGELOG.md#2641) [Compare Source](https://github.com/i18next/i18next/compare/v26.4.0...v26.4.1) - fix(types): the selector-form `keyPrefix` overload of `getFixedT()` is now available under `enableSelector: 'strict'`. Its constraint was gated on `true | 'optimize'` only, so under `'strict'` it collapsed to `never`, the overload dropped out, and the returned `t` silently lost its `keyPrefix` scope (`t(($) => $.deep)` failed with `Property 'deep' does not exist on type '{}'`). The same call already typechecked under `true` and `'optimize'`. Thanks [@&#8203;hovelopin](https://github.com/hovelopin) ([#&#8203;2446](https://github.com/i18next/i18next/pull/2446)). ### [`v26.4.0`](https://github.com/i18next/i18next/blob/HEAD/CHANGELOG.md#2640) [Compare Source](https://github.com/i18next/i18next/compare/v26.3.6...v26.4.0) - perf: cache `toResolveHierarchy` results per `(code, fallbackCode)` pair. The hierarchy resolver runs on every `t()` call and calls `Intl.getCanonicalLocales` multiple times, which showed up prominently when profiling render-heavy UIs (e.g. virtualized data grids); with the cache the per-call cost drops from \~886 ns to \~41 ns. The cache is invalidated automatically when `options.fallbackLng` changes (reassignment or in-place array mutation); if you mutate other resolution-relevant options at runtime (`load`, `lowerCaseLng`, `cleanCode`, `nonExplicitSupportedLngs`), call `i18next.services.languageUtils.clearCache()` afterwards. Function-valued `fallbackLng` and per-call array/object `fallbackLng` options are never cached, so dynamic fallbacks keep working as before. Thanks [@&#8203;equaterina](https://github.com/equaterina) ([#&#8203;2444](https://github.com/i18next/i18next/pull/2444)). - chore: update all devDependencies (Babel stays on 7.x until `@rollup/plugin-babel` supports 8, eslint on 9.x for neostandard). Removed the unused `coveralls` package (CI uses the Coveralls GitHub Action) and replaced `sinon` with `nise` + `vitest.spyOn` in the v1 compatibility tests, which resolves all open `npm audit` findings (0 vulnerabilities) and should close the dependabot alerts on the lockfile. ### [`v26.3.6`](https://github.com/i18next/i18next/blob/HEAD/CHANGELOG.md#2636) [Compare Source](https://github.com/i18next/i18next/compare/v26.3.5...v26.3.6) - fix: allow TypeScript 7 in the optional `typescript` peer dependency range (`^5 || ^6 || ^7`). With `typescript@7.0.2` in a project, `npm install` failed with an `ERESOLVE` peer conflict. The published types are TS7-compatible as-is: every `test/typescript` suite produces identical results under 6.0 and 7.0.2. Reported in [react-i18next#1927](https://github.com/i18next/react-i18next/issues/1927), thanks [@&#8203;andikapradanaarif](https://github.com/andikapradanaarif). ### [`v26.3.5`](https://github.com/i18next/i18next/blob/HEAD/CHANGELOG.md#2635) [Compare Source](https://github.com/i18next/i18next/compare/v26.3.4...v26.3.5) - fix: `$t()` nesting options blocks that span multiple lines are now parsed. `nest()` decided where the nested key ends by testing `match[1]` with `/{.*}/`, whose dot does not cross line breaks — so a `$t(key, { ... })` options object containing a newline was treated as having no options, mis-split as formatters, and the nested lookup ran without its options (placeholders stayed unresolved). The nesting regexp itself already matches newlines inside `$t(...)`; adding the `s` (dotAll) flag makes multiline options behave like the single-line form. Thanks [@&#8203;spokodev](https://github.com/spokodev) ([#&#8203;2440](https://github.com/i18next/i18next/pull/2440)). - fix: `getUsedParamsDetails` (the `returnDetails: true` path) no longer mutates the passed `replace` object. It wrote `count` straight onto `options.replace` so the returned `usedParams` would include it — a caller reusing one `replace` object across `t()` calls then carried a stale `count` into later interpolations (e.g. a previous call's `count: 5` rendered instead of the current call's value). The details are now built from a copy; `usedParams` still includes `count`. Thanks [@&#8203;spokodev](https://github.com/spokodev) ([#&#8203;2441](https://github.com/i18next/i18next/pull/2441)). - fix: with the default `skipOnVariables: true` + `escapeValue: true`, a `{{placeholder}}` carried inside an interpolated value now stays literal even when the value contains escapable characters. The skip logic advanced the regex `lastIndex` by the raw value length, but the escaped text written into the string is longer, so `lastIndex` landed inside the inserted value and a trailing `{{placeholder}}` in it got interpolated — leaking another in-scope variable that should have stayed literal (values without escapable characters were already skipped correctly). The advance now uses the escaped length that is actually written, and the regex-safe `$`-doubling is applied only at the `String.replace` call so it can't distort the length arithmetic. Thanks [@&#8203;spokodev](https://github.com/spokodev) ([#&#8203;2442](https://github.com/i18next/i18next/pull/2442)). ### [`v26.3.4`](https://github.com/i18next/i18next/blob/HEAD/CHANGELOG.md#2634) [Compare Source](https://github.com/i18next/i18next/compare/v26.3.3...v26.3.4) - fix(security): `deepExtend` (used by `addResourceBundle(..., deep, overwrite)`) no longer recurses into inherited properties. It checked key existence with the `in` operator, which walks the prototype chain, so a source key matching an inherited built-in (e.g. `hasOwnProperty`, `toString`) caused recursion into the shared `Object.prototype` function and, with `overwrite: true`, could overwrite e.g. `Object.prototype.hasOwnProperty.call` with a non-callable value — corrupting a shared built-in process-wide (DoS). Existence is now checked with `Object.prototype.hasOwnProperty.call`, so such keys are copied as plain own data instead. This complements the existing `__proto__`/`constructor` guard and is also strictly more correct for an own-property merge. Only affects applications that pass attacker-controlled data with `deep: true` and `overwrite: true`; no standard backend/integration does this. Distinct from CVE-2026-48713 / CVE-2026-48714 (different packages, `setPath` mechanism). See advisory [GHSA-6jcc-5g8w-32mx](https://github.com/i18next/i18next/security/advisories/GHSA-6jcc-5g8w-32mx), CVSS 5.9 (`CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:U/C:N/I:L/A:H`). Thanks to zx (Jace) [@&#8203;manus-use](https://github.com/manus-use) for the responsible disclosure. ### [`v26.3.3`](https://github.com/i18next/i18next/blob/HEAD/CHANGELOG.md#2633) [Compare Source](https://github.com/i18next/i18next/compare/v26.3.2...v26.3.3) - fix(types): selector `t($ => $.arr, { returnObjects: true, context })` on a JSON array of **heterogeneous** objects now preserves each element's full shape (e.g. `{ transKey1: string; transKey2: string }[]`) instead of collapsing to a union of partial element types. Two type-level causes: (1) `FilterKeys` evaluated the whole array element type at once, so `keyof (A | B)` only saw the keys common to every element — it now distributes over the object union and filters each element independently; (2) when TypeScript merges mismatched array element types it injects phantom optional `undefined` keys (e.g. `transKey1_withContext?: undefined` on elements that don't define it), which the context-detection helpers mistook for real context variants — they now skip keys typed as `undefined`. Also adds a dedicated `context` + `returnObjects: true` selector overload using `const Fn` + `ReturnType<Fn>`, so `Target` is no longer collapsed to `unknown` via `ApplyTarget`. Resolves Problem 1 of [#&#8203;2398](https://github.com/i18next/i18next/issues/2398) (Problem 2 was already fixed on master). Thanks [@&#8203;sauravgupta-dotcom](https://github.com/sauravgupta-dotcom) ([#&#8203;2438](https://github.com/i18next/i18next/pull/2438)). Fixes [#&#8203;2398](https://github.com/i18next/i18next/issues/2398). ### [`v26.3.2`](https://github.com/i18next/i18next/blob/HEAD/CHANGELOG.md#2632) [Compare Source](https://github.com/i18next/i18next/compare/v26.3.1...v26.3.2) - fix: chained formatters with a parenthesised option that contains the format separator (e.g. `join(separator: ', ')`) now work at **any** position in the chain, not just first. Previously the comma-in-parens reassembly only repaired `formats[0]`, so `{{v, uppercase, join(separator: ', ')}}` split the `join(...)` option on the inner comma and never rejoined it, producing corrupt output. Replaced the first-position-only repair with a position-independent pass that re-joins fragments until each open paren closes. Thanks [@&#8203;spokodev](https://github.com/spokodev) ([#&#8203;2437](https://github.com/i18next/i18next/pull/2437)). ### [`v26.3.1`](https://github.com/i18next/i18next/blob/HEAD/CHANGELOG.md#2631) [Compare Source](https://github.com/i18next/i18next/compare/v26.3.0...v26.3.1) - fix(types): `t()` with a `keyPrefix` no longer pollutes its return type with sibling keys' values. A regression in 26.3.0 — the `[Res] extends [never]` guards added to `KeysBuilderWithReturnObjects` / `KeysBuilderWithoutReturnObjects` turned the builders into deferred conditional types, so `KeyPrefix<Ns>` stopped resolving to a literal union and `keyPrefix` inference widened to the whole namespace. Symptom: `useTranslation(ns, { keyPrefix: 'a.b' })` then `t('title')` would resolve to `'<a.b>.title' | '<other.path>.title' | ...` instead of just the scoped value. Affected every `react-i18next` user using `keyPrefix`. Restored to the eager 26.2.0 form. The same-namespace conflict handling from [#&#8203;2434](https://github.com/i18next/i18next/issues/2434) still works via `_DropConflictKeys` at the merge layer (in `options.d.ts`). Thanks [@&#8203;aaronrosenthal](https://github.com/aaronrosenthal) ([#&#8203;2436](https://github.com/i18next/i18next/pull/2436)). ### [`v26.3.0`](https://github.com/i18next/i18next/blob/HEAD/CHANGELOG.md#2630) [Compare Source](https://github.com/i18next/i18next/compare/v26.2.0...v26.3.0) - feat(types): introduce `ResourceNamespaceMap` — a separate mergeable augmentation surface for namespace resource types, designed for monorepos where multiple packages each want to contribute their own namespaces. Previously, every package had to coordinate on a single `CustomTypeOptions.resources` declaration (or fall back to typing dependency namespaces as `any`) because `resources` is a single property of an interface and TypeScript reports TS2717 when two declarations of the same property disagree. The new interface merges naturally across `declare module 'i18next'` blocks, so each package can ship its own `i18next.d.ts` independently. Per-property merge handles same-namespace contributions from multiple packages, and same-key/different-literal conflicts are silently dropped to avoid poisoning `t()` overload resolution. Fully backwards-compatible — existing `CustomTypeOptions.resources` augmentations continue to work, and both surfaces can coexist. Scalar options (`defaultNS`, `returnNull`, `enableSelector`, etc.) still belong on `CustomTypeOptions`. Thanks [@&#8203;sh3xu](https://github.com/sh3xu) ([#&#8203;2434](https://github.com/i18next/i18next/pull/2434)). Fixes [#&#8203;2409](https://github.com/i18next/i18next/issues/2409). ### [`v26.2.0`](https://github.com/i18next/i18next/blob/HEAD/CHANGELOG.md#2620) [Compare Source](https://github.com/i18next/i18next/compare/v26.1.0...v26.2.0) - feat(types): new `parseInterpolation` TypeOption (default `true`). When set to `false` in `CustomTypeOptions`, the type-level extractor stops parsing translation strings for `{{variable}}` patterns. Required by `i18next-icu` users — the default extractor mistakes ICU MessageFormat nested-brace plurals like `{count, plural, one {{count} row} other {{count} rows}}` for an interpolation block and demands a phantom variable name. The flag is type-only; runtime interpolation is governed by `InterpolationOptions` and is unaffected. Fixes [i18next-icu#85](https://github.com/i18next/i18next-icu/issues/85). - fix(types): expose `enableSelector` on `InitOptions` so `i18next.init({ enableSelector: 'strict' })` typechecks without a module augmentation. The runtime already reads `opts?.enableSelector` from init options; this lands the matching type declaration next to the other selector-resolution knobs. Accepts `false | true | 'optimize' | 'strict'`. Thanks [@&#8203;Faithfinder](https://github.com/Faithfinder) ([#&#8203;2431](https://github.com/i18next/i18next/pull/2431)) ### [`v26.1.0`](https://github.com/i18next/i18next/blob/HEAD/CHANGELOG.md#2610) [Compare Source](https://github.com/i18next/i18next/compare/v26.0.10...v26.1.0) - feat: `enableSelector: 'strict'` (TypeOptions + runtime option). Opt-in mode that drops the flattened-primary form from `NsResource` at the type level — every namespace (primary included) is exposed only under its own key on `$`, uniformly across single- and multi-ns hooks. At runtime, a leading selector path segment matching the scope's namespace list is always rewritten as a namespace prefix, including the primary. Eliminates the silent-miss surface area where `t($ => $.primary.foo)` typechecks but doesn't resolve under the default mode (see [#&#8203;2429](https://github.com/i18next/i18next/issues/2429)). Backward-compatible: default `enableSelector: false | true | 'optimize'` behavior is unchanged. Note: strict mode is incompatible with the [#&#8203;2405](https://github.com/i18next/i18next/issues/2405) pattern (keys whose names match sibling namespaces) — those users should stay on default mode. ### [`v26.0.10`](https://github.com/i18next/i18next/blob/HEAD/CHANGELOG.md#26010) [Compare Source](https://github.com/i18next/i18next/compare/v26.0.9...v26.0.10) - feat: `getFixedT` accepts a fourth optional `fixedOpts` argument carrying `scopeNs` — the full namespace list the bound `t` was created for. The selector API uses `scopeNs` to detect when a path's first segment is a namespace prefix, **without** changing resolution scope. Resolution still uses the bound `ns` (a single primary string in the typical react-i18next setup), so plain `t('key')` lookups stay isolated to the primary namespace exactly as before — only `t($ => $.secondaryNs.foo)` selectors now route correctly under `useTranslation([nsA, nsB])`. Fixes the runtime side of [#&#8203;2429](https://github.com/i18next/i18next/issues/2429) for the `react-i18next` default-`nsMode` case. The 4th argument is opt-in: existing 3-arg `getFixedT(lng, ns, keyPrefix)` callers see no behavior change. ### [`v26.0.9`](https://github.com/i18next/i18next/blob/HEAD/CHANGELOG.md#2609) [Compare Source](https://github.com/i18next/i18next/compare/v26.0.8...v26.0.9) - fix(types): unformatted interpolation values are now typed as `string | number` (was `string`). i18next stringifies values at runtime, so requiring callers to wrap numbers in `String(...)` for plain `{{var}}` placeholders was unnecessary friction — and could mask the real problem when a non-string value was passed alongside multiple interpolation slots (the `t()` overload resolution would fall through to the 3-arg form and report a confusing "not assignable to string" error against the options object). Typed format specifiers like `{{x, number}}`, `{{x, currency}}`, `{{x, datetime}}`, etc. keep their precise types; this only relaxes the no-format default. The `count` variable remains `number`-only ### [`v26.0.8`](https://github.com/i18next/i18next/blob/HEAD/CHANGELOG.md#2608) [Compare Source](https://github.com/i18next/i18next/compare/v26.0.7...v26.0.8) - fix(types): restore the pre-v25.10.4 `ExistsFunction` shape so plain arrow functions can again be assigned to `ExistsFunction`-typed variables (TypeScript cannot infer type predicates through multi-overload assignment). Direct `i18next.exists(key)` calls still narrow `key` to `SelectorKey` — the predicate is now declared inline on `i18n.exists`. Custom wrappers that want the narrowing can type themselves as `typeof i18next.exists` [2425](https://github.com/i18next/i18next/issues/2425) ### [`v26.0.7`](https://github.com/i18next/i18next/blob/HEAD/CHANGELOG.md#2607) [Compare Source](https://github.com/i18next/i18next/compare/v26.0.6...v26.0.7) - fix: when a plural lookup misses, the `missingKey` debug log now shows the actual plural-resolved key (e.g. `foo.bar_many` for Polish `count: 14`) instead of the base key — making it obvious which plural category was expected and missing [2423](https://github.com/i18next/i18next/issues/2423) - chore: drop `@babel/runtime` runtime dependency. The build no longer generates any `@babel/runtime` imports, so the package is unused by consumers. Rollup now uses `babelHelpers: 'bundled'` so any helpers that are ever needed in the future will be inlined rather than imported externally [2424](https://github.com/i18next/i18next/issues/2424) - chore: stop emitting `dist/esm/i18next.bundled.js`. It was byte-identical to `dist/esm/i18next.js` because no helpers were being imported [2424](https://github.com/i18next/i18next/issues/2424) ### [`v26.0.6`](https://github.com/i18next/i18next/blob/HEAD/CHANGELOG.md#2606) [Compare Source](https://github.com/i18next/i18next/compare/v26.0.5...v26.0.6) Security release — all issues found via an internal audit. - security: warn when a translation string combines `escapeValue: false` with interpolated variables inside a `$t(key, { ... "{{var}}" ... })` nesting-options block. In that narrow combination, attacker-controlled string values containing `"` can break out of the JSON options literal and inject additional nesting options (e.g. redirect `lng`/`ns`). The default `escapeValue: true` configuration is unaffected because HTML-escaping neutralises the quote before `JSON.parse`. See the [security note in the Nesting docs](https://www.i18next.com/translation-function/nesting#security-note-interpolated-values-inside-a-nesting-options-block) for the full pattern and mitigations - security: apply `regexEscape` to `unescapePrefix` / `unescapeSuffix` on par with the other interpolation delimiters. Prevents ReDoS (catastrophic-backtracking) when a misconfigured delimiter contains regex metacharacters, and fixes silent breakage of the `{{- var}}` syntax when the delimiter contains characters like `(`, `[`, `.` - security: strip CR/LF/NUL and other C0/C1 control characters from string log arguments to prevent log forging via user-controlled translation keys, language codes, namespaces, or interpolation variable names (CWE-117) - chore: ignore `.env*` and `*.pem`/`*.key` files in `.gitignore` ### [`v26.0.5`](https://github.com/i18next/i18next/blob/HEAD/CHANGELOG.md#2605) [Compare Source](https://github.com/i18next/i18next/compare/v26.0.4...v26.0.5) - fix: `cloneInstance().changeLanguage()` no longer fails to update language state when the target language is not yet loaded — a race between `init()`'s deferred `load()` and the user's `changeLanguage()` could overwrite `isLanguageChangingTo`, causing `setLngProps` to be skipped [2422](https://github.com/i18next/i18next/issues/2422) ### [`v26.0.4`](https://github.com/i18next/i18next/blob/HEAD/CHANGELOG.md#2604) [Compare Source](https://github.com/i18next/i18next/compare/v26.0.3...v26.0.4) - fix(types): inline formatting options like `{{price, currency(EUR)}}` are now correctly resolved to their base format type (e.g. `number` for `currency`) instead of falling back to `string` [2378](https://github.com/i18next/i18next/issues/2378) ### [`v26.0.3`](https://github.com/i18next/i18next/blob/HEAD/CHANGELOG.md#2603) [Compare Source](https://github.com/i18next/i18next/compare/v26.0.2...v26.0.3) - fix(types): `addResourceBundle` now accepts an optional 6th `options` parameter (`{ silent?: boolean; skipCopy?: boolean }`) matching the runtime API [2419](https://github.com/i18next/i18next/issues/2419) ### [`v26.0.2`](https://github.com/i18next/i18next/blob/HEAD/CHANGELOG.md#2602) [Compare Source](https://github.com/i18next/i18next/compare/v26.0.1...v26.0.2) - fix(types): `t("key", {} as TOptions)` no longer produces a type error — the context constraint now bypasses strict checking when `context` is `unknown` (e.g. from `TOptions`) [2418](https://github.com/i18next/i18next/issues/2418) ### [`v26.0.1`](https://github.com/i18next/i18next/blob/HEAD/CHANGELOG.md#26010) [Compare Source](https://github.com/i18next/i18next/compare/v26.0.0...v26.0.1) - feat: `getFixedT` accepts a fourth optional `fixedOpts` argument carrying `scopeNs` — the full namespace list the bound `t` was created for. The selector API uses `scopeNs` to detect when a path's first segment is a namespace prefix, **without** changing resolution scope. Resolution still uses the bound `ns` (a single primary string in the typical react-i18next setup), so plain `t('key')` lookups stay isolated to the primary namespace exactly as before — only `t($ => $.secondaryNs.foo)` selectors now route correctly under `useTranslation([nsA, nsB])`. Fixes the runtime side of [#&#8203;2429](https://github.com/i18next/i18next/issues/2429) for the `react-i18next` default-`nsMode` case. The 4th argument is opt-in: existing 3-arg `getFixedT(lng, ns, keyPrefix)` callers see no behavior change. ### [`v26.0.0`](https://github.com/i18next/i18next/blob/HEAD/CHANGELOG.md#2600) [Compare Source](https://github.com/i18next/i18next/compare/v25.10.10...v26.0.0) **This is a major breaking release:** ##### Breaking Changes - **Remove deprecated `initImmediate` option** — the backward-compatibility mapping from `initImmediate` to `initAsync` (introduced in v24) has been removed. Use `initAsync` instead. - **Remove legacy `interpolation.format` function** — the old monolithic format function (`interpolation: { format: (value, format, lng) => ... }`) is no longer supported. The built-in Formatter (or a custom Formatter module via `.use()`) is now always used. Migrate to the [new formatting approach](https://www.i18next.com/translation-function/formatting) using `i18next.services.formatter.add()` or `.addCached()` for custom formatters. - **Remove console support notice** — the console support notice introduced in v25.8.0 has been removed, along with the `showSupportNotice` option and all related internal suppression logic (`globalThis.__i18next_supportNoticeShown`, `I18NEXT_NO_SUPPORT_NOTICE` env var). See our blog post for the [full story](https://www.locize.com/blog/i18next-support-notice). - **Remove `simplifyPluralSuffix` option** — this option was unused by the core PluralResolver (which relies entirely on `Intl.PluralRules`). It only had an effect in the old v1/v2/v3 compatibility layer. The v4 test compatibility layer now defaults to `true` internally. - **Remove deprecated `@babel/polyfill`** from devDependencies. ##### Improvements - **Code modernization** across all source files: - Replace `indexOf() > -1` / `indexOf() < 0` with `.includes()` (\~40+ occurrences) - Replace `indexOf() === 0` with `.startsWith()` where appropriate - Replace `var` with `const`, `'' + object` with `String(object)`, `.substring()` with `.slice()` - Replace `.apply(observer, [event, ...args])` with direct call `observer(event, ...args)` - Remove unnecessary `.call(this, ...)` in BackendConnector retry logic - Fix `array-callback-return` in LanguageUtils `getBestMatchFromCodes` - Clean up all stale `eslint-disable` comments from source files - **EventEmitter**: add `once()` method for one-time event subscriptions - **Memory leak fix**: move module-level `checkedLoadedFor` cache to Translator instance, preventing cross-instance state leakage - **TypeScript**: fix `BackendModule` generic parameter naming inconsistency between CJS and ESM type definitions - **TypeScript**: add `once()` method to `i18n` and `ResourceStore` type interfaces - **ESLint 9**: migrate from ESLint 8 (airbnb-base) to ESLint 9 flat config with [neostandard](https://github.com/neostandard/neostandard) - **Vitest 4**: upgrade from vitest 3 to vitest 4, migrate workspace files to `test.projects` config </details> --- ### Configuration 📅 **Schedule**: (UTC) - Branch creation - At any time (no schedule defined) - Automerge - At any time (no schedule defined) 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box --- This PR has been generated by [Mend Renovate CLI](https://github.com/renovatebot/renovate). <!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My45Ni4wIiwidXBkYXRlZEluVmVyIjoiNDQuMTgxLjYiLCJ0YXJnZXRCcmFuY2giOiJtYWluIiwibGFiZWxzIjpbXX0=-->
Update dependency i18next to v26
All checks were successful
ci/woodpecker/pr/testbuild Pipeline was successful
33012ba899
Renovate force-pushed renovate/i18next-26.x from 33012ba899
All checks were successful
ci/woodpecker/pr/testbuild Pipeline was successful
to d67935b94f
All checks were successful
ci/woodpecker/pr/testbuild Pipeline was successful
2026-07-25 05:30:09 +00:00
Compare
All checks were successful
ci/woodpecker/pr/testbuild Pipeline was successful
This pull request can be merged automatically.
This branch is out-of-date with the base branch
You are not authorized to merge this pull request.
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin renovate/i18next-26.x:renovate/i18next-26.x
git switch renovate/i18next-26.x

Merge

Merge the changes and update on Forgejo.

Warning: The "Autodetect manual merge" setting is not enabled for this repository, you will have to mark this pull request as manually merged afterwards.

git switch main
git merge --no-ff renovate/i18next-26.x
git switch renovate/i18next-26.x
git rebase main
git switch main
git merge --ff-only renovate/i18next-26.x
git switch renovate/i18next-26.x
git rebase main
git switch main
git merge --no-ff renovate/i18next-26.x
git switch main
git merge --squash renovate/i18next-26.x
git switch main
git merge --ff-only renovate/i18next-26.x
git switch main
git merge renovate/i18next-26.x
git push origin main
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
Bluemedia/LibreCharge!554
No description provided.