Skip to content

Commit

Permalink
Extract reusable l10n parts into abstraction
Browse files Browse the repository at this point in the history
  • Loading branch information
Vinnl committed Apr 29, 2024
1 parent 56c50e9 commit a11afd5
Show file tree
Hide file tree
Showing 28 changed files with 281 additions and 142 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
StepDeterminationData,
getNextGuidedStep,
} from "../../../../../../../../../functions/server/getRelevantGuidedSteps";
import { ExtendedReactLocalization } from "../../../../../../../../../hooks/l10n";
import { ExtendedReactLocalization } from "../../../../../../../../../functions/l10n";
import { TelemetryButton } from "../../../../../../../../../components/client/TelemetryButton";

export type Props = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
getNextGuidedStep,
} from "../../../../../../../../../functions/server/getRelevantGuidedSteps";
import { FixView } from "../../FixView";
import { ExtendedReactLocalization } from "../../../../../../../../../hooks/l10n";
import { ExtendedReactLocalization } from "../../../../../../../../../functions/l10n";
import { TelemetryButton } from "../../../../../../../../../components/client/TelemetryButton";
import noBreachesIllustration from "../../images/high-risk-breaches-none.svg";
import { CONST_ONEREP_DATA_BROKER_COUNT } from "../../../../../../../../../../constants";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import noBreachesIllustration from "../images/high-risk-breaches-none.svg";
import { GuidedExperienceBreaches } from "../../../../../../../../functions/server/getUserBreaches";
import { FraudAlertModal } from "./FraudAlertModal";
import { getLocale } from "../../../../../../../../functions/universal/getLocale";
import { ExtendedReactLocalization } from "../../../../../../../../hooks/l10n";
import { ExtendedReactLocalization } from "../../../../../../../../functions/l10n";
import { StepLink } from "../../../../../../../../functions/server/getRelevantGuidedSteps";
import { TelemetryLink } from "../../../../../../../../components/client/TelemetryLink";
import { TelemetryButton } from "../../../../../../../../components/client/TelemetryButton";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import passwordIllustration from "../images/leaked-passwords.svg";
import securityQuestionsIllustration from "../images/security-questions.svg";
import { SubscriberBreach } from "../../../../../../../../../utils/subscriberBreaches";
import { GuidedExperienceBreaches } from "../../../../../../../../functions/server/getUserBreaches";
import { ExtendedReactLocalization } from "../../../../../../../../hooks/l10n";
import { ExtendedReactLocalization } from "../../../../../../../../functions/l10n";
import { Button } from "../../../../../../../../components/client/Button";
import { StepLink } from "../../../../../../../../functions/server/getRelevantGuidedSteps";
import { getLocale } from "../../../../../../../../functions/universal/getLocale";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import phoneIllustration from "../images/security-recommendations-phone.svg";
import ipIllustration from "../images/security-recommendations-ip.svg";
import { GuidedExperienceBreaches } from "../../../../../../../../functions/server/getUserBreaches";
import { SubscriberBreach } from "../../../../../../../../../utils/subscriberBreaches";
import { ExtendedReactLocalization } from "../../../../../../../../hooks/l10n";
import { ExtendedReactLocalization } from "../../../../../../../../functions/l10n";
import { Button } from "../../../../../../../../components/client/Button";
import { StepLink } from "../../../../../../../../functions/server/getRelevantGuidedSteps";

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import Image from "next/image";
import styles from "./View.module.scss";
import AddEmailDialogIllustration from "./images/DeleteAccountDialogIllustration.svg";
import { Toolbar } from "../../../../../../components/client/toolbar/Toolbar";
import { ExtendedReactLocalization } from "../../../../../../hooks/l10n";
import { ExtendedReactLocalization } from "../../../../../../functions/l10n";
import { OpenInNew } from "../../../../../../components/server/Icons";
import { EmailListing } from "./EmailListing";
import { EmailAddressAdder } from "./EmailAddressAdder";
Expand Down
2 changes: 1 addition & 1 deletion src/app/(proper_react)/(redesign)/(public)/HeroImage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

