Compare commits

...

5 Commits

Author SHA1 Message Date
Elizabeth Thompson
83bf447a3f Delete .github/pr-screenshots/menu-translated.png 2026-06-24 11:55:59 -07:00
Elizabeth Thompson
f1dd91f0b1 Delete .github/pr-screenshots/embedded-translated.png 2026-06-24 11:55:45 -07:00
Elizabeth Thompson
91f986467c fix(i18n): defer module evaluation for plugins and Menu until language pack is ready
Static imports of setupPlugins and Menu were evaluated synchronously at bundle
parse time, before initPreamble() resolved. This meant module-level t() calls
in plugin control panels (e.g. NVD3Controls, plugin-chart-table consts,
Separator) and menu data (commonMenuData) still ran before translations loaded.

Switch to dynamic imports with webpackMode: "eager" so modules stay in the
same bundle chunk but their evaluation is deferred until after initPreamble()
completes, matching the pattern already used in views/index.tsx.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-17 16:19:34 +00:00
Elizabeth Thompson
ad1c976305 chore: add E2E verification screenshots for i18n race condition fix
Screenshots from Playwright tests verifying the fix in menu.tsx and
embedded/index.tsx. Both captured with a 2-second artificial delay on
the language pack endpoint; all UI strings appear in German.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-06 00:24:09 +00:00
Elizabeth Thompson
efeb981626 fix(i18n): defer plugin init and menu render until language pack is ready
In embedded/index.tsx, setupPlugins() was called at module top level,
causing it to run synchronously before initPreamble()'s async language
pack fetch could resolve. All chart plugin control panel t() calls were
therefore cached in English. This fix chains plugin setup off the
initPreamble() promise so the language pack is always configured first.

In menu.tsx (used for backend-rendered Flask views), createRoot().render()
was called synchronously, allowing React to render Menu components before
the language pack resolved. This fix defers render until initPreamble()
settles.

The spa entry point (views/index.tsx) already handled this correctly via
a dynamic import after awaiting initPreamble().

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-06 00:24:09 +00:00
2 changed files with 82 additions and 53 deletions

View File

@@ -32,8 +32,8 @@ import {
} from '@apache-superset/core/theme';
import Switchboard from '@superset-ui/switchboard';
import getBootstrapData, { applicationRoot } from 'src/utils/getBootstrapData';
import initPreamble from 'src/preamble';
import setupClient from 'src/setup/setupClient';
import setupPlugins from 'src/setup/setupPlugins';
import { useUiConfig } from 'src/components/UiConfigContext';
import { store, USER_LOADED } from 'src/views/store';
import { Loading } from '@superset-ui/core/components';
@@ -41,7 +41,6 @@ import { ErrorBoundary } from 'src/components';
import { addDangerToast } from 'src/components/MessageToasts/actions';
import ToastContainer from 'src/components/MessageToasts/ToastContainer';
import { UserWithPermissionsAndRoles } from 'src/types/bootstrapTypes';
import setupCodeOverrides from 'src/setup/setupCodeOverrides';
import {
EmbeddedContextProviders,
getThemeController,
@@ -50,8 +49,27 @@ import { embeddedApi } from './api';
import { getDataMaskChangeTrigger } from './utils';
import { validateMessageEvent } from './originValidation';
setupPlugins();
setupCodeOverrides({ embedded: true });
// Defer plugin setup until after the language pack loads to prevent t() calls in
// plugin control panel configs from being cached in English before translations are ready.
// Dynamic imports (webpackMode: "eager") keep modules in the same bundle chunk but defer
// their evaluation until after initPreamble() resolves, so module-level t() calls in plugin
// control panels and setup code run only after translations are available.
const pluginsReady = initPreamble()
.catch(err => {
logging.warn(
'Preamble initialization failed, loading plugins without translations.',
err,
);
})
.then(async () => {
const [{ default: setupPlugins }, { default: setupCodeOverrides }] =
await Promise.all([
import(/* webpackMode: "eager" */ 'src/setup/setupPlugins'),
import(/* webpackMode: "eager" */ 'src/setup/setupCodeOverrides'),
]);
setupPlugins();
setupCodeOverrides({ embedded: true });
});
const debugMode = process.env.WEBPACK_MODE === 'development';
const bootstrapData = getBootstrapData();
@@ -172,32 +190,34 @@ function start() {
method: 'GET',
endpoint: '/api/v1/me/roles/',
});
return getMeWithRole().then(
({ result }) => {
// fill in some missing bootstrap data
// (because at pageload, we don't have any auth yet)
// this allows the frontend's permissions checks to work.
bootstrapData.user = result;
store.dispatch({
type: USER_LOADED,
user: result,
});
if (!root) {
root = createRoot(appMountPoint);
}
root.render(<EmbeddedApp />);
},
err => {
// something is most likely wrong with the guest token; reset the guard
// so a rehandshake with a valid token can retry.
logging.error(err);
showFailureMessage(
t(
'Something went wrong with embedded authentication. Check the dev console for details.',
),
);
started = false;
},
return pluginsReady.then(() =>
getMeWithRole().then(
({ result }) => {
// fill in some missing bootstrap data
// (because at pageload, we don't have any auth yet)
// this allows the frontend's permissions checks to work.
bootstrapData.user = result;
store.dispatch({
type: USER_LOADED,
user: result,
});
if (!root) {
root = createRoot(appMountPoint);
}
root.render(<EmbeddedApp />);
},
err => {
// something is most likely wrong with the guest token; reset the guard
// so a rehandshake with a valid token can retry.
logging.error(err);
showFailureMessage(
t(
'Something went wrong with embedded authentication. Check the dev console for details.',
),
);
started = false;
},
),
);
}

View File

@@ -29,8 +29,8 @@ import { ReactRouter5Adapter } from 'use-query-params/adapters/react-router-5';
import createCache from '@emotion/cache';
import { ThemeProvider } from '@apache-superset/core/theme';
import { theme } from '@apache-superset/core/theme';
import Menu from 'src/features/home/Menu';
import getBootstrapData from 'src/utils/getBootstrapData';
import initPreamble from 'src/preamble';
import { setupStore } from './store';
import querystring from 'query-string';
@@ -44,28 +44,37 @@ const emotionCache = createCache({
key: 'menu',
});
const app = (
<CacheProvider value={emotionCache}>
<ThemeProvider theme={theme}>
<Provider store={store}>
<BrowserRouter>
<QueryParamProvider
adapter={ReactRouter5Adapter}
options={{
searchStringToObject: querystring.parse,
objectToSearchString: (object: Record<string, any>) =>
querystring.stringify(object, { encode: false }),
}}
>
<Menu data={menu} />
</QueryParamProvider>
</BrowserRouter>
</Provider>
</ThemeProvider>
</CacheProvider>
);
const menuMountPoint = document.getElementById('app-menu');
if (menuMountPoint) {
createRoot(menuMountPoint).render(app);
initPreamble()
.catch(() => {}) // preamble logs failures internally; always render the menu
.then(async () => {
// Defer Menu import until after initPreamble() resolves so that module-level
// t() calls in Menu's dependency tree (e.g. commonMenuData.ts) run only after
// translations are loaded. webpackMode: "eager" keeps the module in this bundle
// chunk but defers evaluation until the import() expression is awaited.
const { default: Menu } = await import(
/* webpackMode: "eager" */ 'src/features/home/Menu'
);
createRoot(menuMountPoint).render(
<CacheProvider value={emotionCache}>
<ThemeProvider theme={theme}>
<Provider store={store}>
<BrowserRouter>
<QueryParamProvider
adapter={ReactRouter5Adapter}
options={{
searchStringToObject: querystring.parse,
objectToSearchString: (object: Record<string, any>) =>
querystring.stringify(object, { encode: false }),
}}
>
<Menu data={menu} />
</QueryParamProvider>
</BrowserRouter>
</Provider>
</ThemeProvider>
</CacheProvider>,
);
});
}