Uploaded image for project: 'XWiki Platform'
  1. XWiki Platform
  2. XWIKI-24742

The locale of a client-side translation query is ignored

    XMLWordPrintable

Details

    • Unknown
    • N/A
    • N/A

    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.

      Attachments

        Issue Links

          Activity

            People

              mleduc Manuel Leduc
              mleduc Manuel Leduc
              Votes:
              0 Vote for this issue
              Watchers:
              0 Start watching this issue

              Dates

                Created:
                Updated:
                Resolved: