Skip to content
Snippets Groups Projects
Unverified Commit 70a8ab70 authored by Nicolò Pretto's avatar Nicolò Pretto Committed by GitHub
Browse files

feat(sdk): support `locale` prop on `MetabaseProvider` (#47569)

* wip locale header middleware

* wip fe code for the locale provider for both public/static and sdk

* clean up code

* move api.baseurl assignment outside of useEffect to make it work on first render

* e2e test for locale

* Revert "wip locale header middleware"

This reverts commit c215eac3.

* remove public/static code as we want to focus on the sdk

* adds docs for `X-Metabase-Locale` header and for why we don't use I18NApi.locale

* Update frontend/src/metabase/lib/api.js

* doc: explain why we have the unused state
parent fd615bf2
No related branches found
No related tags found
No related merge requests found
/* eslint-disable no-unscoped-text-selectors */
import { ORDERS_DASHBOARD_ID } from "e2e/support/cypress_sample_instance_data";
import {
restore,
setTokenFeatures,
visitFullAppEmbeddingUrl,
} from "e2e/support/helpers";
import {
EMBEDDING_SDK_STORY_HOST,
describeSDK,
} from "e2e/support/helpers/e2e-embedding-sdk-helpers";
import {
JWT_SHARED_SECRET,
setupJwt,
} from "e2e/support/helpers/e2e-jwt-helpers";
const STORIES = {
DE_LOCALE: "embeddingsdk-locale--de-locale",
} as const;
describeSDK(
"scenarios > embedding-sdk > locale set on MetabaseProvider",
() => {
beforeEach(() => {
restore();
cy.signInAsAdmin();
setTokenFeatures("all");
setupJwt();
cy.signOut();
});
it("when locale=de it should display german text", () => {
visitFullAppEmbeddingUrl({
url: EMBEDDING_SDK_STORY_HOST,
qs: {
id: STORIES.DE_LOCALE,
viewMode: "story",
},
onBeforeLoad: (window: any) => {
window.JWT_SHARED_SECRET = JWT_SHARED_SECRET;
window.METABASE_INSTANCE_URL = Cypress.config().baseUrl;
window.DASHBOARD_ID = ORDERS_DASHBOARD_ID;
},
});
cy.findByText("Als PDF exportieren").should("exist");
});
},
);
......@@ -19,6 +19,7 @@ import {
import type { SdkStoreState } from "embedding-sdk/store/types";
import type { SDKConfig } from "embedding-sdk/types";
import type { MetabaseTheme } from "embedding-sdk/types/theme";
import { LocaleProvider } from "metabase/public/LocaleProvider";
import { setOptions } from "metabase/redux/embed";
import { EmotionCacheProvider } from "metabase/styled-components/components/EmotionCacheProvider";
......@@ -40,6 +41,7 @@ export interface MetabaseProviderProps {
eventHandlers?: SdkEventHandlersConfig;
theme?: MetabaseTheme;
className?: string;
locale?: string;
}
interface InternalMetabaseProviderProps extends MetabaseProviderProps {
......@@ -54,6 +56,7 @@ export const MetabaseProviderInternal = ({
theme,
store,
className,
locale,
}: InternalMetabaseProviderProps): JSX.Element => {
const { fontFamily } = theme ?? {};
useInitData({ config });
......@@ -90,7 +93,7 @@ export const MetabaseProviderInternal = ({
<SdkThemeProvider theme={theme}>
<SdkFontsGlobalStyles baseUrl={config.metabaseInstanceUrl} />
<div className={className} id={EMBEDDING_SDK_ROOT_ELEMENT_ID}>
{children}
<LocaleProvider locale={locale}>{children}</LocaleProvider>
<SdkUsageProblemDisplay config={config} />
<PortalContainer />
<FullPagePortalContainer />
......
......@@ -25,6 +25,12 @@ interface InitDataLoaderParameters {
export const useInitData = ({ config }: InitDataLoaderParameters) => {
const { allowConsoleLog = true } = config;
// This is outside of a useEffect otherwise calls done on the first render could use the wrong value
// This is the case for example for the locale json files
if (api.basename !== config.metabaseInstanceUrl) {
api.basename = config.metabaseInstanceUrl;
}
const dispatch = useSdkDispatch();
const loginStatus = useSdkSelector(getLoginStatus);
......@@ -55,8 +61,6 @@ export const useInitData = ({ config }: InitDataLoaderParameters) => {
return;
}
api.basename = config.metabaseInstanceUrl;
setupSdkAuth(config, dispatch);
}, [config, dispatch, loginStatus.status]);
......
import {
MetabaseProvider,
StaticDashboard,
} from "embedding-sdk/components/public";
import { storybookSdkDefaultConfig } from "embedding-sdk/test/CommonSdkStoryWrapper";
export default {
title: "EmbeddingSDK/Locale",
};
export const DeLocale = () => (
<MetabaseProvider config={storybookSdkDefaultConfig} locale="de">
<StaticDashboard dashboardId={(window as any).DASHBOARD_ID || 1} />
</MetabaseProvider>
);
......@@ -337,3 +337,12 @@ const instance = new Api();
export default instance;
export const { GET, POST, PUT, DELETE } = instance;
export const setLocaleHeader = locale => {
/* `X-Metabase-Locale` is a header that the BE stores as *user* locale for the scope of the request.
* We need it to localize downloads. It *currently* only work if there is a user, so it won't work
* for public/static embedding.
*/
// eslint-disable-next-line no-literal-metabase-strings -- Header name, not a user facing string
DEFAULT_OPTIONS.headers["X-Metabase-Locale"] = locale ?? undefined;
};
......@@ -2,6 +2,7 @@ import dayjs from "dayjs";
import moment from "moment-timezone"; // eslint-disable-line no-restricted-imports -- deprecated usage
import { addLocale, useLocale } from "ttag";
import api from "metabase/lib/api";
import { DAY_OF_WEEK_OPTIONS } from "metabase/lib/date-time";
import MetabaseSettings from "metabase/lib/settings";
......@@ -9,11 +10,16 @@ import MetabaseSettings from "metabase/lib/settings";
export async function loadLocalization(locale) {
// we need to be sure to set the initial localization before loading any files
// so load metabase/services only when we need it
const { I18NApi } = require("metabase/services");
// load and parse the locale
const translationsObject =
locale !== "en"
? await I18NApi.locale({ locale })
? // We don't use I18NApi.locale/the GET helper because those helpers adds custom headers,
// which will make the browser do the pre-flight request on the SDK.
// The backend doesn't seem to support pre-flight request on the static assets, but even
// if it supported them it's more performant to skip the pre-flight request
await fetch(`${api.basename}/app/locales/${locale}.json`).then(
response => response.json(),
)
: // We don't serve en.json. Instead, use this object to fall back to theliterals.
{
headers: {
......
import { type PropsWithChildren, useEffect, useState } from "react";
import { setLocaleHeader } from "metabase/lib/api";
import { loadLocalization } from "metabase/lib/i18n";
export const LocaleProvider = ({
children,
locale = "en",
}: PropsWithChildren<{ locale?: string | null }>) => {
// The state is not used explicitly, but we need to trigger a re-render when the locale changes
// as changing the locale in ttag doesn't trigger react components to update
const [_isLoadingLocale, setIsLoadingLocale] = useState(false);
useEffect(() => {
if (locale) {
setIsLoadingLocale(true);
setLocaleHeader(locale);
loadLocalization(locale).then(() => setIsLoadingLocale(false));
}
}, [locale]);
// note: we may show a loader here while loading, this would prevent race
// conditions and things being rendered for some time with the wrong locale
// downside is that it would make the initial load slower
return <>{children}</>;
};
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment