Allow loading Storybook in other locales

This commit is contained in:
Vincent 2024-01-05 12:23:25 +01:00 коммит произвёл Vincent
Родитель 971ce2cfb7
Коммит 31397d0b02
15 изменённых файлов: 74 добавлений и 54 удалений

Просмотреть файл

@ -9,14 +9,11 @@ import { action } from "@storybook/addon-actions";
import { linkTo } from "@storybook/addon-links";
import "../src/app/globals.css";
import { metropolis } from "../src/app/fonts/Metropolis/metropolis";
import { getEnL10nBundlesSync } from "../src/app/functions/server/mockL10n";
import { TestComponentWrapper } from "../src/TestComponentWrapper";
const inter = Inter({ subsets: ["latin"], variable: "--font-inter" });
const AppDecorator: Preview["decorators"] = (storyFn) => {
const l10nBundles = getEnL10nBundlesSync();
useEffect(() => {
// We have to add these classes to the body, rather than simply wrapping the
// storyFn in a container, because some components (most notably, the ones

Просмотреть файл

@ -7,9 +7,9 @@ import { L10nProvider } from "./contextProviders/localization";
import { PublicEnvProvider } from "./contextProviders/public-env";
import { SessionProvider } from "next-auth/react";
import { ReactAriaI18nProvider } from "./contextProviders/react-aria";
import { getEnL10nBundlesSync } from "./app/functions/server/mockL10n";
import { getOneL10nBundleSync } from "./app/functions/server/mockL10n";
const l10nBundles = getEnL10nBundlesSync();
const l10nBundles = getOneL10nBundleSync();
export const TestComponentWrapper = (props: { children: ReactNode }) => {
return (

Просмотреть файл

@ -7,7 +7,7 @@ import type { Meta, StoryObj } from "@storybook/react";
import { OnerepScanResultRow, OnerepScanRow } from "knex/types/tables";
import { View as DashboardEl } from "./View";
import { Shell } from "../../../../Shell";
import { getEnL10nSync } from "../../../../../../functions/server/mockL10n";
import { getOneL10nSync } from "../../../../../../functions/server/mockL10n";
import {
createRandomScanResult,
createRandomBreach,
@ -148,7 +148,7 @@ const DashboardWrapper = (props: DashboardWrapperProps) => {
return (
<CountryCodeProvider countryCode={props.countryCode}>
<Shell l10n={getEnL10nSync()} session={mockedSession} nonce="">
<Shell l10n={getOneL10nSync()} session={mockedSession} nonce="">
<DashboardEl
user={user}
userBreaches={breaches}

Просмотреть файл

@ -11,7 +11,7 @@ import {
createUserWithPremiumSubscription,
} from "../../../../../../../../../../apiMocks/mockData";
import { Shell } from "../../../../../../../Shell";
import { getEnL10nSync } from "../../../../../../../../../functions/server/mockL10n";
import { getOneL10nSync } from "../../../../../../../../../functions/server/mockL10n";
import { LatestOnerepScanData } from "../../../../../../../../../../db/tables/onerep_scans";
const mockedScan: OnerepScanRow = {
@ -50,7 +50,7 @@ export const AutomaticRemoveViewStory: Story = {
name: "1d. Automatically resolve brokers",
render: () => {
return (
<Shell l10n={getEnL10nSync()} session={mockedSession} nonce="">
<Shell l10n={getOneL10nSync()} session={mockedSession} nonce="">
<AutomaticRemoveView
data={{
countryCode: "us",

Просмотреть файл

@ -11,7 +11,7 @@ import {
createUserWithPremiumSubscription,
} from "../../../../../../../../../../apiMocks/mockData";
import { Shell } from "../../../../../../../Shell";
import { getEnL10nSync } from "../../../../../../../../../functions/server/mockL10n";
import { getOneL10nSync } from "../../../../../../../../../functions/server/mockL10n";
import { LatestOnerepScanData } from "../../../../../../../../../../db/tables/onerep_scans";
import { hasPremium } from "../../../../../../../../../functions/universal/user";
@ -51,7 +51,7 @@ export const ManualRemoveViewStory: Story = {
name: "1c. Manually resolve brokers",
render: () => {
return (
<Shell l10n={getEnL10nSync()} session={mockedSession} nonce="">
<Shell l10n={getOneL10nSync()} session={mockedSession} nonce="">
<ManualRemoveView
scanData={mockedScanData}
breaches={mockedBreaches}

Просмотреть файл

@ -11,7 +11,7 @@ import {
createUserWithPremiumSubscription,
} from "../../../../../../../../../../apiMocks/mockData";
import { Shell } from "../../../../../../../Shell";
import { getEnL10nSync } from "../../../../../../../../../functions/server/mockL10n";
import { getOneL10nSync } from "../../../../../../../../../functions/server/mockL10n";
import { LatestOnerepScanData } from "../../../../../../../../../../db/tables/onerep_scans";
const mockedScan: OnerepScanRow = {
@ -50,7 +50,7 @@ export const StartFreeScanViewStory: Story = {
name: "1a. Free scan",
render: () => {
return (
<Shell l10n={getEnL10nSync()} session={mockedSession} nonce="">
<Shell l10n={getOneL10nSync()} session={mockedSession} nonce="">
<StartFreeScanView
data={{
countryCode: "us",

Просмотреть файл

@ -10,7 +10,7 @@ import {
createUserWithPremiumSubscription,
} from "../../../../../../../../../../apiMocks/mockData";
import { Shell } from "../../../../../../../Shell";
import { getEnL10nSync } from "../../../../../../../../../functions/server/mockL10n";
import { getOneL10nSync } from "../../../../../../../../../functions/server/mockL10n";
import { LatestOnerepScanData } from "../../../../../../../../../../db/tables/onerep_scans";
const brokerOptions = {
@ -93,7 +93,7 @@ const ViewWrapper = (props: ViewWrapperProps) => {
};
return (
<Shell l10n={getEnL10nSync()} session={mockedSession} nonce="">
<Shell l10n={getOneL10nSync()} session={mockedSession} nonce="">
<ViewDataBrokersView
data={{
latestScanData: scanData,

Просмотреть файл

@ -11,7 +11,7 @@ import {
createUserWithPremiumSubscription,
} from "../../../../../../../../../../apiMocks/mockData";
import { Shell } from "../../../../../../../Shell";
import { getEnL10nSync } from "../../../../../../../../../functions/server/mockL10n";
import { getOneL10nSync } from "../../../../../../../../../functions/server/mockL10n";
import { LatestOnerepScanData } from "../../../../../../../../../../db/tables/onerep_scans";
const mockedScan: OnerepScanRow = {
@ -50,7 +50,7 @@ export const ManualRemoveViewStory: Story = {
name: "1e. Welcome to Premium",
render: () => {
return (
<Shell l10n={getEnL10nSync()} session={mockedSession} nonce="">
<Shell l10n={getOneL10nSync()} session={mockedSession} nonce="">
<WelcomeToPremiumView
data={{
countryCode: "us",

Просмотреть файл

@ -8,7 +8,7 @@ import {
createUserWithPremiumSubscription,
} from "../../../../../../../../../../apiMocks/mockData";
import { Shell } from "../../../../../../../Shell";
import { getEnL10nSync } from "../../../../../../../../../functions/server/mockL10n";
import { getOneL10nSync } from "../../../../../../../../../functions/server/mockL10n";
import { HighRiskBreachLayout } from "../HighRiskBreachLayout";
import {
HighRiskBreachTypes,
@ -102,7 +102,7 @@ const HighRiskBreachWrapper = (props: {
};
return (
<Shell l10n={getEnL10nSync()} session={mockedSession} nonce="">
<Shell l10n={getOneL10nSync()} session={mockedSession} nonce="">
<HighRiskBreachLayout
subscriberEmails={[]}
type={props.type}

Просмотреть файл

@ -8,7 +8,7 @@ import {
createUserWithPremiumSubscription,
} from "../../../../../../../../../../apiMocks/mockData";
import { Shell } from "../../../../../../../Shell";
import { getEnL10nSync } from "../../../../../../../../../functions/server/mockL10n";
import { getOneL10nSync } from "../../../../../../../../../functions/server/mockL10n";
import { LeakedPasswordsLayout } from "../LeakedPasswordsLayout";
import {
LeakedPasswordsTypes,
@ -64,7 +64,7 @@ const LeakedPasswordsWrapper = (props: {
}
return (
<Shell l10n={getEnL10nSync()} session={mockedSession} nonce="">
<Shell l10n={getOneL10nSync()} session={mockedSession} nonce="">
<LeakedPasswordsLayout
subscriberEmails={[]}
type={props.type}

Просмотреть файл

@ -8,7 +8,7 @@ import {
createUserWithPremiumSubscription,
} from "../../../../../../../../../../apiMocks/mockData";
import { Shell } from "../../../../../../../Shell";
import { getEnL10nSync } from "../../../../../../../../../functions/server/mockL10n";
import { getOneL10nSync } from "../../../../../../../../../functions/server/mockL10n";
import { SecurityRecommendationsLayout } from "../SecurityRecommendationsLayout";
import {
SecurityRecommendationTypes,
@ -42,7 +42,7 @@ const SecurityRecommendationsWrapper = (props: {
type: SecurityRecommendationTypes;
}) => {
return (
<Shell l10n={getEnL10nSync()} session={mockedSession} nonce="">
<Shell l10n={getOneL10nSync()} session={mockedSession} nonce="">
<SecurityRecommendationsLayout
subscriberEmails={[]}
type={props.type}

Просмотреть файл

@ -7,7 +7,7 @@ import { render, screen } from "@testing-library/react";
import { axe } from "jest-axe";
import { Session } from "next-auth";
import { userEvent } from "@testing-library/user-event";
import { getEnL10nSync } from "../../../../../../functions/server/mockL10n";
import { getOneL10nSync } from "../../../../../../functions/server/mockL10n";
import { TestComponentWrapper } from "../../../../../../../TestComponentWrapper";
import { EmailRow } from "../../../../../../../db/tables/emailAddresses";
import { SerializedSubscriber } from "../../../../../../../next-auth";
@ -61,7 +61,7 @@ it("passes the axe accessibility audit", async () => {
const { container } = render(
<TestComponentWrapper>
<SettingsView
l10n={getEnL10nSync()}
l10n={getOneL10nSync()}
user={mockedUser}
breachCountByEmailAddress={{
[mockedUser.email]: 42,
@ -85,7 +85,7 @@ it("preselects 'Send all breach alerts to the primary email address' if that's t
render(
<TestComponentWrapper>
<SettingsView
l10n={getEnL10nSync()}
l10n={getOneL10nSync()}
user={{
...mockedUser,
subscriber: {
@ -120,7 +120,7 @@ it("preselects 'Send breach alerts to the affected email address' if that's the
render(
<TestComponentWrapper>
<SettingsView
l10n={getEnL10nSync()}
l10n={getOneL10nSync()}
user={{
...mockedUser,
subscriber: {
@ -157,7 +157,7 @@ it("sends a call to the API to change the email alert preferences when changing
render(
<TestComponentWrapper>
<SettingsView
l10n={getEnL10nSync()}
l10n={getOneL10nSync()}
user={{
...mockedUser,
subscriber: {
@ -203,7 +203,7 @@ it("refreshes the session token after changing email alert preferences, to ensur
render(
<TestComponentWrapper>
<SettingsView
l10n={getEnL10nSync()}
l10n={getOneL10nSync()}
user={{
...mockedUser,
subscriber: {
@ -235,7 +235,7 @@ it("marks unverified email addresses as such", () => {
render(
<TestComponentWrapper>
<SettingsView
l10n={getEnL10nSync()}
l10n={getOneL10nSync()}
user={mockedUser}
breachCountByEmailAddress={{
[mockedUser.email]: 42,
@ -266,7 +266,7 @@ it("calls the API to resend a verification email if requested to", async () => {
render(
<TestComponentWrapper>
<SettingsView
l10n={getEnL10nSync()}
l10n={getOneL10nSync()}
user={mockedUser}
breachCountByEmailAddress={{
[mockedUser.email]: 42,
@ -307,7 +307,7 @@ it("calls the 'remove' action when clicking the rubbish bin icon", async () => {
render(
<TestComponentWrapper>
<SettingsView
l10n={getEnL10nSync()}
l10n={getOneL10nSync()}
user={mockedUser}
breachCountByEmailAddress={{
[mockedUser.email]: 42,
@ -342,7 +342,7 @@ it.skip("calls the 'add' action when adding another email address", async () =>
render(
<TestComponentWrapper>
<SettingsView
l10n={getEnL10nSync()}
l10n={getOneL10nSync()}
user={mockedUser}
breachCountByEmailAddress={{
[mockedUser.email]: 42,

Просмотреть файл

@ -4,13 +4,13 @@
import type { Meta, StoryObj } from "@storybook/react";
import { View } from "./LandingView";
import { getEnL10nSync } from "../../../functions/server/mockL10n";
import { getOneL10nSync } from "../../../functions/server/mockL10n";
const meta: Meta<typeof View> = {
title: "Pages/Public landing page",
component: View,
args: {
l10n: getEnL10nSync(),
l10n: getOneL10nSync(),
},
};
@ -48,6 +48,7 @@ export const LandingNonUsDe: Story = {
args: {
eligibleForPremium: false,
countryCode: "de",
l10n: getOneL10nSync("de"),
},
};
@ -56,5 +57,6 @@ export const LandingNonUsFr: Story = {
args: {
eligibleForPremium: false,
countryCode: "fr",
l10n: getOneL10nSync("fr"),
},
};

Просмотреть файл

@ -2,8 +2,8 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import { getEnL10nBundlesInNodeContext, getEnL10nSync } from "../mockL10n";
import { getOneL10nBundleInNodeContext, getOneL10nSync } from "../mockL10n";
export const getL10nBundles = getEnL10nBundlesInNodeContext;
export const getL10nBundles = getOneL10nBundleInNodeContext;
export const getL10n = getEnL10nSync;
export const getL10n = getOneL10nSync;

Просмотреть файл

@ -17,22 +17,29 @@ import type { LocaleData } from "./l10n";
// Code in this file is only used in tests and Storybook, not in production:
/* c8 ignore start */
export function getEnL10nBundlesSync(): LocaleData[] {
export function getOneL10nBundleSync(locale = detectLocale()): LocaleData[] {
return process.env.STORYBOOK === "true"
? getEnL10nBundlesInWebpackContext()
: getEnL10nBundlesInNodeContext();
? getOneL10nBundleInWebpackContext(locale)
: getOneL10nBundleInNodeContext(locale);
}
export function getEnL10nBundlesInWebpackContext(): LocaleData[] {
const referenceStringsContext: { keys: () => string[] } & ((
export function getOneL10nBundleInWebpackContext(
locale = detectLocale(),
): LocaleData[] {
const allStringsContext: { keys: () => string[] } & ((
path: string,
// `require` isn't usually valid JS, so skip type checking for that:
// eslint-disable-next-line @typescript-eslint/no-explicit-any
) => string) = (require as any).context(
"../../../../locales/en",
"../../../../locales/",
true,
/\.ftl$/,
);
const allStringFilenames = allStringsContext.keys();
const localeStringFilenames = allStringFilenames.filter((filepath) =>
filepath.startsWith(`./${locale}/`),
);
const pendingTranslationsContext: { keys: () => string[] } & ((
path: string,
// `require` isn't usually valid JS, so skip type checking for that:
@ -42,10 +49,9 @@ export function getEnL10nBundlesInWebpackContext(): LocaleData[] {
true,
/\.ftl$/,
);
const referenceSourceFilenames = referenceStringsContext.keys();
const pendingSourceFilenames = pendingTranslationsContext.keys();
const bundleSources: string[] = referenceSourceFilenames
.map((filePath) => referenceStringsContext(filePath))
const bundleSources: string[] = localeStringFilenames
.map((filePath) => allStringsContext(filePath))
.concat(
pendingSourceFilenames.map((filePath) =>
pendingTranslationsContext(filePath),
@ -54,13 +60,15 @@ export function getEnL10nBundlesInWebpackContext(): LocaleData[] {
return [
{
locale: "en",
locale: locale,
bundleSources: bundleSources,
},
];
}
export function getEnL10nBundlesInNodeContext(): LocaleData[] {
export function getOneL10nBundleInNodeContext(
locale = detectLocale(),
): LocaleData[] {
const {
readdirSync: nodeReaddirSync,
readFileSync: nodeReadFileSync,
@ -71,7 +79,7 @@ export function getEnL10nBundlesInNodeContext(): LocaleData[] {
const { resolve: resolvePath }: { resolve: typeof resolve } = require("path");
const referenceStringsPath = resolvePath(
__dirname,
"../../../../locales/en/",
`../../../../locales/${locale}/`,
);
const pendingStringsPath = resolvePath(
__dirname,
@ -91,7 +99,7 @@ export function getEnL10nBundlesInNodeContext(): LocaleData[] {
return [
{
locale: "en",
locale: locale,
bundleSources: bundleSources,
},
];
@ -122,15 +130,19 @@ function getBundle(localeData: LocaleData): FluentBundle {
}
/**
* This function loads a ReactLocalization instance with the `en` and pending strings.
* This function loads a ReactLocalization instance with the given locale (`en` by default) and pending strings.
*
* This is useful in tests and Storybook, where we can't call `headers` from
* `next/headers` to determine the user's locale, and even just importing from a
* module that references it can result in an error about only being able to use
* it in Server Components.
*
* @param locale {string} Locale to load; `en` by default.
*/
export function getEnL10nSync(): ExtendedReactLocalization {
const localeData: LocaleData[] = getEnL10nBundlesSync();
export function getOneL10nSync(
locale = detectLocale(),
): ExtendedReactLocalization {
const localeData: LocaleData[] = getOneL10nBundleSync(locale);
const bundles: FluentBundle[] = localeData.map((data) => getBundle(data));
// The ReactLocalization instance stores and caches the sequence of generated
@ -151,3 +163,12 @@ export function getEnL10nSync(): ExtendedReactLocalization {
return extendedL10n;
}
function detectLocale() {
const locale =
typeof document !== "undefined"
? new URLSearchParams(document.location.search).get("locale") ?? undefined
: undefined;
return locale ?? "en";
}