This is an automated email from the ASF dual-hosted git repository.
rusackas pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/superset.git
The following commit(s) were added to refs/heads/master by this push:
new 76351ff12c4 fix(i18n): ensure language pack loads before React renders
(#36893)
76351ff12c4 is described below
commit 76351ff12c4dbeffba62aa327a20abb063705500
Author: Tu Shaokun <[email protected]>
AuthorDate: Tue Feb 10 16:29:04 2026 +0800
fix(i18n): ensure language pack loads before React renders (#36893)
---
.../tests/DatasourceEditor.test.tsx | 3 +
.../FiltersConfigModal/FiltersConfigModal.test.tsx | 2 +-
.../DatasetList/DatasetList.permissions.test.tsx | 1 +
superset-frontend/src/preamble.ts | 135 +++++++++++++--------
superset-frontend/src/views/index.tsx | 18 ++-
superset/views/base.py | 7 +-
6 files changed, 113 insertions(+), 53 deletions(-)
diff --git
a/superset-frontend/src/components/Datasource/components/DatasourceEditor/tests/DatasourceEditor.test.tsx
b/superset-frontend/src/components/Datasource/components/DatasourceEditor/tests/DatasourceEditor.test.tsx
index 5e243224f1a..a2da3a5544c 100644
---
a/superset-frontend/src/components/Datasource/components/DatasourceEditor/tests/DatasourceEditor.test.tsx
+++
b/superset-frontend/src/components/Datasource/components/DatasourceEditor/tests/DatasourceEditor.test.tsx
@@ -42,12 +42,15 @@ jest.mock('@superset-ui/core', () => ({
}));
beforeEach(() => {
+ jest.useRealTimers();
+ fetchMock.removeRoutes();
fetchMock.get(DATASOURCE_ENDPOINT, [], { name: DATASOURCE_ENDPOINT });
setupDatasourceEditorMocks();
jest.clearAllMocks();
});
afterEach(async () => {
+ jest.useRealTimers();
await cleanupAsyncOperations();
fetchMock.clearHistory().removeRoutes();
// Reset module mock since jest.fn() doesn't support mockRestore()
diff --git
a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigModal.test.tsx
b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigModal.test.tsx
index 961a558bb96..58dba03d08b 100644
---
a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigModal.test.tsx
+++
b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigModal.test.tsx
@@ -506,7 +506,7 @@ test('deletes a filter including dependencies', async () =>
{
const filterTabs = within(filterContainer).getAllByRole('tab');
const deleteIcon = filterTabs[1].querySelector('[data-icon="delete"]');
fireEvent.click(deleteIcon!);
- userEvent.click(screen.getByRole('button', { name: SAVE_REGEX }));
+ await userEvent.click(screen.getByRole('button', { name: SAVE_REGEX }));
await waitFor(() =>
expect(onSave).toHaveBeenCalledWith(
expect.objectContaining({
diff --git
a/superset-frontend/src/pages/DatasetList/DatasetList.permissions.test.tsx
b/superset-frontend/src/pages/DatasetList/DatasetList.permissions.test.tsx
index 6c4169413de..d69594fbdc0 100644
--- a/superset-frontend/src/pages/DatasetList/DatasetList.permissions.test.tsx
+++ b/superset-frontend/src/pages/DatasetList/DatasetList.permissions.test.tsx
@@ -35,6 +35,7 @@ import {
jest.setTimeout(30000);
beforeEach(() => {
+ jest.useRealTimers();
setupMocks();
jest.clearAllMocks();
});
diff --git a/superset-frontend/src/preamble.ts
b/superset-frontend/src/preamble.ts
index 4595e822ebd..97c3f5c2ade 100644
--- a/superset-frontend/src/preamble.ts
+++ b/superset-frontend/src/preamble.ts
@@ -17,7 +17,8 @@
* under the License.
*/
import { configure, LanguagePack } from '@apache-superset/core/ui';
-import { makeApi, initFeatureFlags, SupersetClient } from '@superset-ui/core';
+import { logging } from '@apache-superset/core';
+import { makeApi, initFeatureFlags } from '@superset-ui/core';
import { extendedDayjs as dayjs } from '@superset-ui/core/utils/dates';
import setupClient from './setup/setupClient';
import setupColors from './setup/setupColors';
@@ -25,6 +26,7 @@ import setupFormatters from './setup/setupFormatters';
import setupDashboardComponents from './setup/setupDashboardComponents';
import { User } from './types/bootstrapTypes';
import getBootstrapData, { applicationRoot } from './utils/getBootstrapData';
+import { makeUrl } from './utils/pathUtils';
import './hooks/useLocale';
// Import dayjs plugin types for global TypeScript support
@@ -37,62 +39,97 @@ import 'dayjs/plugin/duration';
import 'dayjs/plugin/updateLocale';
import 'dayjs/plugin/localizedFormat';
-configure();
+let initPromise: Promise<void> | null = null;
-// Grab initial bootstrap data
-const bootstrapData = getBootstrapData();
+const LANGUAGE_PACK_REQUEST_TIMEOUT_MS = 5000;
-setupFormatters(
- bootstrapData.common.d3_format,
- bootstrapData.common.d3_time_format,
-);
-
-// Setup SupersetClient early so we can fetch language pack
-setupClient({ appRoot: applicationRoot() });
-
-// Load language pack before anything else
-(async () => {
- const lang = bootstrapData.common.locale || 'en';
- if (lang !== 'en') {
- try {
- // Second call to configure to set the language pack
- const { json } = await SupersetClient.get({
- endpoint: `/superset/language_pack/${lang}/`,
- });
- configure({ languagePack: json as LanguagePack });
- dayjs.locale(lang);
- } catch (err) {
- console.warn(
- 'Failed to fetch language pack, falling back to default.',
- err,
- );
- configure();
- dayjs.locale('en');
- }
+export default function initPreamble(): Promise<void> {
+ if (initPromise) {
+ return initPromise;
}
- // Continue with rest of setup
- initFeatureFlags(bootstrapData.common.feature_flags);
+ initPromise = (async () => {
+ configure();
- setupColors(
- bootstrapData.common.extra_categorical_color_schemes,
- bootstrapData.common.extra_sequential_color_schemes,
- );
+ // Grab initial bootstrap data
+ const bootstrapData = getBootstrapData();
- setupDashboardComponents();
+ setupFormatters(
+ bootstrapData.common.d3_format,
+ bootstrapData.common.d3_time_format,
+ );
- const getMe = makeApi<void, User>({
- method: 'GET',
- endpoint: '/api/v1/me/',
- });
+ // Setup SupersetClient early so we can fetch language pack
+ setupClient({ appRoot: applicationRoot() });
- if (bootstrapData.user?.isActive) {
- document.addEventListener('visibilitychange', () => {
- if (document.visibilityState === 'visible') {
- getMe().catch(() => {
- // SupersetClient will redirect to login on 401
+ // Load language pack before rendering
+ // Use native fetch to avoid race condition with SupersetClient
initialization
+ const lang = bootstrapData.common.locale || 'en';
+ if (lang !== 'en') {
+ const abortController = new AbortController();
+ const timeoutId = window.setTimeout(() => {
+ abortController.abort();
+ }, LANGUAGE_PACK_REQUEST_TIMEOUT_MS);
+
+ try {
+ const languagePackUrl = makeUrl(`/superset/language_pack/${lang}/`);
+ const resp = await fetch(languagePackUrl, {
+ signal: abortController.signal,
});
+ if (!resp.ok) {
+ throw new Error(`Failed to fetch language pack: ${resp.status}`);
+ }
+ const json = await resp.json();
+ configure({ languagePack: json as LanguagePack });
+ dayjs.locale(lang);
+ } catch (err) {
+ logging.warn(
+ 'Failed to fetch language pack, falling back to default.',
+ err,
+ );
+ configure();
+ dayjs.locale('en');
+ } finally {
+ window.clearTimeout(timeoutId);
}
+ }
+
+ // Continue with rest of setup
+ initFeatureFlags(bootstrapData.common.feature_flags);
+
+ setupColors(
+ bootstrapData.common.extra_categorical_color_schemes,
+ bootstrapData.common.extra_sequential_color_schemes,
+ );
+
+ setupDashboardComponents();
+
+ const getMe = makeApi<void, User>({
+ method: 'GET',
+ endpoint: '/api/v1/me/',
});
- }
-})();
+
+ if (bootstrapData.user?.isActive) {
+ document.addEventListener('visibilitychange', () => {
+ if (document.visibilityState === 'visible') {
+ getMe().catch(() => {
+ // SupersetClient will redirect to login on 401
+ });
+ }
+ });
+ }
+ })().catch(err => {
+ // Allow retry by clearing the cached promise on failure
+ initPromise = null;
+ throw err;
+ });
+
+ return initPromise;
+}
+
+// This module is prepended to multiple webpack entrypoints (see
`webpack.config.js`).
+// Kick off initialization eagerly, while still allowing entrypoints to
`await` it
+// before rendering when needed (e.g. the login page).
+initPreamble().catch(err => {
+ logging.warn('Preamble initialization failed.', err);
+});
diff --git a/superset-frontend/src/views/index.tsx
b/superset-frontend/src/views/index.tsx
index 79784fbc9ba..c96528c9f79 100644
--- a/superset-frontend/src/views/index.tsx
+++ b/superset-frontend/src/views/index.tsx
@@ -19,6 +19,20 @@
import 'src/public-path';
import ReactDOM from 'react-dom';
-import App from './App';
+import { logging } from '@apache-superset/core';
+import initPreamble from 'src/preamble';
-ReactDOM.render(<App />, document.getElementById('app'));
+const appMountPoint = document.getElementById('app');
+
+if (appMountPoint) {
+ (async () => {
+ try {
+ await initPreamble();
+ } finally {
+ const { default: App } = await import(/* webpackMode: "eager" */
'./App');
+ ReactDOM.render(<App />, appMountPoint);
+ }
+ })().catch(err => {
+ logging.error('Unhandled error during app initialization', err);
+ });
+}
diff --git a/superset/views/base.py b/superset/views/base.py
index afca4d2c448..15b35d94374 100644
--- a/superset/views/base.py
+++ b/superset/views/base.py
@@ -476,7 +476,12 @@ def cached_common_bootstrap_data( # pylint:
disable=unused-argument
and bool(available_specs[GSheetsEngineSpec])
)
- language = locale.language if locale else "en"
+ if isinstance(locale, Locale):
+ language = locale.language
+ elif isinstance(locale, str):
+ language = locale
+ else:
+ language = app.config.get("BABEL_DEFAULT_LOCALE", "en")
auth_type = app.config["AUTH_TYPE"]
auth_user_registration = app.config["AUTH_USER_REGISTRATION"]
frontend_config["AUTH_USER_REGISTRATION"] = auth_user_registration