Skip to content

chore(deps): update i18next to 27.1.2 - #33

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

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

Conversation

@renovate

@renovate renovate Bot commented Nov 7, 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.7.4 → 27.1.2 age confidence

Release Notes

i18next/i18next (i18next)

v27.1.2

Compare Source

  • security: language codes and namespaces that come from requests no longer grow the memory of a server without end. The backend connector kept one entry per failed load, so that it does not read it again, and the language hierarchy and plural rule caches kept one entry per language code. With the default supportedLngs: false, every made-up language a detector found (?lng=xx1, ?lng=xx2, ...) or every made-up namespace requested from i18next-http-middleware's getResourcesHandler added about 180 bytes for good. Now only the most recent 1000 failed loads are kept (an older one is read again when it is needed), and both caches are emptied at 1000 codes, as the formatted language codes already were. Pairs with i18next-http-middleware 3.10.1 (GHSA-mjcw-gfh6-3cxm).

v27.1.1

Compare Source

  • perf: t() skips the interpolation and nesting regexp scans when the text has no interpolation or nesting prefix, about 6-14% faster for short texts and up to 75% for long ones. The output does not change. Thanks @​g4lb (#​2469)

v27.1.0

Compare Source

  • feat: when the plural form selected for count is missing, t() uses the _other form of the same language before the next fallback language, as CLDR does (lateral inheritance), instead of returning the key. Current CLDR data gives French, Spanish, Italian and Portuguese a many form for exact millions, so files with only _one and _other showed the key (or the fallback language) for count: 1000000. The plain key still comes first, so key + key_other keeps resolving key; with a context, key_<context>_other comes before key_other; ordinals use _ordinal_other; and defaultValue_other is used after defaultValue. A form filled in from _other is no longer reported as missing (saveMissing, missingKeyHandler, exists()). Thanks @​TenPotatoes (#​2468)

v27.0.1

Compare Source

  • fix: each cached format keeps at most 1000 formatters, dropping the oldest when full. This covers the built-in formats (number, currency, datetime, relativetime, list) with the default cacheInBuiltFormats: true, and formats added with addCached. The cache key holds all t() options, including the interpolated values, so every new amount, date or name added a formatter that was never released: about 50 MB per 100k values, growing without end in a long-running server. A dropped formatter is made again from the same language and options, so the output does not change. Thanks @​liustack (#​2467).

v27.0.0

Compare Source

This is a major breaking release. It changes defaults that most setups had to override, gives every instance its own logger and post-processors, refuses language codes that look like markup or paths, and removes options that were broken or had no effect. 26.7 warned in debug mode about most of the runtime changes. The migration guide has the details; to keep the v26 behavior where an option exists:

i18next.init({
  fallbackLng: 'dev', // or better your source language: there is no implicit 'dev' any more
  returnEmptyString: true, // an empty translation is a translation again
  cleanCode: false, // keep codes without a region as they are ('EN' stays 'EN')
});
declare module 'i18next' {
  interface CustomTypeOptions {
    strictInterpolationChecks: false;
    returnEmptyString: true;
  }
}
Changed defaults
  • fallbackLng defaults to false instead of ['dev']. Without a fallbackLng, i18next no longer requests dev from the backend (/locales/dev/translation.json with i18next-http-backend), and saveMissing sends missing keys for the current language instead of dev. With supportedLngs and no fallbackLng, an unsupported language leaves language and languages unset, as fallbackLng: false already did. Set fallbackLng to your main language, or to 'dev' for the previous behavior.
  • returnEmptyString defaults to false: an empty translation falls back like a missing value (to the fallback language, the default value or the key), as null already does. i18next-cli writes '' for untranslated keys in the other languages, which now fall back instead of rendering blank. An explicit '' default from code still counts: t('key', '') and t('key', { defaultValue: '' }) return ''. Set returnEmptyString: true (in init() and in CustomTypeOptions) if an empty translation is meant to render nothing, for example for a Locize project with "preserve empty strings" on.
  • Language codes without a region get their canonical casing, as codes with one already did: 'EN' becomes 'en'. Only the casing changes: aliases like tl, iw or eng stay as they are ('TL' becomes 'tl'), and so do codes that are not BCP 47 (en_US, my_custom). i18next 26.7.0 and 26.7.1 warned that v27 turns iw into he or tl into fil; that was wrong and 26.7.2 corrects it. iw, tl and the like stay as they are. Set cleanCode: false to keep codes without a region exactly as they are. cleanCode is deprecated.
  • types: strictInterpolationChecks defaults to true: t() needs the options for a string with variables, so a {{name}} cannot be left out of a call. Type a wrapper around t() with ParseKeysWithoutVariables, declare the variables interpolation.defaultVariables fills in with defaultVariables in CustomTypeOptions, or set strictInterpolationChecks: false to keep the v26 types (also for ICU and MF2 messages, whose arguments are required either way). The fixes below close the gap plural keys left.
  • types: returnEmptyString defaults to false in TypeOptions too, so a key whose value is '' is typed as the key.
Changed behavior
  • The logger and the post-processors belong to the instance. use() of a logger or a post-processor on one instance no longer changes the others, and the init() of one instance no longer rewires the logging of all of them. An instance from createInstance() does not see what the default i18next instance (or any other) registered: call use() on each instance. A clone (cloneInstance()) keeps its parent's logger and post-processors, also those its parent registers later, and what it registers itself stays its own. A plugin that calls use() from its own init() (the Locize InContext editor) registers right away, as before. services.logger stays the instance's logger, with forward(). In debug mode, a postProcess name that is not registered on the instance logs a warning.
  • An object or an array without returnObjects is handled like a missing key: t() returns the default value if one is passed, else the key (with appendNamespaceToMissingKey, parseMissingKeyHandler and the post-processors applied), instead of the sentence key '... (en)' returned an object instead of string., which ended up in the UI. It is not saved as missing, since the key exists. The debug warning stays, and returnedObjectHandler still decides what is returned when it is set (returnedObjectHandler: (key) => `key '${key}' returned an object instead of string.` brings a sentence back).
  • addResources() keeps every value: numbers, booleans and objects were dropped silently, only strings and arrays were added.
  • The default escape no longer escapes /: '$10/month' stays '$10/month' instead of '$10&#x2F;month', and dates in react-i18next's Trans no longer render as 17&#x2F;12&#x2F;2025 (discussion 2199). &, <, >, " and ' are still escaped, which covers HTML text and quoted attributes; a / cannot end either. Update snapshots that contain &#x2F;, or pass your own interpolation.escape to keep escaping it.
  • A language code from lng, changeLanguage(), a language detector or loadLanguages() that does not have the shape of isSafeLanguageCode() (letters, digits, -, _, @ and ., no .., at most 128 characters) is refused unless it is configured: listed in supportedLngs, in a fallbackLng that is not a function or in preload, or a language in resources, as given to init(). changeLanguage() continues as with an unsupported code: with the detector's next code, else the fallback language, else none (with fallbackLng: false, a code refused at init() leaves language unset); loadLanguages() skips it. With supportedLngs, the input is matched as before ('de-DE;q=0.9' still gives 'de'), and only an unsafe match is refused, which only nonExplicitSupportedLngs can return ('de-<x>' for a supported 'de'). Without supportedLngs, a detector that returned "><img src=x onerror=alert(1)> made that string i18next.language, which apps put into <html lang>, and backends were asked to load it. BCP 47, POSIX (en_US.UTF-8@euro, sr@latin) and private-use codes (en-x-pirate) pass; to use a code with other characters (+, spaces, /), list it in supportedLngs. In debug mode a refused code logs a warning.
  • createInstance(options) merges the defaults right away, without initializing: before init(), instance.options has the defaults, getFixedT() no longer throws (t() returns '' until init()), and debug: true already shows the warnings before init(). init() still tells the options you set from the defaults, so a keySeparator or nsSeparator passed to createInstance() counts as set by you (#​2426). loadNamespaces() before init() does nothing and calls back, as it did for an instance without options.
  • keySeparator: false means flat keys only: a lookup no longer splits a key at the text false (t('nestedfalsec') returned the value of nested.c), and a selector path is joined with . like a key prefix, so selectors work on the flat keys ($ => $['a.b'] and $ => $.a.b look up the flat key 'a.b'; $ => $.nested.c no longer resolves nested, then c). types: with keySeparator: false in CustomTypeOptions, the keys are the flat keys (no nestedfalsec keys), a key containing false is no longer split, and keyPrefix takes the parts of the flat keys before a ., which is how getFixedT() joins it ('nested' for 'nested.c'). A key prefix combined with selectors is not typed for flat keys.
Removed
  • The dotted language in the resource store calls: getResource('en.translation.key'), addResource('en.translation.key', value) and addResourceBundle('en.translation', resources). Pass language, namespace and key as separate arguments; addResource(), addResources() and addResourceBundle() throw a TypeError for the dotted form. A language code with a . (de_DE.UTF-8) is now a language like any other.
  • interpolation.prefixEscaped, suffixEscaped, nestingPrefixEscaped and nestingSuffixEscaped, which were inserted into the RegExp unescaped. Pass the literal delimiters as prefix, suffix, nestingPrefix and nestingSuffix.
  • types: compatibilityJSON (it had no effect since v24).
  • types: the option types of plugins. locizeLastUsed is declared by locize-lastused (5.1.0 and later). ReactOptions stays importable from 'i18next' but is empty: react-i18next fills it in (17.1.13 and later, and 18), so the react options are checked when react-i18next is in the program.
  • types: the _ and $ helpers in typescript/t.d.ts and typescript/options.d.ts ($FirstNamespace, _ValueOf and 84 more) are no longer importable; every declaration of those files counted as exported. The other types of those files (TOptionsBase, TFunctionSignature, ParseTReturn, ...) are now exported on purpose. If you imported one of the hidden helpers, please open an issue: exporting it again does not break anything.
  • The root index.js, a CommonJS shim that the exports map made unreachable.
Support
  • Node.js 22 or later (engines); CI tests 22, 24 and 26.
  • The build targets ES2020 (Chrome and Edge 80, Firefox 80, Safari 14, Opera 67, Samsung Internet 13) instead of browserslist defaults, which moved with every update of the browser data. The output is the same as 26.7's.
  • TypeScript ^5 || ^6 || ^7, unchanged.
Kept
  • Backends and async language detectors without callbackApi still work; i18next guesses their calling convention from the number of parameters and logs a deprecation warning in debug mode.
  • isInitialized, initializedStoreOnce and initializedLanguageOnce stay writable, so react-i18next 17's useSSR and next-i18next's Pages Router work with 27. Writing them is deprecated: call hydrate().
  • services.backendConnector.backend stays readable.
Type fixes for strictInterpolationChecks
  • fix(types): with strictInterpolationChecks, a plural key no longer switches the check off. Without count a plural key like item (item_one, item_other) has no value of its own, and the string it resolved to hid the variables of every other key in a union: (key: 'greeting' | 'item') => t(key) and (key: ParseKeys<'ns'>) => t(key) compiled as soon as the namespace had a plural key, while the same wrapper without one failed. Now such a call is checked against the plural forms, ordinal ones and 'ns:item' keys included: t('item') needs count when the forms use it, and a wrapper over a union with a plural key fails as it does without one. To migrate, pass count, or type the wrapper with ParseKeysWithoutVariables.
  • fix(types): with strictInterpolationChecks, a call with options needs the variables of a plural key's forms too: t('item', { defaultValue: 'x' }) and (key: 'plain' | 'item') => t(key, { defaultValue: 'x' }) need count when the forms use it, as t('item') does. Before, any options object turned the check off for a plural key without count. Without the flag the options types are unchanged.
  • fix(types): ParseKeysWithoutVariables leaves out a key whose ordinal forms take count, and keeps a key that has its own value next to its plural forms, since t() returns that value without count.
  • fix(types): t('ns:item') for a plural key without count returns string, as t('item') does, instead of never.

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,

[!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 changed the title chore(deps): update i18next to 27.0.0 chore(deps): update i18next to 27.0.1 Nov 7, 2026
@renovate
renovate Bot force-pushed the renovate/i18next-27.x branch 2 times, most recently from 72dc6e7 to bab3218 Compare November 9, 2026 23:12
@renovate renovate Bot changed the title chore(deps): update i18next to 27.0.1 chore(deps): update i18next to 27.1.0 Nov 9, 2026
@renovate
renovate Bot force-pushed the renovate/i18next-27.x branch from bab3218 to 6570485 Compare November 10, 2026 14:01
@renovate renovate Bot changed the title chore(deps): update i18next to 27.1.0 chore(deps): update i18next to 27.1.1 Nov 10, 2026
@renovate
renovate Bot force-pushed the renovate/i18next-27.x branch from 6570485 to cc06522 Compare November 15, 2026 07:29
@renovate renovate Bot changed the title chore(deps): update i18next to 27.1.1 chore(deps): update i18next to 27.1.2 Nov 15, 2026
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.

0 participants