Repository navigation
fix(deps): update dependency i18next to v26 - autoclosed - #179
Closed
renovate[bot] wants to merge 1 commit into
Closed
renovate[bot] wants to merge 1 commit into
renovate[bot] wants to merge 1 commit into
Conversation
renovate
Bot
force-pushed
the
renovate/i18next-26.x
branch
3 times, most recently
from
March 31, 2026 15:07
642aaf4 to
811b648
Compare
renovate
Bot
force-pushed
the
renovate/i18next-26.x
branch
3 times, most recently
from
April 15, 2026 13:02
99626ea to
a23db07
Compare
renovate
Bot
force-pushed
the
renovate/i18next-26.x
branch
4 times, most recently
from
April 24, 2026 22:25
d0cb913 to
ce6699b
Compare
renovate
Bot
force-pushed
the
renovate/i18next-26.x
branch
from
April 29, 2026 11:06
ce6699b to
f85f4aa
Compare
renovate
Bot
force-pushed
the
renovate/i18next-26.x
branch
5 times, most recently
from
May 12, 2026 15:01
d61234e to
459190d
Compare
renovate
Bot
force-pushed
the
renovate/i18next-26.x
branch
3 times, most recently
from
May 18, 2026 15:07
57be8d1 to
08b9263
Compare
renovate
Bot
force-pushed
the
renovate/i18next-26.x
branch
2 times, most recently
from
May 28, 2026 15:04
6f6bbe2 to
3014d0d
Compare
renovate
Bot
force-pushed
the
renovate/i18next-26.x
branch
from
June 11, 2026 21:12
3014d0d to
eb35261
Compare
renovate
Bot
force-pushed
the
renovate/i18next-26.x
branch
2 times, most recently
from
June 30, 2026 09:57
70af6f8 to
324f292
Compare
renovate
Bot
force-pushed
the
renovate/i18next-26.x
branch
3 times, most recently
from
July 9, 2026 17:40
bd7043b to
e094ee6
Compare
renovate
Bot
force-pushed
the
renovate/i18next-26.x
branch
from
July 12, 2026 12:30
e094ee6 to
7d83180
Compare
renovate
Bot
force-pushed
the
renovate/i18next-26.x
branch
from
August 22, 2026 04:04
7d83180 to
8878e1b
Compare
renovate
Bot
force-pushed
the
renovate/i18next-26.x
branch
3 times, most recently
from
August 26, 2026 17:15
bda5771 to
00aa45e
Compare
renovate
Bot
force-pushed
the
renovate/i18next-26.x
branch
2 times, most recently
from
September 7, 2026 18:11
833a9d0 to
089d06e
Compare
renovate
Bot
force-pushed
the
renovate/i18next-26.x
branch
4 times, most recently
from
October 17, 2026 21:24
580d4b5 to
76e9de3
Compare
renovate
Bot
force-pushed
the
renovate/i18next-26.x
branch
8 times, most recently
from
October 26, 2026 19:02
8cd4f00 to
dbbbe8f
Compare
renovate
Bot
force-pushed
the
renovate/i18next-26.x
branch
2 times, most recently
from
November 2, 2026 01:56
021912f to
2d894f9
Compare
renovate
Bot
force-pushed
the
renovate/i18next-26.x
branch
from
November 5, 2026 17:42
2d894f9 to
da664b9
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Note
This PR body was truncated due to platform limits.
This PR contains the following updates:
^25.6.3→^26.0.0Release Notes
i18next/i18next (i18next)
v26.7.2Compare Source
'iw'into'he'or'tl'into'fil'and asked you to rename them. v27 only gives such codes their canonical casing ('EN'becomes'en','IW'becomes'iw'), soiw,tl,engand similar codes stay as they are. If you renamed them because of that warning, you can keep or restore the old codes. The warning now only appears when the casing changes.v26.7.1Compare Source
reloadResources()retries the bundles that failed to load. The load queue skipped them also when reloading, so a language whose first load failed (a backend or network outage at startup) stayed missing. i18next-http-backend 4.0.8, i18next-chained-backend 5.0.7 and i18next-locize-backend 10.0.5 reload throughreloadResources()with i18next 26.7, so theirreloadIntervalno longer recovered from such a failure, as their own reload did before.reloadResources()without languages reloads the current languages and every language the backend loaded or tried to load, for the instance and its clones, as the API docs describe ("pass languages and namespaces if you don't want to reload all"). It reloaded only the current languages, so on a server (one clone per request with i18next-http-middleware) a backend'sreloadIntervalnever refreshed the languages loaded for requests. It never requestscimode. With languages given, nothing changes.v26.7.0Compare Source
The last 26.x minor before v27, which follows in about a week. It adds the APIs v27 builds on, and with
debug: trueit warns wherever a setup relies on something v27 changes. Most of the v27 defaults are existing options, so you can try them now:The other v27 changes (
/no longer escaped in values, a logger and post-processors per instance, the key instead of a sentence for an object hit, the dotted-language store calls and*Escapedoptions removed) have no switch; the debug warnings below show where they apply.skipOnVariables: true, a missing interpolation variable is now logged in debug mode, once per string and variable:missed to pass in variable name for interpolating Hello {{name}}. The raw{{name}}reached the UI without any log line, even withdebug: true, because the warning only ran withskipOnVariables: false. A variable passed asundefinedand amissingInterpolationHandlerstill log nothing.i18next.hydrate(resources, lng)puts resources rendered on the server into the store and changes to the server's language, each only the first time. It does what react-i18next'suseSSRdid by writingresourceStore.data,options.ns,isInitialized,initializedStoreOnceandinitializedLanguageOncedirectly, and sets the same flags, so integrations can switch to it and stay compatible with code that still reads or writes them. On a clone it does nothing, asuseSSRdoes: a clone shares its parent's store (one clone per request with i18next-http-middleware). Called beforeinit(), it logs a warning and changes nothing.services.utils.reloadResources()lets a backend reload its bundles through i18next (asi18next.reloadResources(), with retries and theloadedevent), instead of callingbackendConnector.read()andloaded()itself as thereloadIntervalof i18next-http-backend and i18next-chained-backend does. Without languages (nolng, no detector, the defaultfallbackLng),reloadResources()reloads nothing instead of throwingCannot read properties of undefined (reading 'forEach'); a backend that also reloadspreloadpasses its languages.callbackApi(an instance or static property).true:read(language, namespace, callback),create(languages, namespace, key, fallbackValue, callback, options)anddetect(callback);false: they return a promise or a value, andcreategets the options as its fifth argument. Without it, i18next keeps guessing from the number of parameters (Function.length), which default and rest parameters change: i18next-chained-backend'screate(..., clb = () => {}, opts = {})has length 4 and never got its options (isUpdate), and i18next-http-backend's callback form ofcreatehas length 5, the length reserved for the callback-less form. Guessing is deprecated: in debug mode an unmarked backend or async detector logs a deprecation warning, once perinit().isSafeLanguageCode(code)is exported for plugins that take a language from a request, a URL or storage: letters, digits,-,_,@and.(no..), at most 128 characters, not a prototype key. It is not a BCP 47 check, soen_US,zh_Hant_TW,sr@latinandde_DE.UTF-8pass; markup, slashes and spaces do not. The language detector, i18next-http-middleware and i18next-http-backend each have their own rule today.use()logs a warning in debug mode when a second backend, language detector,i18nFormator formatter replaces the first one; only one of each is used per instance, and losing an ICU setup this way was silent. Asuse()usually runs beforeinit(), the warning is printed onceinit()has set up the logger.ParseKeysWithoutVariables<Ns, KPrefix>, the keyst()takes without options: their string has no variables, and a plural key is left out when its forms takecount. A wrapper typed with it, like(key: ParseKeysWithoutVariables<'common'>) => t(key), compiles withstrictInterpolationCheckswithout a cast.defaultVariablesinCustomTypeOptionsdeclares the variablesinterpolation.defaultVariablesfills in at runtime (defaultVariables: { appName: string }). They become optional in thet()options, also withstrictInterpolationChecks; passed, they keep their type.interpolationFormatSeparatorinCustomTypeOptionsfor a custominterpolation.formatSeparator: the types split{{price, currency}}at a hard-coded,, so withformatSeparator: ';'the format was not recognized. The per-callskipInterpolationoption is typed, andreturnedObjectHandlermay return a value (what it returns is whatt()returns; it was typed as returningvoid).SelectorFn,ApplyTarget,ConstrainTargetandGetSourceare exported on purpose, under their current names. react-i18next imports them forTrans; they were importable only because every declaration in the type files counts as exported.nsSeparator: falseinCustomTypeOptions, the key union no longer containsnsfalsekey-style keys for the other namespaces, and a key that contains the textfalse(isfalsey) is no longer split at it, which typed itst()result asnever.saveMissingnow checks that the namespace is loaded for the instance that misses the key. EachcloneInstance()pointed the parent'sservices.utils.hasLoadedNamespaceat the new clone, and the backend connector, which all clones share, used that one: a key missing in one request was not saved (did not save key ... as the namespace ... was not yet loaded) while a newer request was still loading its language, and the parent'sservices.utilsanswered for the newest clone. A clone'sservices.utilsis now bound to the clone, and the parent keeps its own. A directservices.backendConnector.saveMissing()call, like the missing keys route of i18next-http-middleware, still checks the newest clone (follow-up to #1413).dir()without a language (beforeinit(), or with no language resolved) returns'ltr'instead of'rtl'(discussion #2135).t()beforeinit()returns an empty string, as typed (with a warning when debug logging is already on); it returnedundefined.off('a b', listener)removes the listener from both events, ason('a b', listener)adds it to both. It looked for an event named'a b', soonce('a b', listener)never removed its listener and ran it on every event.getResource('en.translation.key'),addResource('en.translation.key', value),addResourceBundle('en.translation', resources)) log a deprecation warning in debug mode; they are removed in v27. Pass language, namespace and key as separate arguments.interpolation.prefixEscaped,suffixEscaped,nestingPrefixEscapedandnestingSuffixEscaped(inserted into the RegExp unescaped) log a deprecation warning in debug mode and are marked@deprecated; they are removed in v27. Pass the literal delimiters asprefix,suffix,nestingPrefixandnestingSuffix.compatibilityJSONis marked@deprecated(it has no effect since v24) and removed from the types in v27.'EN'stays'EN'today and becomes'en'in v27,'iw'becomes'he'.cleanCode: truealready lower-cases them.fallbackLngis not set, the first request for the default fallback languagedev(loading it, or saving missing keys to it) logs a notice in debug mode: v27 defaults tofallbackLng: falseand no longer requestsdev. SetfallbackLngto your source language, or to'dev'to keep the current behavior.lng,changeLanguage()or the language detector, is not configured (supportedLngs,fallbackLng,preloador the resources) and failsisSafeLanguageCodelogs a deprecation warning, once per code: withoutsupportedLngs, a detector that returns"><img src=x onerror=alert(1)>makes that stringi18next.language, and apps put it into<html lang>. Nothing changes in 26.7; v27 is planned to refuse such codes and continue as with an unsupported code. Language codes are still not bound to a format.use()logs a deprecation warning (calluse()on this instance too), and a non-cloneinit()that replaces the logger another instance set withuse()logs one through that logger./under the default escape logs that v27 no longer escapes/($10/monthrenders as$10/monthtoday; not withescapeValue: false, where react-i18next'sTransescapes one call itself and decodes the entities afterwards), and an object or array hit withoutreturnObjectslogs that v27 returns the key instead of the sentencekey '... (en)' returned an object instead of string..fileswhitelist instead of.npmignore, which had to mirror every local or secret file by hand. The tarball is unchanged. The rooti18next.jsandi18next.min.js(copies ofdist/umd/, still published for the unversioned unpkg URLs) are built bynpm run buildand no longer tracked in git.v26.6.0Compare Source
strictInterpolationCheckstype option (defaultfalse). WithstrictInterpolationChecks: trueinCustomTypeOptions,t()needs its options for a string with{{variables}}: for'Hello {{name}}!',t('greeting')is a type error andt('greeting', { name })compiles. Left out, the variable reaches the UI as the raw{{name}}placeholder. This applies to string keys, selectors, selector arrays andgetFixedT, and withstrictKeyChecksalso tot(key, defaultValue). Without the flag nothing changes. With the flag on, a wrapper that passes on a key type ((key: ParseKeys<'ns'>) => t(key), or a genericK extends ParseKeys) no longer compiles without options, as the variables of its string are not known there.parseInterpolation: 'icu'and'mf2'require their arguments as before. Thanks @Alex0007 (#2465).count, as a single selector does since v25.9.0. Withoutcount, i18next skips the plural member at runtime, sot([($) => $.item, ($) => $.plain])returned theplainvalue while typed as the plural forms. An array of selectors withdefaultValuenow returns the literal values and the default value ('a' | 'b' | 'x') instead ofstring. Thanks @Alex0007 (#2465).v26.5.12Compare Source
keySeparator: falseor a custom separator like'::', a nested interpolation value that is0,falseor''is now inserted.'You have {{stats.unread}} unread messages'with{ stats: { unread: 0 } }kept the raw{{stats.unread}}placeholder (or became''withskipOnVariables: false), also for formatted values like{{order.total, currency(USD)}}and for nesteddefaultVariables: a falsy value was looked up again with the configured separator, which does not splitstats.unread. The default'.'separator was not affected. When the same name exists both nested and as a flat dotted key ({ a: { b: 0 }, 'a.b': 'flat' }), the nested0,falseor''now wins, as a truthy one already did; a nestednullstill falls back to the flat key. Thanks @nk-batman21 (#2464).cloneInstance({ forkResourceStore: true })no longer changes the options object it is given. It deletedforkResourceStorefrom it, so reusing the same object for a second clone silently shared the parent's resource store (adding a resource to that clone also changed the parent), and frozen options threw aTypeError. Clones withoutforkResourceStoreare unchanged. Thanks @edsonsims (#2463).t(($) => ...)(enableSelector: true/'strict') no longer filters the whole namespace, and no longer does it again for every distinctcontextvalue. To drop keys without anything to select, the filtered type looked at the filtered keys of every nested object, down to the leaves, also for the branches the selector does not read. It now decides from the source value. With 10,000 keys in 100 groups, one call went from 3.3M type instantiations (1.7s) to 0.85M (0.8s), and ten calls with ten contexts from 21.7M (8.8s) to 1.05M (0.9s), on TypeScript 6. Selected values are unchanged for context, plural, ordinal, nested and array keys, with three side effects: a type error on a selector now shows the type asFilterKeysObject<{ ...source keys }, Context>instead of the filtered object, an object whose values are allnullorundefined({ x: null }) is kept as{}instead of being dropped, and anullkey next to its plural forms (foo: null,foo_one,foo_other) is typed as the plural value instead ofnever. Thanks @GabrielVooDB (#2462).v26.5.11Compare Source
false,0or''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')andt('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 nestednull, which is not a valid lookup by default (returnNull: false), still falls back to the flat key. Thanks @lprnmns (#2461).v26.5.10Compare Source
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 withsupportedLngs: ['en', 'fil-PH']a Finnish user (fi-FI) got Filipino instead of the fallback language, andha(Hausa) pickedhaw-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, likear→arz-EG,sw→swh-KEorno→nob-NO; list the two-letter code insupportedLngs(or map it withfallbackLng) if you relied on that. Thanks @Pharex38 (#2460).v26.5.9Compare Source
constructor,toStringormap. 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 whenignoreJSONStructureis on (the default): a nested key named like an inherited method that is missing in the current language (t('roles.constructor')withroles.constructoronly in the fallback language) returnedfunction Object() { [native code] }instead of falling back, andexists('roles.valueOf')returnedtrue. These keys are now missing, so the fallback language,missingInterpolationHandler,saveMissingand the missing key handler apply as for any other missing key. Getters defined on a class ({{user.fullName}}withget 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.8Compare Source
joinArraysnow get the same resolved details as strings. The joined value was extended withlastKeyin the place of the resolved lookup, so withpostProcessPassResolveda post processor got nousedLng,usedNSorexactUsedKey(t('steps', { joinArrays: ' / ' })reportedundefined/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)']) threwMaximum call stack size exceeded. They now stop at the guard, as strings do. Ani18nFormatplugin withhandleAsObjectandparsealso no longer throwsCannot read properties of undefined (reading 'usedNS')on joined arrays. Thanks @nk-batman21 (#2458).v26.5.7Compare Source
cacheInBuiltFormats: true, built-in formats now also work when thet()options contain a BigInt or a circular object. The cache key was built withJSON.stringifyover 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 withcacheInBuiltFormats: false. This also applies to formatters added withaddCached. Other calls still use the cache as before. Thanks @nk-batman21 (#2457).v26.5.6Compare Source
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 deepaddResourceBundle()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 withoutforkResourceStorestill share the store as before. Thanks @nk-batman21 (#2456, follow-up to #2270).v26.5.5Compare Source
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, sot('{{- a}}', { a: '{{b}}', b: 'c' })returned'c'instead of'{{b}}'. With pre-built HTML that carries user text, liket('You may contact {{name}} at {{- emailHTML}}.', { name, emailHTML }), that text could pull another variable of the samet()call into the output. Results withskipOnVariables: false, and values without placeholders, are unchanged. Compatibility note: a{{- var}}value whose placeholders were meant to be filled from the samet()call now keeps them literally;skipOnVariables: falserestores the old behavior. Thanks @nk-batman21 (#2455, reported in #1595).v26.5.4Compare Source
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 withString.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 withskipOnVariables: falseare unchanged. Thanks @nk-batman21 (#2454, reported in #1595).useRawValueToEscape: true, a non-string value in an unescaped{{- var}}placeholder or withescapeValue: falseno longer throwsval.replace is not a function; it is converted to a string like any other value.v26.5.3Compare Source
countalso when the call passes an explicitns.t(($) => $.item)already failed withoutcount, butt(($) => $.item, { ns: 'items' })compiled, although at runtime it looks upiteminstead ofitem_one/item_otherand returns the key. It now fails the same way, also withordinal: true,defaultValue, annsarray, a key prefix andi18n.t. Calls withcount,context,returnObjects: trueand arrays of selectors, as well as return types, are unchanged. Thanks @nyarthan (#2452).v26.5.2Compare Source
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 asparseInterpolation: 'icu'and'mf2'. Which calls compile, their return types and the error positions are unchanged on TypeScript 5.9, 6 and 7.v26.5.1Compare Source
keySeparator: falseor a custom separator like'::', a nested interpolation value that is0,falseor''is now inserted.'You have {{stats.unread}} unread messages'with{ stats: { unread: 0 } }kept the raw{{stats.unread}}placeholder (or became''withskipOnVariables: false), also for formatted values like{{order.total, currency(USD)}}and for nesteddefaultVariables: a falsy value was looked up again with the configured separator, which does not splitstats.unread. The default'.'separator was not affected. When the same name exists both nested and as a flat dotted key ({ a: { b: 0 }, 'a.b': 'flat' }), the nested0,falseor''now wins, as a truthy one already did; a nestednullstill falls back to the flat key. Thanks @nk-batman21 (#2464).cloneInstance({ forkResourceStore: true })no longer changes the options object it is given. It deletedforkResourceStorefrom it, so reusing the same object for a second clone silently shared the parent's resource store (adding a resource to that clone also changed the parent), and frozen options threw aTypeError. Clones withoutforkResourceStoreare unchanged. Thanks @edsonsims (#2463).t(($) => ...)(enableSelector: true/'strict') no longer filters the whole namespace, and no longer does it again for every distinctcontextvalue. To drop keys without anything to select, the filtered type looked at the filtered keys of every nested object, down to the leaves, also for the branches the selector does not read. It now decides from the source value. With 10,000 keys in 100 groups, one call went from 3.3M type instantiations (1.7s) to 0.85M (0.8s), and ten calls with ten contexts from 21.7M (8.8s) to 1.05M (0.9s), on TypeScript 6. Selected values are unchanged for context, plural, ordinal, nested and array keys, with three side effects: a type error on a selector now shows the type asFilterKeysObject<{ ...source keys }, Context>instead of the filtered object, an object whose values are allnullorundefined({ x: null }) is kept as{}instead of being dropped, and anullkey next to its plural forms (foo: null,foo_one,foo_other) is typed as the plural value instead ofnever. Thanks @GabrielVooDB (#2462).v26.5.0Compare Source
parseInterpolation: 'icu'andparseInterpolation: 'mf2'typet()options from the message's ICU MessageFormat arguments (i18next-icu) or MessageFormat 2 variables (i18next-mf2), so ICU and MF2 users no longer needparseInterpolation: falseand 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, defaulttrueis unchanged.{name}takesstring | number,{n, number},pluralandselectordinaltakenumber,{d, date}and{t, time}takeDate,selecttakes its case keys or any string (forother), and arguments nested in cases are required too.#and tags are not arguments, apostrophe quoting is read like intl-messageformat does.{$name}takesstring | number, a variable annotated with:number,:integer,:currency,:percentor:unit(inline or through.input/.local) takesnumber,:datetime,:dateand:timetakeDate,:stringtakesstring, option and markup variables (minimumFractionDigits=$digits,{#link href=$url}) are required too,.localvariables are not arguments, and|quoted literals|and\{escapes are skipped.interpolationFormatTypeMapoverrides a type by ICU format or MF2 function name, e.g.date: Date | numberor a customuppercase: string.t(),getFixedT(),i18n.t, key prefixes, namespaces, the selector API and react-i18next'sTransvalues. Messages need literal types, as the resources file from i18next-cli'stypescommand has them.false(TypeScript 6), 0.5s instead of 0.26s with TypeScript 7. Messages that no call uses cost nothing.Property 'name' is missing) instead of'{ nmae: string }' is not assignable to type 'string'from the default-value overload.v26.4.7Compare Source
cloneInstance()no longer inheritspreload.loadResources()adds everypreloadlanguage to each load, and clones copiedpreloadfrom the parent, so a clone'sinit()and everychangeLanguage()walked all preloaded languages × all namespaces throughqueueLoad()/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: itschangeLanguage()no longer waits for parent preload languages that are still loading (the parent's owninit()still does), andclone.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 passingpreloadtocloneInstance()keeps the old behavior. Thanks @theMostCuriousHomunculus for the detailed report (#2451).v26.4.6Compare Source
toResolveHierarchy()checked the rawfallbackLngvalue against the list, but the list holds formatted codes, so any fallback written differently from its formatted form slipped through:lng: 'de-CH'withfallbackLng: 'de-ch'gavelanguages['de-CH', 'de', 'de-CH'], and the same happened withfallbackLng: '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.5Compare Source
hasLoadedNamespace(ns, { lng })no longer staysfalseforever when the passed inlngis loaded under another code, or not at all. It checked the load state of the rawlng, butloadLanguages()stores a language under its resolved code (de-ch→de-CH,de-CH→dewithload: 'languageOnly') and skips alngoutsidesupportedLngsentirely (only the fallback gets loaded), so that state never got set. react-i18next'suseTranslation(ns, { lng })then never became ready: withuseSuspenseit 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 alngoutsidesupportedLngsonly waits for the fallback, which is also whatt()resolves to. Calls withoutlngand theprecheck(react-i18next'sbindI18n: 'languageChanging') are unchanged (react-i18next#1934).v26.4.4Compare Source
$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 hungt()for minutes. With the default config this was reachable through any interpolated value (t('greet', { name: '$t(aaaaaaaaaaaaaaaaaaaaaaaa' })): escaping does not touch$,tor(, and theskipOnVariablescheck runs the regexp on the interpolated result, sonest: falsedid 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.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 (defaultskipOnVariables: true) was resolved anyway by the outer loop: withouter: 'A $t(inner) B'andinner: 'Hi {{name}}',t('outer', { name: '$t(secret)' })rendered thesecrettranslation. It now staysA Hi $t(secret) B, the same as without the outer nesting; withskipOnVariables: falseit 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 throughString.replace, so$&,$`,$'and$$stay literal as before (26.4.2).v26.4.3Compare Source
ResourceStore.hasLanguageSomeTranslations()no longer enumerates every key of a large bundle on each call. It ranObject.keys()on the whole bundle just to learn whether it had at least one key, andinit()/changeLanguage()reach it three times per instance (once while picking the language, twice viasetResolvedLanguage()). With large flat bundles and one i18next instance per SSR request reusing the same resources, that showed up as measurable CPU work before anyt()call. Bundle objects already found non-empty are now remembered in aWeakSetshared 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 withdeleteon every key while it is still in the store keeps counting as having translations; useremoveResourceBundle()for that. Thanks @dmchoi77 for the detailed report (#2449).v26.4.2Compare Source
$&,$`,$'and$$inside a nested value ($t(key)) now stay literal.nest()handed the resolved value straight toString.replaceas 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$. Throught()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 andt()never returned — also under the defaultescapeValue: truewhen the value arrives via a variable forwarded through nesting options ($t(key, { "name": "{{name}}" })with a name containing$&). The value is now$-escaped at theString.replacecall, the same guardinterpolate()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.1Compare Source
keyPrefixoverload ofgetFixedT()is now available underenableSelector: 'strict'. Its constraint was gated ontrue | 'optimize'only, so under'strict'it collapsed tonever, the overload dropped out, and the returnedtsilently lost itskeyPrefixscope (t(($) => $.deep)failed withProperty 'deep' does not exist on type '{}'). The same call already typechecked undertrueand'optimize'. Thanks @hovelopin (#2446).v26.4.0Compare Source
toResolveHierarchyresults per(code, fallbackCode)pair. The hierarchy resolver runs on everyt()call and callsIntl.getCanonicalLocalesmultiple 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 whenoptions.fallbackLngchanges (reassignment or in-place array mutation); if you mutate other resolution-relevant options at runtime (load,lowerCaseLng,cleanCode,nonExplicitSupportedLngs), calli18next.services.languageUtils.clearCache()afterwards. Function-valuedfallbackLngand per-call array/objectfallbackLngoptions are never cached, so dynamic fallbacks keep working as before. Thanks @equaterina (#2444).@rollup/plugin-babelsupports 8, eslint on 9.x for neostandard). Removed the unusedcoverallspackage (CI uses the Coveralls GitHub Action) and replacedsinonwithnise+vitest.spyOnin the v1 compatibility tests, which resolves all opennpm auditfindings (0 vulnerabilities) and should close the dependabot alerts on the lockfile.v26.3.6Compare Source
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.5Compare Source
$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).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).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.4Compare Source
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 recursionConfiguration
📅 Schedule: (UTC)
🚦 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.
This PR was generated by Mend Renovate. View the repository job log.