Skip to content

fix(deps): update dependency i18next to v26 - autoclosed - #179

Closed
renovate[bot] wants to merge 1 commit into
mainfrom
renovate/i18next-26.x
Closed

renovate[bot] wants to merge 1 commit into
mainfrom
renovate/i18next-26.x

Conversation

@renovate

@renovate renovate Bot commented Mar 28, 2026 •

Copy link
Copy Markdown
Contributor

Note

This PR body was truncated due to platform limits.

This PR contains the following updates:

Package Change Age Confidence
i18next (source) ^25.6.3 → ^26.0.0 age confidence

Release Notes

i18next/i18next (i18next)

v26.7.2

Compare Source

  • fix: the debug-mode deprecation warning for a language code without a region no longer says that v27 replaces aliases. 26.7.0 and 26.7.1 warned that v27 turns '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'), so iw, tl, eng and 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.1

Compare Source

  • fix: 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 through reloadResources() with i18next 26.7, so their reloadInterval no longer recovered from such a failure, as their own reload did before.
  • fix: 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's reloadInterval never refreshed the languages loaded for requests. It never requests cimode. With languages given, nothing changes.

v26.7.0

Compare Source

The last 26.x minor before v27, which follows in about a week. It adds the APIs v27 builds on, and with debug: true it warns wherever a setup relies on something v27 changes. Most of the v27 defaults are existing options, so you can try them now:

i18next.init({
  debug: true, // prints the warnings below
  fallbackLng: 'en', // v27: fallbackLng defaults to false, set your source language (no implicit 'dev')
  returnEmptyString: false, // v27 default: an empty string falls back like a missing key
  cleanCode: true, // close to v27, which canonicalizes codes without a region ('EN' becomes 'en')
});
declare module 'i18next' {
  interface CustomTypeOptions {
    strictInterpolationChecks: true; // v27 default
    returnEmptyString: false; // v27 default
  }
}

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 *Escaped options removed) have no switch; the debug warnings below show where they apply.

  • feat: with the default 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 with debug: true, because the warning only ran with skipOnVariables: false. A variable passed as undefined and a missingInterpolationHandler still log nothing.
  • feat: 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's useSSR did by writing resourceStore.data, options.ns, isInitialized, initializedStoreOnce and initializedLanguageOnce directly, 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, as useSSR does: a clone shares its parent's store (one clone per request with i18next-http-middleware). Called before init(), it logs a warning and changes nothing.
  • feat: services.utils.reloadResources() lets a backend reload its bundles through i18next (as i18next.reloadResources(), with retries and the loaded event), instead of calling backendConnector.read() and loaded() itself as the reloadInterval of i18next-http-backend and i18next-chained-backend does. Without languages (no lng, no detector, the default fallbackLng), reloadResources() reloads nothing instead of throwing Cannot read properties of undefined (reading 'forEach'); a backend that also reloads preload passes its languages.
  • feat: backends and async language detectors can declare their calling convention with callbackApi (an instance or static property). true: read(language, namespace, callback), create(languages, namespace, key, fallbackValue, callback, options) and detect(callback); false: they return a promise or a value, and create gets 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's create(..., clb = () => {}, opts = {}) has length 4 and never got its options (isUpdate), and i18next-http-backend's callback form of create has 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 per init().
  • feat: 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, so en_US, zh_Hant_TW, sr@latin and de_DE.UTF-8 pass; markup, slashes and spaces do not. The language detector, i18next-http-middleware and i18next-http-backend each have their own rule today.
  • feat: use() logs a warning in debug mode when a second backend, language detector, i18nFormat or formatter replaces the first one; only one of each is used per instance, and losing an ICU setup this way was silent. As use() usually runs before init(), the warning is printed once init() has set up the logger.
  • feat(types): ParseKeysWithoutVariables<Ns, KPrefix>, the keys t() takes without options: their string has no variables, and a plural key is left out when its forms take count. A wrapper typed with it, like (key: ParseKeysWithoutVariables<'common'>) => t(key), compiles with strictInterpolationChecks without a cast.
  • feat(types): defaultVariables in CustomTypeOptions declares the variables interpolation.defaultVariables fills in at runtime (defaultVariables: { appName: string }). They become optional in the t() options, also with strictInterpolationChecks; passed, they keep their type.
  • feat(types): interpolationFormatSeparator in CustomTypeOptions for a custom interpolation.formatSeparator: the types split {{price, currency}} at a hard-coded ,, so with formatSeparator: ';' the format was not recognized. The per-call skipInterpolation option is typed, and returnedObjectHandler may return a value (what it returns is what t() returns; it was typed as returning void).
  • feat(types): SelectorFn, ApplyTarget, ConstrainTarget and GetSource are exported on purpose, under their current names. react-i18next imports them for Trans; they were importable only because every declaration in the type files counts as exported.
  • fix(types): with nsSeparator: false in CustomTypeOptions, the key union no longer contains nsfalsekey-style keys for the other namespaces, and a key that contains the text false (isfalsey) is no longer split at it, which typed its t() result as never.
  • fix: with clones (i18next-http-middleware makes one per request), saveMissing now checks that the namespace is loaded for the instance that misses the key. Each cloneInstance() pointed the parent's services.utils.hasLoadedNamespace at 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's services.utils answered for the newest clone. A clone's services.utils is now bound to the clone, and the parent keeps its own. A direct services.backendConnector.saveMissing() call, like the missing keys route of i18next-http-middleware, still checks the newest clone (follow-up to #​1413).
  • fix: dir() without a language (before init(), or with no language resolved) returns 'ltr' instead of 'rtl' (discussion #​2135).
  • fix: t() before init() returns an empty string, as typed (with a warning when debug logging is already on); it returned undefined.
  • fix: off('a b', listener) removes the listener from both events, as on('a b', listener) adds it to both. It looked for an event named 'a b', so once('a b', listener) never removed its listener and ran it on every event.
  • deprecate: dotted languages in the resource store calls (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.
  • deprecate: interpolation.prefixEscaped, suffixEscaped, nestingPrefixEscaped and nestingSuffixEscaped (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 as prefix, suffix, nestingPrefix and nestingSuffix.
  • deprecate: compatibilityJSON is marked @deprecated (it has no effect since v24) and removed from the types in v27.
  • deprecate: a language code without a region that v27 canonicalizes differently logs a deprecation warning in debug mode: 'EN' stays 'EN' today and becomes 'en' in v27, 'iw' becomes 'he'. cleanCode: true already lower-cases them.
  • deprecate: when fallbackLng is not set, the first request for the default fallback language dev (loading it, or saving missing keys to it) logs a notice in debug mode: v27 defaults to fallbackLng: false and no longer requests dev. Set fallbackLng to your source language, or to 'dev' to keep the current behavior.
  • deprecate: in debug mode, a language code that comes from lng, changeLanguage() or the language detector, is not configured (supportedLngs, fallbackLng, preload or the resources) and fails isSafeLanguageCode logs a deprecation warning, once per code: without supportedLngs, a detector that returns "><img src=x onerror=alert(1)> makes that string i18next.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.
  • deprecate: the logger and post-processors are shared by all instances in v26 and per instance from v27 (clones keep their parent's). In debug mode, an instance that runs a post-processor only another instance registered with use() logs a deprecation warning (call use() on this instance too), and a non-clone init() that replaces the logger another instance set with use() logs one through that logger.
  • deprecate: in debug mode, a value with a / under the default escape logs that v27 no longer escapes / ($10/month renders as $10&#x2F;month today; not with escapeValue: false, where react-i18next's Trans escapes one call itself and decodes the entities afterwards), and an object or array hit without returnObjects logs that v27 returns the key instead of the sentence key '... (en)' returned an object instead of string..
  • chore: the package is published from a files whitelist instead of .npmignore, which had to mirror every local or secret file by hand. The tarball is unchanged. The root i18next.js and i18next.min.js (copies of dist/umd/, still published for the unversioned unpkg URLs) are built by npm run build and no longer tracked in git.
  • ci: the public types are checked on TypeScript 5.0, the lowest version of the peer range.

v26.6.0

Compare Source

  • feat(types): new strictInterpolationChecks type option (default false). With strictInterpolationChecks: true in CustomTypeOptions, t() needs its options for a string with {{variables}}: for 'Hello {{name}}!', t('greeting') is a type error and t('greeting', { name }) compiles. Left out, the variable reaches the UI as the raw {{name}} placeholder. This applies to string keys, selectors, selector arrays and getFixedT, and with strictKeyChecks also to t(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 generic K 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).
  • fix(types): an array of selectors with a plural member now requires count, as a single selector does since v25.9.0. Without count, i18next skips the plural member at runtime, so t([($) => $.item, ($) => $.plain]) returned the plain value while typed as the plural forms. An array of selectors with defaultValue now returns the literal values and the default value ('a' | 'b' | 'x') instead of string. Thanks @​Alex0007 (#​2465).

v26.5.12

Compare Source

  • fix: with keySeparator: false or a custom separator like '::', a nested interpolation value that is 0, false or '' is now inserted. 'You have {{stats.unread}} unread messages' with { stats: { unread: 0 } } kept the raw {{stats.unread}} placeholder (or became '' with skipOnVariables: false), also for formatted values like {{order.total, currency(USD)}} and for nested defaultVariables: a falsy value was looked up again with the configured separator, which does not split stats.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 nested 0, false or '' now wins, as a truthy one already did; a nested null still falls back to the flat key. Thanks @​nk-batman21 (#​2464).
  • fix: cloneInstance({ forkResourceStore: true }) no longer changes the options object it is given. It deleted forkResourceStore from 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 a TypeError. Clones without forkResourceStore are unchanged. Thanks @​edsonsims (#​2463).
  • perf(types): a selector t(($) => ...) (enableSelector: true / 'strict') no longer filters the whole namespace, and no longer does it again for every distinct context value. 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 as FilterKeysObject<{ ...source keys }, Context> instead of the filtered object, an object whose values are all null or undefined ({ x: null }) is kept as {} instead of being dropped, and a null key next to its plural forms (foo: null, foo_one, foo_other) is typed as the plural value instead of never. Thanks @​GabrielVooDB (#​2462).

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: with keySeparator: false or a custom separator like '::', a nested interpolation value that is 0, false or '' is now inserted. 'You have {{stats.unread}} unread messages' with { stats: { unread: 0 } } kept the raw {{stats.unread}} placeholder (or became '' with skipOnVariables: false), also for formatted values like {{order.total, currency(USD)}} and for nested defaultVariables: a falsy value was looked up again with the configured separator, which does not split stats.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 nested 0, false or '' now wins, as a truthy one already did; a nested null still falls back to the flat key. Thanks @​nk-batman21 (#​2464).
  • fix: cloneInstance({ forkResourceStore: true }) no longer changes the options object it is given. It deleted forkResourceStore from 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 a TypeError. Clones without forkResourceStore are unchanged. Thanks @​edsonsims (#​2463).
  • perf(types): a selector t(($) => ...) (enableSelector: true / 'strict') no longer filters the whole namespace, and no longer does it again for every distinct context value. 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 as FilterKeysObject<{ ...source keys }, Context> instead of the filtered object, an object whose values are all null or undefined ({ x: null }) is kept as {} instead of being dropped, and a null key next to its plural forms (foo: null, foo_one, foo_other) is typed as the plural value instead of never. Thanks @​GabrielVooDB (#​2462).

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

[!IMPORTANT]
✂ PR body was truncated to here.


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 was generated by Mend Renovate. View the repository job log.

@renovate renovate Bot assigned sarod Mar 28, 2026
@renovate
renovate Bot force-pushed the renovate/i18next-26.x branch 3 times, most recently from 642aaf4 to 811b648 Compare March 31, 2026 15:07
@renovate
renovate Bot force-pushed the renovate/i18next-26.x branch 3 times, most recently from 99626ea to a23db07 Compare April 15, 2026 13:02
@renovate
renovate Bot force-pushed the renovate/i18next-26.x branch 4 times, most recently from d0cb913 to ce6699b Compare April 24, 2026 22:25
@renovate
renovate Bot force-pushed the renovate/i18next-26.x branch from ce6699b to f85f4aa Compare April 29, 2026 11:06
@renovate
renovate Bot force-pushed the renovate/i18next-26.x branch 5 times, most recently from d61234e to 459190d Compare May 12, 2026 15:01
@renovate
renovate Bot force-pushed the renovate/i18next-26.x branch 3 times, most recently from 57be8d1 to 08b9263 Compare May 18, 2026 15:07
@renovate
renovate Bot force-pushed the renovate/i18next-26.x branch 2 times, most recently from 6f6bbe2 to 3014d0d Compare May 28, 2026 15:04
@renovate
renovate Bot force-pushed the renovate/i18next-26.x branch from 3014d0d to eb35261 Compare June 11, 2026 21:12
@renovate
renovate Bot force-pushed the renovate/i18next-26.x branch 2 times, most recently from 70af6f8 to 324f292 Compare June 30, 2026 09:57
@renovate
renovate Bot force-pushed the renovate/i18next-26.x branch 3 times, most recently from bd7043b to e094ee6 Compare July 9, 2026 17:40
@renovate
renovate Bot force-pushed the renovate/i18next-26.x branch from e094ee6 to 7d83180 Compare July 12, 2026 12:30
@renovate
renovate Bot force-pushed the renovate/i18next-26.x branch from 7d83180 to 8878e1b Compare August 22, 2026 04:04
@renovate
renovate Bot force-pushed the renovate/i18next-26.x branch 3 times, most recently from bda5771 to 00aa45e Compare August 26, 2026 17:15
@renovate
renovate Bot force-pushed the renovate/i18next-26.x branch 2 times, most recently from 833a9d0 to 089d06e Compare September 7, 2026 18:11
@renovate
renovate Bot force-pushed the renovate/i18next-26.x branch 4 times, most recently from 580d4b5 to 76e9de3 Compare October 17, 2026 21:24
@renovate
renovate Bot force-pushed the renovate/i18next-26.x branch 8 times, most recently from 8cd4f00 to dbbbe8f Compare October 26, 2026 19:02
@renovate
renovate Bot force-pushed the renovate/i18next-26.x branch 2 times, most recently from 021912f to 2d894f9 Compare November 2, 2026 01:56
@renovate
renovate Bot force-pushed the renovate/i18next-26.x branch from 2d894f9 to da664b9 Compare November 5, 2026 17:42
@renovate renovate Bot changed the title fix(deps): update dependency i18next to v26 fix(deps): update dependency i18next to v26 - autoclosed Nov 7, 2026
@renovate renovate Bot closed this Nov 7, 2026
@renovate
renovate Bot deleted the renovate/i18next-26.x branch November 7, 2026 09:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant