This issue has been created
 
 
XWiki Platform / cid:jira-generated-image-avatar-3455dda9-b298-423b-91c7-efb317bcb90f XWIKI-24742 Open

The locale of a client-side translation query is ignored

 
View issue   ·   Add comment
 

Issue created

 
cid:jira-generated-image-avatar-4349fee1-9641-4c1e-828f-9b146638a7fa Manuel Leduc created this issue on 26/Aug/26 12:01
 
Summary: The locale of a client-side translation query is ignored
Issue Type: cid:jira-generated-image-avatar-3455dda9-b298-423b-91c7-efb317bcb90f Bug
Affects Versions: 18.3.0
Assignee: Unassigned
Components: Localization, Node - Translation
Created: 26/Aug/26 12:01
Priority: cid:jira-generated-image-static-major-4843d17e-c407-462f-a883-9d8006514c7b Major
Reporter: Manuel Leduc
Description:

Problem

The client-side Query type published by @xwiki/platform-localization-api has an optional
locale field:

type Query =
  | string[]
  | {
      prefix?: string | undefined;
      keys: string[];
      locale?: string;
    };

That field never has any effect. Translations are always resolved in the locale of the current page,
whatever the caller asks for, and a key already resolved in one locale is served from the cache in
every other locale.

Steps to reproduce

  1. On a page whose <html> element carries lang="en", resolve the key
    core.viewers.save through the standard resolver, asking for locale: "fr".
  2. Observe the request sent to /rest/wikis/<wiki>/localization/translations.
  3. Resolve the same key again, this time with locale: "en".

Actual behaviour

  1. The request carries locale=en — the page locale — not locale=fr, so the English value comes
    back.
  2. The second call sends no request at all and returns the value cached by the first one.

Concretely, a Vue component whose locale differs from the page locale never gets its own
translations: useI18nAdapter does watch(locale, (lang) => load(lang)), but the reload resolves
in the page locale again, so switching locale re-fetches the same strings.

Cause

Two independent defects on the same path.

1. The default resolver discards the query object. In
core/localization/localization-default/src/index.ts line 40, initialize flattens the query to
a plain array of fully-qualified keys before delegating:

let remainingTranslations = Array.isArray(query)
  ? query
  : query.keys.map((key) => (query.prefix ?? "") + key);
...
resolved = Object.assign(
  resolved,
  await translator.resolve(remainingTranslations),
);

The prefix survives, because it is concatenated into each key, but the locale is dropped: the string
locale does not appear anywhere in localization-default. The translator then takes its
Array.isArray(query) branch and falls back to
document.documentElement.getAttribute("lang") ?? "en", so the if (query.locale) branch at
localization-resolver-xwiki-rest/src/index.ts line 73 is unreachable through the standard
resolver.

2. The translator's cache has no locale dimension. In
core/localization/localization-resolvers/localization-resolver-xwiki-rest/src/index.ts, cache
(line 33) is a flat map from key to value, and the key filters at lines 55 and 63 skip any key
already present in it regardless of the locale being requested. So even a caller that reaches the
translator directly, with a locale, gets the value cached for a previous locale — and when every
requested key is cached, no request is sent at all.

Expected behaviour

  • A query's locale reaches the translator: initialize should forward the query rather than
    flatten it, or carry the locale alongside the remaining keys.
  • The cache should be keyed by locale, so the same key resolved in two locales results in two entries
    and two requests.

Notes

  • The existing unit tests cover the two halves separately — handles object query with a locale
    sends one fr query, caches resolved keys and skips fetch on second call repeats one locale
    — but never the same key across two locales, which is why neither defect is caught. A regression
    test should combine them.
  • A smaller problem in the same function: _resolve(cache) (line 115) hands the caller the shared
    mutable cache object itself rather than the translations for the query. initialize copies it
    through Object.assign, so the standard path is unaffected, but a direct consumer of the
    translator receives an object that keeps changing under it, and that holds keys it never asked for.