import { SVGProps } from "react";
import { ExtendedReactLocalization } from "../../../hooks/l10n";
import { ExtendedReactLocalization } from "../../../functions/l10n";

export const HeroImageAll = ({
l10n,
Expand Down
2 changes: 1 addition & 1 deletion src/app/(proper_react)/(redesign)/(public)/LandingView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import styles from "./LandingView.module.scss";
import { HeroImageAll, HeroImagePremium } from "./HeroImage";
import { SignUpForm } from "./SignUpForm";
import { ExtendedReactLocalization } from "../../../hooks/l10n";
import { ExtendedReactLocalization } from "../../../functions/l10n";
import { PlansTable } from "./PlansTable";
import { useId } from "react";
import {
Expand Down
2 changes: 1 addition & 1 deletion src/app/(proper_react)/(redesign)/(public)/PublicShell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import Image from "next/image";
import Link from "next/link";
import styles from "./PublicShell.module.scss";
import MonitorLogo from "../../images/monitor-logo.svg";
import { ExtendedReactLocalization } from "../../../hooks/l10n";
import { ExtendedReactLocalization } from "../../../functions/l10n";
import { SignInButton } from "../../../components/client/SignInButton";
import { Footer } from "../Footer";

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* 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 { ExtendedReactLocalization } from "../../../hooks/l10n";
import { ExtendedReactLocalization } from "../../../functions/l10n";
import Image from "next/image";
import ScanningForExposuresImage from "./value-prop-images/scanning-for-exposures.svg";
import LeakedPasswordExampleImage from "./value-prop-images/leaked-password-example.svg";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { getAllPriorityDataClasses } from "../../../../../../utils/recommendatio
import { TelemetryLink } from "../../../../../components/client/TelemetryLink";
import { BreachLogo } from "../../../../../components/server/BreachLogo";
import { getLocale } from "../../../../../functions/universal/getLocale";
import { ExtendedReactLocalization } from "../../../../../hooks/l10n";
import { ExtendedReactLocalization } from "../../../../../functions/l10n";
import BreachDetailScanImage from "./images/breach-detail-scan.svg";
import { Button } from "../../../../../components/client/Button";

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
import Link from "next/link";
import styles from "./BreachIndexView.module.scss";
import { HibpLikeDbBreach } from "../../../../../utils/hibp";
import { ExtendedReactLocalization, useL10n } from "../../../../hooks/l10n";
import { useL10n } from "../../../../hooks/l10n";
import { ExtendedReactLocalization } from "../../../../functions/l10n";
import { BreachLogo } from "../../../../components/server/BreachLogo";
import { getLocale } from "../../../../functions/universal/getLocale";
import { useHasRenderedClientSide } from "../../../../hooks/useHasRenderedClientSide";
Expand Down
2 changes: 1 addition & 1 deletion src/app/(proper_react)/(redesign)/Footer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import styles from "./Shell.module.scss";
import Image from "next/image";
import Link from "next/link";
import mozillaLogo from "../images/mozilla-logo.svg";
import { ExtendedReactLocalization } from "../../hooks/l10n";
import { ExtendedReactLocalization } from "../../functions/l10n";
import {
CONST_URL_SUMO_MONITOR_FAQ,
CONST_URL_MONITOR_GITHUB,
Expand Down
2 changes: 1 addition & 1 deletion src/app/(proper_react)/(redesign)/Shell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import monitorLogo from "../images/monitor-logo.webp";
import { MobileShell } from "./MobileShell";
import Link from "next/link";
import { PageLink } from "./PageLink";
import { ExtendedReactLocalization } from "../../hooks/l10n";
import { ExtendedReactLocalization } from "../../functions/l10n";
import {
getSubscriptionBillingAmount,
getPremiumSubscriptionUrl,
Expand Down
121 changes: 121 additions & 0 deletions src/app/functions/l10n/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* 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 { FluentBundle, FluentResource } from "@fluent/bundle";
import { acceptedLanguages, negotiateLanguages } from "@fluent/langneg";
import type { MarkupParser, ReactLocalization } from "@fluent/react";
import { Fragment, createElement } from "react";

/**
* Equivalent to ReactLocalization.getString, but returns a React Fragment.
*
* This is useful because it allows you to replace tags in localised strings
* with HTML elements, without needing to reach out to <Localized>.
*
* (This method got booted out of @fluent/react proper because it's so simple,
* but it's pretty useful:
* https://github.com/projectfluent/fluent.js/pull/595#discussion_r967011632)
*/

export type GetFragment = (
id: Parameters<ReactLocalization["getString"]>[0],
args?: Parameters<ReactLocalization["getElement"]>[2],
fallback?: Parameters<ReactLocalization["getString"]>[2],
) => ReturnType<ReactLocalization["getElement"]> | null;

export type ExtendedReactLocalization = ReactLocalization & {
getFragment: GetFragment;
};

export type LocaleData = {
locale: string;
bundleSources: string[];
};

/**
* Get the localisation sources for the locales relevant to the current user
*
* This function can run on the server side, and only returns serialisable data.
* This means that it can either be used to construct a ReactLocalization object
* on the server side, or be passed to Client Component to construct such an
* object on the client side.
*
* @param acceptLang The user's preferred locales, in the syntax of the Accept-Language HTTP header
* @returns The sources for l10n bundles that can be used to construct a ReactLocalization object
*/
export type GetL10nBundles = (acceptLangHeader?: string) => LocaleData[];
type LocaleId = string;
type CreateGetL10nBundlesOptions = {
availableLocales: LocaleId[];
loadLocaleFiles: (locale: LocaleId) => string[];
loadPendingStrings: () => string[];
getAcceptLangHeader: () => string;
};
export function createGetL10nBundles(
options: CreateGetL10nBundlesOptions,
): GetL10nBundles {
return (acceptLangHeader = options.getAcceptLangHeader()) => {
const languages = acceptedLanguages(acceptLangHeader);
const supportedLocales = process.env.SUPPORTED_LOCALES?.split(",");
const filteredLocales =
// `SUPPORTED_LOCALES` is set in the `.env-dist` file, so it'll always
// be available when running tests.
/* c8 ignore next 2 */
typeof supportedLocales === "undefined"
? options.availableLocales
: options.availableLocales.filter((locale) =>
supportedLocales.includes(locale),
);
const relevantLocales = negotiateLanguages(languages, filteredLocales, {
defaultLocale: "en",
});

const relevantBundleSources = relevantLocales.map((relevantLocale) => {
let bundleSources = options.loadLocaleFiles(relevantLocale);
if (relevantLocale === "en") {
bundleSources = bundleSources.concat(options.loadPendingStrings());
}
return {
locale: relevantLocale,
bundleSources: bundleSources,
};
});

return relevantBundleSources;
};
}

export type GetL10n = (localeData?: LocaleData[]) => ExtendedReactLocalization;
type CreateGetL10nOptions = {
getL10nBundles: GetL10nBundles;
ReactLocalization: typeof ReactLocalization;
parseMarkup?: MarkupParser;
};
export function createGetL10n(options: CreateGetL10nOptions): GetL10n {
const bundles: Record<string, FluentBundle> = {};
function getBundle(localeData: LocaleData): FluentBundle {
if (bundles[localeData.locale]) {
return bundles[localeData.locale];
}
bundles[localeData.locale] = new FluentBundle(localeData.locale);
localeData.bundleSources.forEach((bundleSource) => {
bundles[localeData.locale].addResource(new FluentResource(bundleSource));
});
return bundles[localeData.locale];
}

return (localeData = options.getL10nBundles()) => {
const bundles: FluentBundle[] = localeData.map((data) => getBundle(data));
const l10n = new options.ReactLocalization(bundles, options.parseMarkup);

const getFragment: GetFragment = (id, args, fallback) =>
l10n.getElement(createElement(Fragment, null, fallback ?? id), id, args);

const extendedL10n: ExtendedReactLocalization =
l10n as ExtendedReactLocalization;
extendedL10n.getFragment = getFragment;

return extendedL10n;
};
}
Loading

0 comments on commit a11afd5

Please sign in to comment.