diff --git a/superset-frontend/src/preamble.ts b/superset-frontend/src/preamble.ts index c46efb134d8a..32010d6c0cc0 100644 --- a/superset-frontend/src/preamble.ts +++ b/superset-frontend/src/preamble.ts @@ -19,7 +19,13 @@ import { setConfig as setHotLoaderConfig } from 'react-hot-loader'; import dayjs from 'dayjs'; // eslint-disable-next-line no-restricted-imports -import { configure, makeApi, initFeatureFlags } from '@superset-ui/core'; +import { + configure, + makeApi, + initFeatureFlags, + SupersetClient, + LanguagePack, +} from '@superset-ui/core'; import setupClient from './setup/setupClient'; import setupColors from './setup/setupColors'; import setupFormatters from './setup/setupFormatters'; @@ -27,57 +33,67 @@ import setupDashboardComponents from './setup/setupDashboardComponents'; import { User } from './types/bootstrapTypes'; import getBootstrapData, { applicationRoot } from './utils/getBootstrapData'; +configure(); + +// Set hot reloader config if (process.env.WEBPACK_MODE === 'development') { setHotLoaderConfig({ logLevel: 'debug', trackTailUpdates: false }); } -// eslint-disable-next-line import/no-mutable-exports +// Grab initial bootstrap data const bootstrapData = getBootstrapData(); -// Configure translation -if (typeof window !== 'undefined') { - configure({ languagePack: bootstrapData.common.language_pack }); - dayjs.locale(bootstrapData.common.locale); -} else { - configure(); -} - -// Configure feature flags -initFeatureFlags(bootstrapData.common.feature_flags); - -// Setup SupersetClient +// Setup SupersetClient early so we can fetch language pack setupClient({ appRoot: applicationRoot() }); -setupColors( - bootstrapData.common.extra_categorical_color_schemes, - bootstrapData.common.extra_sequential_color_schemes, -); +// 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'); + } + } -// Setup number formatters -setupFormatters( - bootstrapData.common.d3_format, - bootstrapData.common.d3_time_format, -); + // Continue with rest of setup + initFeatureFlags(bootstrapData.common.feature_flags); -setupDashboardComponents(); + setupColors( + bootstrapData.common.extra_categorical_color_schemes, + bootstrapData.common.extra_sequential_color_schemes, + ); -const getMe = makeApi({ - method: 'GET', - endpoint: '/api/v1/me/', -}); + setupFormatters( + bootstrapData.common.d3_format, + bootstrapData.common.d3_time_format, + ); -/** - * When you re-open the window, we check if you are still logged in. - * If your session expired or you signed out, we'll redirect to login. - * If you aren't logged in in the first place (!isActive), then we shouldn't do this. - */ -if (bootstrapData.user?.isActive) { - document.addEventListener('visibilitychange', () => { - // we only care about the tab becoming visible, not vice versa - if (document.visibilityState !== 'visible') return; + setupDashboardComponents(); - getMe().catch(() => { - // ignore error, SupersetClient will redirect to login on a 401 - }); + const getMe = makeApi({ + 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 + }); + } + }); + } +})(); diff --git a/superset/views/base.py b/superset/views/base.py index 8eb201d58601..40877e1b2607 100644 --- a/superset/views/base.py +++ b/superset/views/base.py @@ -62,7 +62,6 @@ from superset.extensions import cache_manager from superset.reports.models import ReportRecipientType from superset.superset_typing import FlaskResponse -from superset.translations.utils import get_language_pack from superset.utils import core as utils, json from superset.utils.filters import get_dataset_access_filters from superset.views.error_handling import json_error_response @@ -364,7 +363,6 @@ def cached_common_bootstrap_data( # pylint: disable=unused-argument "static_assets_prefix": conf["STATIC_ASSETS_PREFIX"], "conf": frontend_config, "locale": language, - "language_pack": get_language_pack(language), "d3_format": conf.get("D3_FORMAT"), "d3_time_format": conf.get("D3_TIME_FORMAT"), "currencies": conf.get("CURRENCIES"), diff --git a/superset/views/core.py b/superset/views/core.py index 2d9cfabd4ce3..c77f8dc4fe98 100755 --- a/superset/views/core.py +++ b/superset/views/core.py @@ -19,11 +19,22 @@ import contextlib import logging +import os +import re from datetime import datetime from typing import Any, Callable, cast from urllib import parse -from flask import abort, flash, g, redirect, request, Response, url_for +from flask import ( + abort, + flash, + g, + redirect, + request, + Response, + send_file, + url_for, +) from flask_appbuilder import expose from flask_appbuilder.security.decorators import ( has_access, @@ -32,6 +43,7 @@ ) from flask_babel import gettext as __, lazy_gettext as _ from sqlalchemy.exc import SQLAlchemyError +from werkzeug.utils import safe_join from superset import ( app, @@ -891,6 +903,24 @@ def fetch_datasource_metadata(self) -> FlaskResponse: datasource.raise_for_access() return json_success(json.dumps(sanitize_datasource_data(datasource.data))) + @event_logger.log_this + @has_access + @expose("/language_pack//") + def language_pack(self, lang: str) -> FlaskResponse: + # Only allow expected language formats like "en", "pt_BR", etc. + if not re.match(r"^[a-z]{2,3}(_[A-Z]{2})?$", lang): + abort(400, "Invalid language code") + + base_dir = os.path.join(os.path.dirname(__file__), "..", "translations") + file_path = safe_join(base_dir, lang, "LC_MESSAGES", "messages.json") + + if file_path and os.path.isfile(file_path): + return send_file(file_path, mimetype="application/json") + + return json_error_response( + "Language pack doesn't exist on the server", status=404 + ) + @event_logger.log_this @expose("/welcome/") def welcome(self) -> FlaskResponse: diff --git a/tests/integration_tests/reports/api_tests.py b/tests/integration_tests/reports/api_tests.py index 7737e346e154..8b595f563cdf 100644 --- a/tests/integration_tests/reports/api_tests.py +++ b/tests/integration_tests/reports/api_tests.py @@ -1698,7 +1698,7 @@ def test_update_report_preserve_ownership(self): .filter(ReportSchedule.name == "name1") .one_or_none() ) - assert updated_report.owners == current_owners + assert set(updated_report.owners) == set(current_owners) @pytest.mark.usefixtures("create_report_schedules") def test_update_report_clear_owner_list(self):