Skip to content

feat(i18n): serve language packs as versioned, immutable-cacheable scripts#41780

Draft
rusackas wants to merge 8 commits into
masterfrom
feat/i18n-versioned-language-pack
Draft

feat(i18n): serve language packs as versioned, immutable-cacheable scripts#41780
rusackas wants to merge 8 commits into
masterfrom
feat/i18n-versioned-language-pack

Conversation

@rusackas

@rusackas rusackas commented Jul 5, 2026

Copy link
Copy Markdown
Member

SUMMARY

Stacked on #39357 — do not merge before it. The first two commits here are that PR's branch (its diff will disappear from this one once it merges and this is rebased). Marked draft until then.

#39357 fixes the code-split translation race (#35330) by inlining the Jed language pack into the bootstrap HTML. That's correct but ships ~237KB raw / ~67KB gzipped of pack in every non-English full-page response, since HTML isn't cacheable. This follow-up keeps the synchronous-configuration guarantee while making the pack a first-class cacheable asset:

  • spa.html emits <script src="/language_pack/<lang>/<version>/script.js"> before the entry bundle, where <version> is a short content hash of the pack file (cached in-process alongside the pack itself).
  • The new endpoint serves window.__SUPERSET_LANGUAGE_PACK__ = {…}; with Cache-Control: public, max-age=31536000, immutable — browsers fetch the pack once per translation change instead of once per page load, and cache-bust automatically on upgrade. (The pre-existing unversioned /language_pack/<lang>/ fetch could serve packs up to a year stale after an upgrade, thanks to SEND_FILE_MAX_AGE_DEFAULT.)
  • A stale version in a cached HTML page still gets current content, served no-cache so it can't pin under the wrong address.
  • English emits no tag and pays nothing (unchanged).
  • Packs supplied via COMMON_BOOTSTRAP_OVERRIDES_FUNC still ride the bootstrap payload and suppress the script tag; spa.html stashes them on window instead, so the historical [6.0.0rc1] Partial Translations on pt_BR #35330 workaround keeps working and takes precedence.
  • preamble.ts's fallback chain is unchanged (bootstrap pack → window global → async fetch), so a failed script load degrades gracefully.
  • The endpoint is deliberately unauthenticated: translation catalogs are static public content shipped in this repo, contain no user or tenant data, and must load for anonymous principals (login page, embedded dashboards — note the sibling /language_pack/<lang>/ fetch endpoint's @has_access would 302 for embedded anonymous users, which the script tag avoids).
  • Embedded gets the same treatment: EmbeddedView passes the same template context.

BEFORE/AFTER SCREENSHOTS OR ANIMATED GIF

N/A — no visual changes. Behavior change is in caching headers and HTML size.

TESTING INSTRUCTIONS

pytest tests/unit_tests/translations/utils_test.py \
  tests/unit_tests/views/test_language_pack_script.py \
  tests/unit_tests/views/test_bootstrap_auth.py \
  tests/unit_tests/views/test_base.py
cd superset-frontend && npx jest src/preamble.test.ts

Manual: set a non-English locale, load any page, and confirm (1) the <script src="/language_pack/…/script.js"> tag precedes the entry bundle, (2) the response carries immutable, (3) a hard reload serves it from browser cache, (4) translations render in code-split chunks on first paint.

ADDITIONAL INFORMATION

🤖 Generated with Claude Code

alex-poor and others added 7 commits June 18, 2026 22:44
…t chunks

Module-level `const X = t('...')` calls in code-split chunks evaluate
before the async `/superset/language_pack/<lang>/` fetch in preamble.ts
can resolve, capturing the English msgid forever. #36893 fixed the
initial-render race by awaiting initPreamble before ReactDOM.render,
but chunks like DashboardContainer (ChartContextMenu ->
useDrillDetailMenuItems) still evaluate later and hit an unconfigured
translator. Result: strings inside component render bodies translate,
but strings captured at module scope stay English, even though
bootstrapData.common.locale is set and the language pack endpoint
returns a valid pack. Reported for pt_BR, ru, zh and others in #35330.

Fix: inject the full Jed language pack into the HTML bootstrap payload
under `common.language_pack`. An inline <script> in spa.html — which
runs before the entry bundle loads — stashes the pack on
`window.__SUPERSET_LANGUAGE_PACK__`. TranslatorSingleton lazily
self-configures from that global on first `getInstance()`, so every
chunk-local copy webpack splitChunks may produce ends up configured
identically. preamble.ts now prefers the bootstrap-injected pack and
keeps the async fetch as a fallback for entries that don't extend
spa.html (e.g. embedded).

Net effect: `t()` returns the translated msgstr on first evaluation
everywhere, including module-level constants in lazy chunks.

Fixes #35330
…emoize

Address review feedback on the bootstrap language-pack injection:

* Drop the duplicate `_load_language_pack` reader in views/base.py and
  reuse `superset.translations.utils.get_language_pack`, which already
  has a process-level dict cache keyed by locale and is the existing
  loader used by the async `/superset/language_pack/<lang>/` endpoint.

* Move the pack injection out of `cached_common_bootstrap_data` (which
  is memoized per `(user_id, locale)`) and into `common_bootstrap_payload`
  so the cached entry stays small and the pack itself is shared across
  users for the same locale instead of being duplicated in cache.

* Treat an empty/falsy pack as "no pack" so locales that miss the JSON
  file (the shared utility falls back to an empty English pack) hand
  the frontend `null` and the existing async-fetch fallback fires,
  matching prior behavior.

* Replace the `_load_language_pack` unit tests (function removed) with
  tests against `common_bootstrap_payload`. The new tests don't depend
  on the per-user memoize, removing a flakiness path where a prior test
  populating `(user_id=1, locale=None)` would skip the under-test code.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…payload

`common_bootstrap_payload` now copies the cached dict and adds a
`language_pack` key so the pack isn't duplicated inside the per-user
memoize. Update the existing locale-stringification test to assert on
the relevant fields rather than the full dict, since the wrapper
intentionally returns a superset of the cached payload.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
get_language_pack returns the empty English Jed pack on a miss (never
falsy), so the trailing 'or None' was dead code. It also clobbered a
language_pack already set via COMMON_BOOTSTRAP_OVERRIDES_FUNC (the
documented #35330 workaround). Prefer an existing pack, then fall back to
the shared loader.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Resolves the preamble.ts conflict: master renamed the language pack
endpoint from /superset/language_pack/<lang>/ to /language_pack/<lang>/
(route move); keep the bootstrap-pack-first structure from this branch
and adopt the new URL in the async-fetch fallback.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ripts

Follow-up to #39357, which inlined the Jed language pack into the
bootstrap HTML to fix the code-split translation race (#35330). That
made non-English full-page loads carry ~67KB (gzipped) of pack on every
request since HTML is uncacheable.

This keeps the synchronous-configuration guarantee but moves the pack
to a content-addressed script: spa.html emits
<script src="/language_pack/<lang>/<version>/script.js"> before the
entry bundle, where <version> is a short content hash of the pack file.
The endpoint serves window.__SUPERSET_LANGUAGE_PACK__ = {...} with
Cache-Control: public, max-age=31536000, immutable, so browsers fetch
the pack once per translation change instead of once per page load, and
cache-bust automatically on upgrade (fixing the stale-pack window the
unversioned /language_pack/<lang>/ fetch had under the 1-year
SEND_FILE_MAX_AGE_DEFAULT).

Details:
- English emits no tag and pays nothing.
- A stale version in a cached HTML page still gets current content,
  served no-cache so it can't pin under the wrong address.
- Packs provided via COMMON_BOOTSTRAP_OVERRIDES_FUNC still ride the
  bootstrap payload and suppress the script tag (spa.html stashes them
  on window instead), preserving the historical workaround.
- The endpoint is deliberately unauthenticated: catalogs are static
  public repo content with no user data, and must load for anonymous
  principals (login page, embedded).
- preamble.ts fallback chain unchanged: bootstrap pack, window global,
  then async fetch if the script failed to load.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@github-actions github-actions Bot added i18n Namespace | Anything related to localization api Related to the REST API packages labels Jul 5, 2026
@netlify

netlify Bot commented Jul 5, 2026

Copy link
Copy Markdown

Deploy Preview for superset-docs-preview ready!

Name Link
🔨 Latest commit 2a581fc
🔍 Latest deploy log https://app.netlify.com/projects/superset-docs-preview/deploys/6a4ad0dcf693c30008854400
😎 Deploy Preview https://deploy-preview-41780--superset-docs-preview.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

…antics

Fixes the two CI failures (generator fixture annotated Iterator[None],
stray unused import in the test package init) and adds tests encoding
the delivery contract:

- English configures the translator bare and never loads a pack (zero
  payload, no fetch).
- Non-English configures synchronously from the window global set by
  the versioned classic script tag, with no fetch and no English flash.
- An operator-supplied bootstrap pack takes precedence over the window
  global.
- The fallback fetch requests only the selected locale and resolves
  before initPreamble() returns, so even that path renders translated.
- The endpoint resolves exactly the locale in the URL and nothing else.
- Static guard: spa.html's pack script tag precedes the entry bundle
  and carries no async/defer, preserving execution order.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

api Related to the REST API i18n Namespace | Anything related to localization packages size/XL

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants