Sourced from i18next's releases.
v26.3.6
- 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.v26.3.5
- 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).v26.3.4
- 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.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)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.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 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).
Sourced from i18next's changelog.
26.3.6
- 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.26.3.5
- 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).26.3.4
- 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). 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-usefor the responsible disclosure.26.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)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.26.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 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).
e1c60d4
26.3.604da43e
fix: allow typescript 7 in optional peerDependencies range
(react-i18next#1927)8eed4ac
build573ae73
26.3.5cc54b05
docs(changelog): 26.3.5 — multiline $t() options, replace mutation,
escaped-l...3180d67
fix: skip interpolation of placeholders inside escaped values (#2442)d16f5a2
fix: stop mutating the passed replace object when returning details (#2441)bed56c1
fix: parse $t() nesting options block that spans multiple lines (#2440)c19e458
docs(changelog): link GHSA advisory for deepExtend fix7bb87d0
docs(changelog): reference security advisory for deepExtend fix