- fix: allow TypeScript 7 in the optional
typescriptpeer dependency range (^5 || ^6 || ^7). Withtypescript@7.0.2in a project,npm installfailed with anERESOLVEpeer conflict. The published types are TS7-compatible as-is: everytest/typescriptsuite produces identical results under 6.0 and 7.0.2. Reported in react-i18next#1927, thanks @andikapradanaarif.
i18next
Frameworks & Librariesi18next release notes.
Changelog
- fix:
$t()nesting options blocks that span multiple lines are now parsed.nest()decided where the nested key ends by testingmatch[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 thes(dotAll) flag makes multiline options behave like the single-line form. Thanks @spokodev (#2440). - fix:
getUsedParamsDetails(thereturnDetails: truepath) no longer mutates the passedreplaceobject. It wrotecountstraight ontooptions.replaceso the returnedusedParamswould include it — a caller reusing onereplaceobject acrosst()calls then carried a stalecountinto later interpolations (e.g. a previous call'scount: 5rendered instead of the current call's value). The details are now built from a copy;usedParamsstill includescount. 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 regexlastIndexby the raw value length, but the escaped text written into the string is longer, solastIndexlanded 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 theString.replacecall so it can't distort the length arithmetic. Thanks @spokodev (#2442).
- fix(security):
deepExtend(used byaddResourceBundle(..., deep, overwrite)) no longer recurses into inherited properties. It checked key existence with theinoperator, which walks the prototype chain, so a source key matching an inherited built-in (e.g.hasOwnProperty,toString) caused recursion into the sharedObject.prototypefunction and, withoverwrite: true, could overwrite e.g.Object.prototype.hasOwnProperty.callwith a non-callable value — corrupting a shared built-in process-wide (DoS). Existence is now checked withObject.prototype.hasOwnProperty.call, so such keys are copied as plain own data instead. This complements the existing__proto__/constructorguard and is also strictly more correct for an own-property merge. Only affects applications that pass attacker-controlled data withdeep: trueandoverwrite: true; no standard backend/integration does this. Distinct from CVE-2026-48713 / CVE-2026-48714 (different packages,setPathmechanism). Thanks to zx (Jace) for the responsible disclosure.
- 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)FilterKeysevaluated the whole array element type at once, sokeyof (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 optionalundefinedkeys (e.g.transKey1_withContext?: undefinedon elements that don't define it), which the context-detection helpers mistook for real context variants — they now skip keys typed asundefined. Also adds a dedicatedcontext+returnObjects: trueselector overload usingconst Fn+ReturnType<Fn>, soTargetis no longer collapsed tounknownviaApplyTarget. Resolves Problem 1 of #2398 (Problem 2 was already fixed on master). Thanks @sauravgupta-dotcom (#2438). Fixes #2398.
- 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 repairedformats[0], so{{v, uppercase, join(separator: ', ')}}split thejoin(...)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).
- fix(types):
t()with akeyPrefixno longer pollutes its return type with sibling keys' values. A regression in 26.3.0 — the[Res] extends [never]guards added toKeysBuilderWithReturnObjects/KeysBuilderWithoutReturnObjectsturned the builders into deferred conditional types, soKeyPrefix<Ns>stopped resolving to a literal union andkeyPrefixinference widened to the whole namespace. Symptom:useTranslation(ns, { keyPrefix: 'a.b' })thent('title')would resolve to'<a.b>.title' | '<other.path>.title' | ...instead of just the scoped value. Affected everyreact-i18nextuser usingkeyPrefix. Restored to the eager 26.2.0 form. The same-namespace conflict handling from #2434 still works via_DropConflictKeysat the merge layer (inoptions.d.ts). Thanks @aaronrosenthal (#2436).
- 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 singleCustomTypeOptions.resourcesdeclaration (or fall back to typing dependency namespaces asany) becauseresourcesis a single property of an interface and TypeScript reports TS2717 when two declarations of the same property disagree. The new interface merges naturally acrossdeclare module 'i18next'blocks, so each package can ship its owni18next.d.tsindependently. Per-property merge handles same-namespace contributions from multiple packages, and same-key/different-literal conflicts are silently dropped to avoid poisoningt()overload resolution. Fully backwards-compatible — existingCustomTypeOptions.resourcesaugmentations continue to work, and both surfaces can coexist. Scalar options (defaultNS,returnNull,enableSelector, etc.) still belong onCustomTypeOptions. Thanks @sh3xu (#2434). Fixes #2409.
- feat(types): new
parseInterpolationTypeOption (defaulttrue). When set tofalseinCustomTypeOptions, the type-level extractor stops parsing translation strings for{{variable}}patterns. Required byi18next-icuusers — 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 byInterpolationOptionsand is unaffected. Fixes i18next-icu#85. - fix(types): expose
enableSelectoronInitOptionssoi18next.init({ enableSelector: 'strict' })typechecks without a module augmentation. The runtime already readsopts?.enableSelectorfrom init options; this lands the matching type declaration next to the other selector-resolution knobs. Acceptsfalse | true | 'optimize' | 'strict'. Thanks @Faithfinder (#2431)
- feat:
enableSelector: 'strict'(TypeOptions + runtime option). Opt-in mode that drops the flattened-primary form fromNsResourceat 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 wheret($ => $.primary.foo)typechecks but doesn't resolve under the default mode (see #2429). Backward-compatible: defaultenableSelector: 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.
- feat:
getFixedTaccepts a fourth optionalfixedOptsargument carryingscopeNs— the full namespace list the boundtwas created for. The selector API usesscopeNsto detect when a path's first segment is a namespace prefix, without changing resolution scope. Resolution still uses the boundns(a single primary string in the typical react-i18next setup), so plaint('key')lookups stay isolated to the primary namespace exactly as before — onlyt($ => $.secondaryNs.foo)selectors now route correctly underuseTranslation([nsA, nsB]). Fixes the runtime side of #2429 for thereact-i18nextdefault-nsModecase. The 4th argument is opt-in: existing 3-arggetFixedT(lng, ns, keyPrefix)callers see no behavior change.