mirror of
https://github.com/apache/superset.git
synced 2026-04-07 10:31:50 +00:00
fix(i18n): ensure language pack loads before React renders (#36893)
This commit is contained in:
@@ -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()
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -35,6 +35,7 @@ import {
|
||||
jest.setTimeout(30000);
|
||||
|
||||
beforeEach(() => {
|
||||
jest.useRealTimers();
|
||||
setupMocks();
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user