fix(i18n): ensure language pack loads before React renders (#36893)

This commit is contained in:
Tu Shaokun
2026-02-10 16:29:04 +08:00
committed by GitHub
parent f6f96ecc49
commit 76351ff12c
6 changed files with 115 additions and 55 deletions

View File

@@ -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()

View File

@@ -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({

View File

@@ -35,6 +35,7 @@ import {
jest.setTimeout(30000);
beforeEach(() => {
jest.useRealTimers();
setupMocks();
jest.clearAllMocks();
});

View File

@@ -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() });
// 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;
});
if (bootstrapData.user?.isActive) {
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'visible') {
getMe().catch(() => {
// SupersetClient will redirect to login on 401
});
}
});
}
})();
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);
});

View File

@@ -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);
});
}

View File

@@ -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