feat(i18n): serve language packs as versioned, immutable-cacheable scripts#41780
Draft
rusackas wants to merge 8 commits into
Draft
feat(i18n): serve language packs as versioned, immutable-cacheable scripts#41780rusackas wants to merge 8 commits into
rusackas wants to merge 8 commits into
Conversation
…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>
✅ Deploy Preview for superset-docs-preview ready!
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.htmlemits<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).window.__SUPERSET_LANGUAGE_PACK__ = {…};withCache-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 toSEND_FILE_MAX_AGE_DEFAULT.)no-cacheso it can't pin under the wrong address.COMMON_BOOTSTRAP_OVERRIDES_FUNCstill ride the bootstrap payload and suppress the script tag;spa.htmlstashes them onwindowinstead, 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./language_pack/<lang>/fetch endpoint's@has_accesswould 302 for embedded anonymous users, which the script tag avoids).EmbeddedViewpasses 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
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 carriesimmutable, (3) a hard reload serves it from browser cache, (4) translations render in code-split chunks on first paint.ADDITIONAL INFORMATION
/language_pack/<lang>/<version>/script.jsroute)🤖 Generated with Claude Code