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

Reply via email to