-
Notifications
You must be signed in to change notification settings - Fork 16.6k
feat(i18n): load language pack asynchronously #34119
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
b5c2a23
982c627
4f5469d
af869ff
29feb7b
8708edc
550a62d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -19,65 +19,81 @@ | |
| 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'; | ||
| import setupDashboardComponents from './setup/setupDashboardComponents'; | ||
| import { User } from './types/bootstrapTypes'; | ||
| import getBootstrapData, { applicationRoot } from './utils/getBootstrapData'; | ||
|
|
||
| configure(); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What is this for? Is there a loading happening between this and the language pack loading?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm unclear what exactly it does, but it prepares a bunch of things around the various "registries" and such. It logs a bunch of things to the console if called too late. Might be worth digging a bit deeper. I'll double check there's no important perf costs in calling it twice. |
||
|
|
||
| // 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<void, User>({ | ||
| 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<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 | ||
| }); | ||
| } | ||
| }); | ||
| } | ||
| })(); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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/<lang>/") | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @mistercrunch It would be nice to put this endpoint under
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yeah I thought through this wondering if this is a private or public endpoint, and thought ideally should be public, but given the partitioning of libraries in the frontend, and the fact that our i18n framework is monolithic currently (no segmentation or context based on the provenance of the strings), it felt like it would require significantly more work to do this properly. Not sure whether the extension framework SIP mentions Without going to deep into it, it seems that if we want for extensions to be localized/localizable, we'd need some way to segment the language pack based on package provenance (backend/frontend, and break down per library on the frontend side). The whole i18n stack might need to offer different ways to fetch and apply translations. Happy to help think through this, but it's a fairly significant amount of work. Note that one minor and valuable thing I considered while doing the work here was to find a way to add metadata to translation to identify what is frontend/backend, and make sure the endpoint only returns the strings intended for the frontend ... The gettext-based/flask-babel framework we use does offer ways to do such things, but I encountered some limitations, and decided to punt on these. This PR here addresses something pretty important - removing the language pack from bootstrap data - but there's much more to do in this area. |
||
| 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: | ||
|
|
||
This comment was marked as resolved.
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.