From 652c09ab50c863fc44ac9a86418af7edb91522a6 Mon Sep 17 00:00:00 2001 From: Reino Muhl Date: Thu, 21 Mar 2024 17:00:19 -0400 Subject: [PATCH] feat(next): add l10n to localizer for next rsc Because: * Ensure all localization strings can be localized in React Server Components. This commit: * Removes LocalizerServer * Adds LocalizerRSC to serve as a wrapper for ReactLocalization that can be used by React Server Components, as well as a few other utility methods. * Adds LocalizerRscFactory that instantiates LocalizerRSC only with bundles for a provided acceptLanguage string. * Adds LocalizerRscFactoryProvider to be used with the NestApp to instantiate and then intialize the LocalizerRscFactory. Initalization will fetch all messages from disk and populate the bundles. * Reorganize shared/l10n library to match Node style guide * Removes demo l10n code * Update purchase-details, terms-and-service components, and success and error pages to use new LocalizerRSC class for localization. * Adds temporary l10n-convert script to provide translation for existing ftl strings. Closes #FXA-8822 --- .../checkout/[interval]/[cartId]/en.ftl | 3 + .../checkout/[interval]/[cartId]/error/en.ftl | 4 + .../[interval]/[cartId]/error/page.tsx | 75 ++-- .../checkout/[interval]/[cartId]/page.tsx | 123 +++++- .../[interval]/[cartId]/success/en.ftl | 21 + .../[interval]/[cartId]/success/page.tsx | 133 +++---- .../[locale]/[offeringId]/checkout/layout.tsx | 33 +- .../payments/next/app/_lib/scripts/convert.ts | 110 ++++++ apps/payments/next/project.json | 6 +- apps/payments/next/public/l10n/README.md | 5 - apps/payments/next/public/l10n/de-DE.ftl | 39 -- apps/payments/next/public/l10n/en-US.ftl | 56 --- apps/payments/next/public/l10n/es-ES.ftl | 82 ---- apps/payments/next/public/l10n/fr-FR.ftl | 55 --- .../payments/ui/src/lib/nestapp/app.module.ts | 4 +- libs/payments/ui/src/lib/nestapp/app.ts | 7 +- .../ui/src/lib/server/purchase-details.tsx | 76 ++-- .../ui/src/lib/server/terms-and-privacy.tsx | 21 +- libs/shared/l10n/jest.config.ts | 4 + libs/shared/l10n/src/client.ts | 5 + libs/shared/l10n/src/index.ts | 21 +- .../l10n/src/lib/determine-direction.spec.ts | 36 -- .../l10n/src/lib/determine-direction.ts | 22 -- .../l10n/src/lib/determine-locale.spec.ts | 83 ---- libs/shared/l10n/src/lib/determine-locale.ts | 42 -- .../{other-languages.ts => l10n.constants.ts} | 1 + ...atters.spec.ts => l10n.formatters.spec.ts} | 5 +- .../lib/{formatters.ts => l10n.formatters.ts} | 3 + libs/shared/l10n/src/lib/l10n.ts | 120 ------ libs/shared/l10n/src/lib/l10n.types.ts | 7 + libs/shared/l10n/src/lib/l10n.utils.spec.ts | 367 ++++++++++++++++++ libs/shared/l10n/src/lib/l10n.utils.ts | 171 ++++++++ .../l10n/src/lib/localize-timestamp.spec.ts | 80 ---- .../shared/l10n/src/lib/localize-timestamp.ts | 53 --- .../l10n/src/lib/localizer/localizer.base.ts | 16 +- .../localizer/localizer.client.bindings.ts | 3 + .../lib/localizer/localizer.client.spec.ts | 3 + .../src/lib/localizer/localizer.client.ts | 6 +- .../src/lib/localizer/localizer.interfaces.ts | 1 - .../src/lib/localizer/localizer.models.ts | 1 - .../src/lib/localizer/localizer.provider.ts | 12 +- .../localizer/localizer.rsc.factory.spec.ts | 81 ++++ .../lib/localizer/localizer.rsc.factory.ts | 63 +++ .../src/lib/localizer/localizer.rsc.spec.ts | 116 ++++++ .../l10n/src/lib/localizer/localizer.rsc.ts | 120 ++++++ .../localizer/localizer.server.bindings.ts | 4 + .../lib/localizer/localizer.server.spec.ts | 69 ---- .../src/lib/localizer/localizer.server.ts | 33 -- .../src/lib/parse-accept-language.spec.ts | 149 ------- .../l10n/src/lib/parse-accept-language.ts | 70 ---- libs/shared/l10n/src/server.ts | 6 +- libs/shared/l10n/tests/__mocks__/empty.ts | 18 + package.json | 3 + tsconfig.base.json | 1 + types/server-only/index.d.ts | 1 + yarn.lock | 10 + 56 files changed, 1427 insertions(+), 1232 deletions(-) create mode 100644 apps/payments/next/app/[locale]/[offeringId]/checkout/[interval]/[cartId]/en.ftl create mode 100644 apps/payments/next/app/[locale]/[offeringId]/checkout/[interval]/[cartId]/error/en.ftl create mode 100644 apps/payments/next/app/[locale]/[offeringId]/checkout/[interval]/[cartId]/success/en.ftl create mode 100644 apps/payments/next/app/_lib/scripts/convert.ts delete mode 100644 apps/payments/next/public/l10n/README.md delete mode 100644 apps/payments/next/public/l10n/de-DE.ftl delete mode 100644 apps/payments/next/public/l10n/en-US.ftl delete mode 100644 apps/payments/next/public/l10n/es-ES.ftl delete mode 100644 apps/payments/next/public/l10n/fr-FR.ftl create mode 100644 libs/shared/l10n/src/client.ts delete mode 100644 libs/shared/l10n/src/lib/determine-direction.spec.ts delete mode 100644 libs/shared/l10n/src/lib/determine-direction.ts delete mode 100644 libs/shared/l10n/src/lib/determine-locale.spec.ts delete mode 100644 libs/shared/l10n/src/lib/determine-locale.ts rename libs/shared/l10n/src/lib/{other-languages.ts => l10n.constants.ts} (92%) rename libs/shared/l10n/src/lib/{formatters.spec.ts => l10n.formatters.spec.ts} (93%) rename libs/shared/l10n/src/lib/{formatters.ts => l10n.formatters.ts} (93%) delete mode 100644 libs/shared/l10n/src/lib/l10n.ts create mode 100644 libs/shared/l10n/src/lib/l10n.types.ts create mode 100644 libs/shared/l10n/src/lib/l10n.utils.spec.ts create mode 100644 libs/shared/l10n/src/lib/l10n.utils.ts delete mode 100644 libs/shared/l10n/src/lib/localize-timestamp.spec.ts delete mode 100644 libs/shared/l10n/src/lib/localize-timestamp.ts create mode 100644 libs/shared/l10n/src/lib/localizer/localizer.rsc.factory.spec.ts create mode 100644 libs/shared/l10n/src/lib/localizer/localizer.rsc.factory.ts create mode 100644 libs/shared/l10n/src/lib/localizer/localizer.rsc.spec.ts create mode 100644 libs/shared/l10n/src/lib/localizer/localizer.rsc.ts delete mode 100644 libs/shared/l10n/src/lib/localizer/localizer.server.spec.ts delete mode 100644 libs/shared/l10n/src/lib/localizer/localizer.server.ts delete mode 100644 libs/shared/l10n/src/lib/parse-accept-language.spec.ts delete mode 100644 libs/shared/l10n/src/lib/parse-accept-language.ts create mode 100644 libs/shared/l10n/tests/__mocks__/empty.ts create mode 100644 types/server-only/index.d.ts diff --git a/apps/payments/next/app/[locale]/[offeringId]/checkout/[interval]/[cartId]/en.ftl b/apps/payments/next/app/[locale]/[offeringId]/checkout/[interval]/[cartId]/en.ftl new file mode 100644 index 0000000000..5275f579dc --- /dev/null +++ b/apps/payments/next/app/[locale]/[offeringId]/checkout/[interval]/[cartId]/en.ftl @@ -0,0 +1,3 @@ +## Component - PaymentConsentCheckbox + +next-payment-confirm-with-legal-links-static-3 = I authorize { -brand-mozilla } to charge my payment method for the amount shown, according to Terms of Service and Privacy Notice, until I cancel my subscription. diff --git a/apps/payments/next/app/[locale]/[offeringId]/checkout/[interval]/[cartId]/error/en.ftl b/apps/payments/next/app/[locale]/[offeringId]/checkout/[interval]/[cartId]/error/en.ftl new file mode 100644 index 0000000000..a670a9f120 --- /dev/null +++ b/apps/payments/next/app/[locale]/[offeringId]/checkout/[interval]/[cartId]/error/en.ftl @@ -0,0 +1,4 @@ +next-payment-error-manage-subscription-button = Manage my subscription +next-iap-upgrade-contact-support = You can still get this product — please contact support so we can help you. +next-payment-error-retry-button = Try again +next-basic-error-message = Something went wrong. Please try again later. diff --git a/apps/payments/next/app/[locale]/[offeringId]/checkout/[interval]/[cartId]/error/page.tsx b/apps/payments/next/app/[locale]/[offeringId]/checkout/[interval]/[cartId]/error/page.tsx index 1825836098..82e524eb00 100644 --- a/apps/payments/next/app/[locale]/[offeringId]/checkout/[interval]/[cartId]/error/page.tsx +++ b/apps/payments/next/app/[locale]/[offeringId]/checkout/[interval]/[cartId]/error/page.tsx @@ -6,17 +6,35 @@ import { headers } from 'next/headers'; import Image from 'next/image'; import Link from 'next/link'; -import { getBundle, getLocaleFromRequest } from '@fxa/shared/l10n'; +import { DEFAULT_LOCALE } from '@fxa/shared/l10n'; import { getCartData } from '../../../../../../_lib/apiClient'; import errorIcon from '../../../../../../../images/error.svg'; +import { app } from '@fxa/payments/ui/server'; // forces dynamic rendering // https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config export const dynamic = 'force-dynamic'; -// Temporary code for demo purposes only - Replaced as part of FXA-8822 -const demoSupportedLanguages = ['en-US', 'fr-FR', 'es-ES', 'de-DE']; +const getErrorReason = (reason: string) => { + switch (reason) { + case 'iap_upgrade_contact_support': + return { + buttonFtl: 'next-payment-error-manage-subscription-button', + buttonLabel: 'Manage my subscription', + message: + 'You can still get this product — please contact support so we can help you.', + messageFtl: 'next-iap-upgrade-contact-support', + }; + default: + return { + buttonFtl: 'next-payment-error-retry-button', + buttonLabel: 'Try again', + message: 'Something went wrong. Please try again later.', + messageFtl: 'next-basic-error-message', + }; + } +}; interface CheckoutParams { cartId: string; @@ -30,39 +48,19 @@ export default async function CheckoutError({ }: { params: CheckoutParams; }) { - const headersList = headers(); - const locale = getLocaleFromRequest( - params, - headersList.get('accept-language'), - demoSupportedLanguages - ); + // Temporarily defaulting to `accept-language` + // This to be updated in FXA-9404 + //const locale = getLocaleFromRequest( + // params, + // headers().get('accept-language') + //); + const locale = headers().get('accept-language') || DEFAULT_LOCALE; - const cartData = getCartData(params.cartId); - const [cart] = await Promise.all([cartData]); - /* eslint-disable @typescript-eslint/no-unused-vars */ - // const cartService = await app.getCartService(); + const cartDataPromise = getCartData(params.cartId); + const l10nPromise = app.getL10n(locale); + const [cart, l10n] = await Promise.all([cartDataPromise, l10nPromise]); - const l10n = await getBundle([locale]); - - const getErrorReason = (reason: string) => { - switch (reason) { - case 'iap_upgrade_contact_support': - return { - buttonFtl: 'payment-error-manage-subscription-button', - buttonLabel: 'Manage my subscription', - message: - 'You can still get this product — please contact support so we can help you.', - messageFtl: 'iap-upgrade-contact-support', - }; - default: - return { - buttonFtl: 'payment-error-retry-button', - buttonLabel: 'Try again', - message: 'Something went wrong. Please try again later.', - messageFtl: 'basic-error-message', - }; - } - }; + const errorReason = getErrorReason(cart.errorReasonId); return ( <> @@ -72,19 +70,14 @@ export default async function CheckoutError({ >

- {l10n - .getMessage(getErrorReason(cart.errorReasonId).messageFtl) - ?.value?.toString() || getErrorReason(cart.errorReasonId).message} + {l10n.getString(errorReason.messageFtl, errorReason.message)}

- {l10n - .getMessage(getErrorReason(cart.errorReasonId).buttonFtl) - ?.value?.toString() || - getErrorReason(cart.errorReasonId).buttonLabel} + {l10n.getString(errorReason.buttonFtl, errorReason.buttonLabel)} diff --git a/apps/payments/next/app/[locale]/[offeringId]/checkout/[interval]/[cartId]/page.tsx b/apps/payments/next/app/[locale]/[offeringId]/checkout/[interval]/[cartId]/page.tsx index da32f91240..f59a9a903c 100644 --- a/apps/payments/next/app/[locale]/[offeringId]/checkout/[interval]/[cartId]/page.tsx +++ b/apps/payments/next/app/[locale]/[offeringId]/checkout/[interval]/[cartId]/page.tsx @@ -3,21 +3,136 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import { app } from '@fxa/payments/ui/server'; +import { DEFAULT_LOCALE } from '@fxa/shared/l10n'; import { auth, signIn, signOut } from 'apps/payments/next/auth'; +import { headers } from 'next/headers'; export const dynamic = 'force-dynamic'; export default async function Checkout() { - /* eslint-disable @typescript-eslint/no-unused-vars */ - const cartService = await app.getCartService(); - const session = await auth(); + // Temporarily defaulting to `accept-language` + // This to be updated in FXA-9404 + //const locale = getLocaleFromRequest( + // params, + // headers().get('accept-language') + //); + const locale = headers().get('accept-language') || DEFAULT_LOCALE; + const sessionPromise = auth(); + const l10nPromise = app.getL10n(locale); + const [session, l10n] = await Promise.all([sessionPromise, l10nPromise]); return ( <>
+
+
+

Temporary L10n Section

+

+ Temporary section to illustrate various translations using the + Localizer classes +

+
+
+

Regular translation - no variables

+

+ {l10n.getString('app-footer-mozilla-logo-label', 'testing2')} +

+
+
+

Regular translation - with variables

+

+ {l10n.getString( + 'app-page-title-2', + { title: 'Test Title' }, + 'testing2' + )} +

+
+
+

Regular translation - With Selector

+

+ {l10n.getString( + 'next-plan-price-interval-day', + { intervalCount: 2, amount: 20 }, + 'testing2' + )} +

+
+
+

Regular translation - With Currency

+

+ {l10n.getString( + 'list-positive-amount', + { + amount: l10n.getLocalizedCurrency(502, 'usd'), + }, + `${l10n.getLocalizedCurrencyString(502, 'usd')}` + )} +

+
+
+

Regular translation - With Date

+

+ {l10n.getString( + 'list-positive-amount', + { + amount: l10n.getLocalizedCurrency(502, 'usd'), + }, + `${l10n.getLocalizedCurrencyString(502, 'usd')}` + )} +

+
+
+

Get Fragment with Fallback element

+

+ {l10n.getFragmentWithSource( + 'next-payment-legal-link-stripe-3', + { + elems: { + stripePrivacyLink: ( + + Stripe privacy policy + + ), + }, + }, + Stripe privacy policy + )} +

+
+
+

Get Element - With reference

+

+ {l10n.getFragmentWithSource( + 'next-payment-confirm-with-legal-links-static-3', + { + elems: { + termsOfServiceLink: ( + + Stripe privacy policy + + ), + privacyNoticeLink: ( + + Stripe privacy policy + + ), + }, + }, + <> + I authorize Mozilla to charge my payment method for the amount + shown, according to{' '} + Terms of Service and{' '} + Privacy Notice, until I + cancel my subscription. + + )} +

+
+
{/* Temporary section to test NextAuth prompt/no prompt signin To be deleted as part of FXA-7521/FXA-7523 if not sooner where necessary diff --git a/apps/payments/next/app/[locale]/[offeringId]/checkout/[interval]/[cartId]/success/en.ftl b/apps/payments/next/app/[locale]/[offeringId]/checkout/[interval]/[cartId]/success/en.ftl new file mode 100644 index 0000000000..27aa757182 --- /dev/null +++ b/apps/payments/next/app/[locale]/[offeringId]/checkout/[interval]/[cartId]/success/en.ftl @@ -0,0 +1,21 @@ +next-payment-confirmation-thanks-heading = Thank you! + +# $email (String) - The user's email. +# $product_name (String) - The name of the subscribed product. +next-payment-confirmation-thanks-subheading = A confirmation email has been sent to { $email } with details on how to get started with { $product_name }. + +next-payment-confirmation-order-heading = Order details +# $invoiceNumber (String) - Invoice number of the successful payment +next-payment-confirmation-invoice-number = Invoice #{ $invoiceNumber } + +# $invoiceDate (Date) - Start date of the latest invoice +next-payment-confirmation-invoice-date = { $invoiceDate } + +next-payment-confirmation-details-heading-2 = Payment information +# $amount (Number) - The amount billed. It will be formatted as currency. +# $interval (String) - The interval between payments. +next-payment-confirmation-amount = { $amount } per { $interval } +# $last4 (Number) - Last four numbers of credit card +next-payment-confirmation-cc-card-ending-in = Card ending in { $last4 } + +next-payment-confirmation-download-button = Continue to download diff --git a/apps/payments/next/app/[locale]/[offeringId]/checkout/[interval]/[cartId]/success/page.tsx b/apps/payments/next/app/[locale]/[offeringId]/checkout/[interval]/[cartId]/success/page.tsx index 56f35299f8..6485841382 100644 --- a/apps/payments/next/app/[locale]/[offeringId]/checkout/[interval]/[cartId]/success/page.tsx +++ b/apps/payments/next/app/[locale]/[offeringId]/checkout/[interval]/[cartId]/success/page.tsx @@ -6,26 +6,17 @@ import { headers } from 'next/headers'; import Image from 'next/image'; import { formatPlanPricing } from '@fxa/payments/ui'; -import { - getBundle, - getFormattedMsg, - getLocaleFromRequest, - getLocalizedCurrency, - getLocalizedDate, - getLocalizedDateString, -} from '@fxa/shared/l10n'; +import { DEFAULT_LOCALE } from '@fxa/shared/l10n'; import { getCartData, getContentfulContent, } from '../../../../../../_lib/apiClient'; import circledConfirm from '../../../../../../../images/circled-confirm.svg'; +import { app } from '@fxa/payments/ui/server'; export const dynamic = 'force-dynamic'; -// Temporary code for demo purposes only - Replaced as part of FXA-8822 -const demoSupportedLanguages = ['en-US', 'fr-FR', 'es-ES', 'de-DE']; - type ConfirmationDetailProps = { title: string; detail1: string | Promise; @@ -60,26 +51,22 @@ export default async function CheckoutSuccess({ }: { params: CheckoutParams; }) { - const headersList = headers(); - const locale = getLocaleFromRequest( - params, - headersList.get('accept-language'), - demoSupportedLanguages - ); + // Temporarily defaulting to `accept-language` + // This to be updated in FXA-9404 + //const locale = getLocaleFromRequest( + // params, + // headers().get('accept-language') + //); + const locale = headers().get('accept-language') || DEFAULT_LOCALE; - const contentfulData = getContentfulContent(params.offeringId, locale); - const cartData = getCartData(params.cartId); - const [contentful, cart] = await Promise.all([contentfulData, cartData]); - /* eslint-disable @typescript-eslint/no-unused-vars */ - // const cartService = await app.getCartService(); - const date = cart.createdAt / 1000; - const planPrice = formatPlanPricing( - cart.nextInvoice.totalAmount, - cart.nextInvoice.currency, - cart.interval - ); - - const l10n = await getBundle([locale]); + const contentfulDataPromise = getContentfulContent(params.offeringId, locale); + const cartDataPromise = getCartData(params.cartId); + const l10nPromise = app.getL10n(locale); + const [contentful, cart, l10n] = await Promise.all([ + contentfulDataPromise, + cartDataPromise, + l10nPromise, + ]); return ( <> @@ -88,73 +75,71 @@ export default async function CheckoutSuccess({

- {l10n - .getMessage('payment-confirmation-thanks-heading') - ?.value?.toString() || 'Thank you!'} + {l10n.getString( + 'next-payment-confirmation-thanks-heading', + 'Thank you!' + )}

- {getFormattedMsg( - l10n, - 'payment-confirmation-thanks-subheading', - `A confirmation email has been sent to ${cart.email} with details on how to get started with ${contentful.purchaseDetails.productName}.`, + {l10n.getString( + 'next-payment-confirmation-thanks-subheading', { email: cart.email, product_name: contentful.purchaseDetails.productName, - } + }, + `A confirmation email has been sent to ${cart.email} with details on how to get started with ${contentful.purchaseDetails.productName}.` )}

@@ -163,10 +148,10 @@ export default async function CheckoutSuccess({ href={contentful.commonContent.successActionButtonUrl} > {contentful.commonContent.successActionButtonLabel || - l10n - .getMessage('payment-confirmation-download-button ') - ?.value?.toString() || - 'Continue to download'} + l10n.getString( + 'next-payment-confirmation-download-button', + 'Continue to download' + )}
diff --git a/apps/payments/next/app/[locale]/[offeringId]/checkout/layout.tsx b/apps/payments/next/app/[locale]/[offeringId]/checkout/layout.tsx index 325ecc7620..ae451459dc 100644 --- a/apps/payments/next/app/[locale]/[offeringId]/checkout/layout.tsx +++ b/apps/payments/next/app/[locale]/[offeringId]/checkout/layout.tsx @@ -1,13 +1,11 @@ /* 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 { headers } from 'next/headers'; -import { PurchaseDetails, TermsAndPrivacy } from '@fxa/payments/ui/server'; -import { getLocaleFromRequest } from '@fxa/shared/l10n'; - +import { PurchaseDetails, TermsAndPrivacy, app } from '@fxa/payments/ui/server'; import { getCartData, getContentfulContent } from '../../../_lib/apiClient'; +import { DEFAULT_LOCALE } from '@fxa/shared/l10n'; // TODO - Replace these placeholders as part of FXA-8227 export const metadata = { @@ -34,14 +32,21 @@ export default async function RootLayout({ children: React.ReactNode; params: CheckoutParams; }) { - const headersList = headers(); - const locale = getLocaleFromRequest( - params, - headersList.get('accept-language') - ); - const contentfulData = getContentfulContent(params.offeringId, locale); - const cartData = getCartData(params.cartId); - const [contentful, cart] = await Promise.all([contentfulData, cartData]); + // Temporarily defaulting to `accept-language` + // This to be updated in FXA-9404 + //const locale = getLocaleFromRequest( + // params, + // headers().get('accept-language') + //); + const locale = headers().get('accept-language') || DEFAULT_LOCALE; + const contentfulDataPromise = getContentfulContent(params.offeringId, locale); + const cartDataPromise = getCartData(params.cartId); + const l10nPromise = app.getL10n(locale); + const [contentful, cart, l10n] = await Promise.all([ + contentfulDataPromise, + cartDataPromise, + l10nPromise, + ]); return ( <> @@ -51,8 +56,8 @@ export default async function RootLayout({
@@ -61,7 +66,7 @@ export default async function RootLayout({
{children} { + console.error(err); + process.exit(1); + }) + .then((result) => process.exit(result)); +} diff --git a/apps/payments/next/project.json b/apps/payments/next/project.json index ce2cd39979..0ab653dfb3 100644 --- a/apps/payments/next/project.json +++ b/apps/payments/next/project.json @@ -81,7 +81,7 @@ "command": "pm2 delete apps/payments/next/pm2.config.js" }, "l10n-merge": { - "dependsOn": ["l10n-prime"], + "dependsOn": ["l10n-convert"], "command": "yarn grunt --gruntfile='apps/payments/next/Gruntfile.js' merge-ftl" }, "l10n-prime": { @@ -94,6 +94,10 @@ "watch-ftl": { "command": "yarn grunt --gruntfile='apps/payments/next/Gruntfile.js' watch-ftl" }, + "l10n-convert": { + "dependsOn": ["l10n-prime"], + "command": "node -r esbuild-register apps/payments/next/app/_lib/scripts/convert.ts" + }, "storybook": { "executor": "@nx/storybook:storybook", "options": { diff --git a/apps/payments/next/public/l10n/README.md b/apps/payments/next/public/l10n/README.md deleted file mode 100644 index 60564771b2..0000000000 --- a/apps/payments/next/public/l10n/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# Temporary - -This is a temporary location for translation files, just to provide limited functionality for the demo components. - -The localization approach will be built out in future work diff --git a/apps/payments/next/public/l10n/de-DE.ftl b/apps/payments/next/public/l10n/de-DE.ftl deleted file mode 100644 index fd6647b5c2..0000000000 --- a/apps/payments/next/public/l10n/de-DE.ftl +++ /dev/null @@ -1,39 +0,0 @@ -## Component - PlanDetails - -next-plan-details-header = Produktdetails -next-plan-details-list-price = Listenpreis -next-plan-details-show-button = Details anzeigen -next-plan-details-hide-button = Details ausblenden -next-plan-details-total-label = Gesamt -next-plan-details-tax = Steuern und Gebühren - -next-terms = Nutzungsbedingungen -next-privacy = Datenschutzhinweis -next-terms-download = Nutzungsbedingungen herunterladen - -payment-confirmation-thanks-heading = Vielen Dank! - -# $email (string) - The user's email. -# $productName (String) - The name of the subscribed product. -payment-confirmation-thanks-subheading = An { $email } wurde eine Bestätigungs-E-Mail mit Details zu den ersten Schritten mit { $product_name } gesendet. - -payment-confirmation-order-heading = Bestelldetails -payment-confirmation-invoice-number = Rechnung #{ $invoiceNumber } -# $invoiceDate (Date) - Start date of the latest invoice -payment-confirmation-invoice-date = { $invoiceDate } - -payment-confirmation-details-heading-2 = Zahlungsinformationen -payment-confirmation-amount = { $amount } pro { $interval } -payment-confirmation-cc-card-ending-in = Karte endet auf { $last4 } - -payment-confirmation-download-button = Weiter zum Download - -subscription-success-title = Abonnementbestätigung -subscription-error-title = Fehler beim Bestätigen des Abonnements… -iap-upgrade-contact-support = Sie können dieses Produkt weiterhin erhalten – wenden Sie sich bitte an den Support, damit wir Ihnen helfen können. -payment-error-manage-subscription-button = Mein Abonnement verwalten -basic-error-message = Etwas ist schiefgegangen. Bitte versuchen Sie es später erneut. -payment-error-manage-subscription-button = Mein Abonnement verwalten -payment-error-retry-button = Erneut versuchen - -sub-guarantee = 30 Tage Geld-zurück-Garantie diff --git a/apps/payments/next/public/l10n/en-US.ftl b/apps/payments/next/public/l10n/en-US.ftl deleted file mode 100644 index 1e18f8c074..0000000000 --- a/apps/payments/next/public/l10n/en-US.ftl +++ /dev/null @@ -1,56 +0,0 @@ -## Component - PlanDetails - -next-plan-details-header = Product details -next-plan-details-list-price = List Price -next-plan-details-show-button = Show details -next-plan-details-hide-button = Hide details -next-plan-details-total-label = Total -next-plan-details-tax = Taxes and Fees - -## Subscription upgrade plan details - shared by multiple components, including plan details and payment form -## $amount (Number) - The amount billed. It will be formatted as currency. - -# $intervalCount (Number) - The interval between payments, in days. -plan-price-interval-daily = { $amount } daily -# $intervalCount (Number) - The interval between payments, in weeks. -plan-price-interval-weekly = { $amount } weekly -# $intervalCount (Number) - The interval between payments, in months. -plan-price-interval-monthly = { $amount } monthly -# $intervalCount (Number) - The interval between payments every 6 months. -plan-price-interval-6monthly = { $amount } every 6 months -# $intervalCount (Number) - The interval between payments, in years. -plan-price-interval-yearly = { $amount } yearly - -list-positive-amount = { $amount } -list-negative-amount = - { $amount } - -next-terms = Terms of Service -next-privacy = Privacy Notice -next-terms-download = Download Terms - -payment-confirmation-thanks-heading = Thank you! - -# $email (string) - The user's email. -# $productName (String) - The name of the subscribed product. -payment-confirmation-thanks-subheading = A confirmation email has been sent to { $email } with details on how to get started with { $product_name }. - -payment-confirmation-order-heading = Order details -payment-confirmation-invoice-number = Invoice #{ $invoiceNumber } - -# $invoiceDate (Date) - Start date of the latest invoice -payment-confirmation-invoice-date = { $invoiceDate } - -payment-confirmation-details-heading-2 = Payment information -payment-confirmation-amount = { $amount } per { $interval } -payment-confirmation-cc-card-ending-in = Card ending in { $last4 } - -payment-confirmation-download-button = Continue to download - -subscription-success-title = Subscription confirmation -subscription-error-title = Error confirming subscription… -iap-upgrade-contact-support = You can still get this product — please contact support so we can help you. -payment-error-manage-subscription-button = Manage my subscription -basic-error-message = Something went wrong. Please try again later. -payment-error-retry-button = Try again - -sub-guarantee = 30-day money-back guarantee diff --git a/apps/payments/next/public/l10n/es-ES.ftl b/apps/payments/next/public/l10n/es-ES.ftl deleted file mode 100644 index dc735fcb30..0000000000 --- a/apps/payments/next/public/l10n/es-ES.ftl +++ /dev/null @@ -1,82 +0,0 @@ -## Component - PlanDetails - -next-plan-details-header = Detalles del producto -next-plan-details-list-price = Lista de precios -next-plan-details-show-button = Mostrar detalles -next-plan-details-hide-button = Ocultar detalles -next-plan-details-total-label = Total -next-plan-details-tax = Impuestos y tasas - -plan-price-interval-day = - { $intervalCount -> - [one] { $amount } diariamente - *[other] { $amount } cada { $intervalCount } días - } - .title = - { $intervalCount -> - [one] { $amount } diariamente - *[other] { $amount } cada { $intervalCount } días - } -# $intervalCount (Number) - The interval between payments, in weeks. -plan-price-interval-week = - { $intervalCount -> - [one] { $amount } semanalmente - *[other] { $amount } cada { $intervalCount } semanas - } - .title = - { $intervalCount -> - [one] { $amount } semanalmente - *[other] { $amount } cada { $intervalCount } semanas - } -# $intervalCount (Number) - The interval between payments, in months. -plan-price-interval-month = - { $intervalCount -> - [one] { $amount } mensualmente - *[other] { $amount } cada { $intervalCount } meses - } - .title = - { $intervalCount -> - [one] { $amount } mensualmente - *[other] { $amount } cada { $intervalCount } meses - } -# $intervalCount (Number) - The interval between payments, in years. -plan-price-interval-year = - { $intervalCount -> - [one] { $amount } anualmente - *[other] { $amount } cada { $intervalCount } años - } - .title = - { $intervalCount -> - [one] { $amount } anualmente - *[other] { $amount } cada { $intervalCount } años - } - -next-terms = Términos del servicio -next-privacy = Aviso de privacidad -next-terms-download = Descargar términos - -payment-confirmation-thanks-heading = ¡Gracias! - -# $email (string) - The user's email. -# $productName (String) - The name of the subscribed product. -payment-confirmation-thanks-subheading = Se ha enviado un correo electrónico de confirmación a { $email } con detalles sobre cómo comenzar a usar { $product_name }. - -payment-confirmation-order-heading = Detalles del pedido -payment-confirmation-invoice-number = Factura #{ $invoiceNumber } -# $invoiceDate (Date) - Start date of the latest invoice -payment-confirmation-invoice-date = { $invoiceDate } - -payment-confirmation-details-heading-2 = Información de pago -payment-confirmation-amount = { $amount } por { $interval } -payment-confirmation-cc-card-ending-in = Tarjeta que termina en { $last4 } - -payment-confirmation-download-button = Continuar para descargar - -subscription-success-title = Confirmación de la suscripción -subscription-error-title = Error al confirmar la suscripción… -iap-upgrade-contact-support = Todavía puedes obtener este producto — por favor contacta con el equipo de soporte para que podamos ayudarte. -payment-error-manage-subscription-button = Administrar mi suscripción -basic-error-message = Algo ha salido mal. Por favor, inténtalo de nuevo más tarde. -payment-error-retry-button = Volver a intentarlo - -sub-guarantee = 30 días de garantía de devolución de dinero diff --git a/apps/payments/next/public/l10n/fr-FR.ftl b/apps/payments/next/public/l10n/fr-FR.ftl deleted file mode 100644 index 522bbfc10d..0000000000 --- a/apps/payments/next/public/l10n/fr-FR.ftl +++ /dev/null @@ -1,55 +0,0 @@ -## Component - PlanDetails - -next-plan-details-header = Détails du produit -next-plan-details-list-price = Prix courant -next-plan-details-show-button = Afficher les détails -next-plan-details-hide-button = Masquer les détails -next-plan-details-total-label = Total -next-plan-details-tax = Taxes et frais - -## Subscription upgrade plan details - shared by multiple components, including plan details and payment form -## $amount (Number) - The amount billed. It will be formatted as currency. - -# $intervalCount (Number) - The interval between payments, in days. -plan-price-interval-daily = { $amount } par jour -# $intervalCount (Number) - The interval between payments, in weeks. -plan-price-interval-weekly = { $amount } par semaine -# $intervalCount (Number) - The interval between payments, in months. -plan-price-interval-monthly = { $amount } par mois -# $intervalCount (Number) - The interval between payments every 6 months. -plan-price-interval-6monthly = { $amount } tous les 6 mois -# $intervalCount (Number) - The interval between payments, in years. -plan-price-interval-yearly = { $amount } par an - -list-positive-amount = { $amount } -list-negative-amount = - { $amount } - -next-terms = Conditions d’utilisation -next-privacy = Politique de confidentialité -next-terms-download = Télécharger les conditions - -payment-confirmation-thanks-heading = Merci ! - -# $email (string) - The user's email. -# $productName (String) - The name of the subscribed product. -payment-confirmation-thanks-subheading = Un e-mail de confirmation a été envoyé à { $email } avec les détails nécessaires pour savoir comment démarrer avec { $product_name }. - -payment-confirmation-order-heading = Détails de la commande -payment-confirmation-invoice-number = Facture n°{ $invoiceNumber } -# $invoiceDate (Date) - Start date of the latest invoice -payment-confirmation-invoice-date = { $invoiceDate } - -payment-confirmation-details-heading-2 = Informations de paiement -payment-confirmation-amount = { $amount } par { $interval } -payment-confirmation-cc-card-ending-in = Carte se terminant par { $last4 } - -payment-confirmation-download-button = Continuer vers le téléchargement - -subscription-success-title = Confirmation d’abonnement -subscription-error-title = Erreur lors de la confirmation de l’abonnement… -iap-upgrade-contact-support = Vous pouvez tout de même obtenir ce produit ; veuillez contacter notre équipe d’assistance afin que nous puissions vous aider. -payment-error-manage-subscription-button = Gérer mon abonnement -basic-error-message = Une erreur est survenue. Merci de réessayer plus tard. -payment-error-retry-button = Veuillez réessayer - -sub-guarantee = Garantie de remboursement de 30 jours diff --git a/libs/payments/ui/src/lib/nestapp/app.module.ts b/libs/payments/ui/src/lib/nestapp/app.module.ts index 58795e6452..4cf985fa97 100644 --- a/libs/payments/ui/src/lib/nestapp/app.module.ts +++ b/libs/payments/ui/src/lib/nestapp/app.module.ts @@ -9,7 +9,7 @@ import { AccountDatabaseNestFactory } from '@fxa/shared/db/mysql/account'; import { Module } from '@nestjs/common'; import { RootConfig } from './config'; -import { LocalizerServerFactory } from '@fxa/shared/l10n/server'; +import { LocalizerRscFactoryProvider } from '@fxa/shared/l10n/server'; @Module({ imports: [ @@ -32,7 +32,7 @@ import { LocalizerServerFactory } from '@fxa/shared/l10n/server'; AccountDatabaseNestFactory, CartService, CartManager, - LocalizerServerFactory, + LocalizerRscFactoryProvider, ], }) export class AppModule {} diff --git a/libs/payments/ui/src/lib/nestapp/app.ts b/libs/payments/ui/src/lib/nestapp/app.ts index aecc841c7a..34549e2a2b 100644 --- a/libs/payments/ui/src/lib/nestapp/app.ts +++ b/libs/payments/ui/src/lib/nestapp/app.ts @@ -8,7 +8,7 @@ import { CartService } from '@fxa/payments/cart'; import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; -import { LocalizerServer } from '@fxa/shared/l10n/server'; +import { LocalizerRscFactory } from '@fxa/shared/l10n/server'; import { singleton } from '../utils/singleton'; class AppSingleton { @@ -22,10 +22,11 @@ class AppSingleton { } } - async getLocalizerServer() { + async getL10n(acceptLanguage: string) { // Temporary until Next.js canary lands await this.initialize(); - return this.app.get(LocalizerServer); + const localizerRscFactory = this.app.get(LocalizerRscFactory); + return localizerRscFactory.createLocalizerRsc(acceptLanguage); } async getCartService() { diff --git a/libs/payments/ui/src/lib/server/purchase-details.tsx b/libs/payments/ui/src/lib/server/purchase-details.tsx index 4f263ca88a..6128a1346c 100644 --- a/libs/payments/ui/src/lib/server/purchase-details.tsx +++ b/libs/payments/ui/src/lib/server/purchase-details.tsx @@ -3,55 +3,45 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import { Invoice } from '@fxa/payments/cart'; -import { - getFormattedMsg, - getLocalizedCurrency, - getLocalizedCurrencyString, -} from '@fxa/shared/l10n'; -import { FluentBundle } from '@fluent/bundle'; import Image from 'next/image'; import { formatPlanPricing } from '../utils/helpers'; +import { LocalizerRsc } from '@fxa/shared/l10n/server'; import '../../styles/index.css'; -import { app } from '@fxa/payments/ui/server'; type ListLabelItemProps = { labelLocalizationId: string; labelFallbackText: string; amount: number; currency: string; - l10n: FluentBundle; + l10n: LocalizerRsc; positiveAmount?: boolean; }; - export const ListLabelItem = ({ + l10n, labelLocalizationId, labelFallbackText, amount, currency, - l10n, positiveAmount = true, }: ListLabelItemProps) => { return (
  • - {l10n.getMessage(labelLocalizationId)?.value?.toString() || - labelFallbackText} + {l10n.getString(labelLocalizationId, labelFallbackText)}
    {positiveAmount - ? getFormattedMsg( - l10n, + ? l10n.getString( `list-positive-amount`, - `${getLocalizedCurrencyString(amount, currency)}`, { - amount: getLocalizedCurrency(amount, currency), - } + amount: l10n.getLocalizedCurrency(amount, currency), + }, + `${l10n.getLocalizedCurrencyString(amount, currency)}` ) - : getFormattedMsg( - l10n, + : l10n.getString( `list-negative-amount`, - `- ${getLocalizedCurrencyString(amount, currency)}`, { - amount: getLocalizedCurrency(amount, currency), - } + amount: l10n.getLocalizedCurrency(amount, currency), + }, + `- ${l10n.getLocalizedCurrencyString(amount, currency)}` )}
  • @@ -59,8 +49,8 @@ export const ListLabelItem = ({ }; type PurchaseDetailsProps = { + l10n: LocalizerRsc; interval: string; - locale: string; invoice: Invoice; purchaseDetails: { details: string[]; @@ -71,7 +61,7 @@ type PurchaseDetailsProps = { }; export async function PurchaseDetails(props: PurchaseDetailsProps) { - const { purchaseDetails, invoice, interval } = props; + const { purchaseDetails, invoice, interval, l10n } = props; const { currency, listAmount, discountAmount, totalAmount, taxAmounts } = invoice; const { details, subtitle, productName, webIcon } = purchaseDetails; @@ -79,15 +69,6 @@ export async function PurchaseDetails(props: PurchaseDetailsProps) { (taxAmount) => !taxAmount.inclusive ); - // TODO - // Move to instantiation on start up. Ideally getBundle's, generateBundle, is only called once at startup, - // and then that instance is used for all requests. - // Approach 1 (Experimental): https://nextjs.org/docs/app/building-your-application/optimizing/instrumentation - // Approach 2 (Node global): https://github.com/vercel/next.js/blob/canary/examples/with-knex/knex/index.js#L13 - //const l10n = await getBundle([props.locale]); - const localizer = await app.getLocalizerServer(); - const l10n = localizer.getBundle(props.locale); - return (
    @@ -109,13 +90,12 @@ export async function PurchaseDetails(props: PurchaseDetailsProps) {

    - {getFormattedMsg( - l10n, + {l10n.getString( `plan-price-interval-${interval}`, - formatPlanPricing(listAmount, currency, interval), { - amount: getLocalizedCurrency(listAmount, currency), - } + amount: l10n.getLocalizedCurrency(listAmount, currency), + }, + formatPlanPricing(listAmount, currency, interval) )}  •  {subtitle} @@ -124,8 +104,7 @@ export async function PurchaseDetails(props: PurchaseDetailsProps) {

    - {l10n.getMessage('next-plan-details-header')?.value?.toString() || - `Plan Details`} + {l10n.getString('next-plan-details-header', 'Plan Details')}

      @@ -157,7 +136,7 @@ export async function PurchaseDetails(props: PurchaseDetailsProps) { amount: discountAmount, currency, l10n, - subtractValue: true, + positiveAmount: false, }} /> )} @@ -175,7 +154,7 @@ export async function PurchaseDetails(props: PurchaseDetailsProps) { )} {exclusiveTaxRates.length > 1 && - exclusiveTaxRates.map((taxRate, idx) => ( + exclusiveTaxRates.map((taxRate) => ( - {l10n - .getMessage('next-plan-details-total-label') - ?.value?.toString() || `Total`} + {l10n.getString('next-plan-details-total-label', 'Total')} - {getFormattedMsg( - l10n, + {l10n.getString( `plan-price-interval-${interval}`, - formatPlanPricing(totalAmount, currency, interval), { - amount: getLocalizedCurrency(totalAmount, currency), - } + amount: l10n.getLocalizedCurrency(totalAmount, currency), + }, + formatPlanPricing(totalAmount, currency, interval) )}
    diff --git a/libs/payments/ui/src/lib/server/terms-and-privacy.tsx b/libs/payments/ui/src/lib/server/terms-and-privacy.tsx index 09557ac4e5..8d36e331e0 100644 --- a/libs/payments/ui/src/lib/server/terms-and-privacy.tsx +++ b/libs/payments/ui/src/lib/server/terms-and-privacy.tsx @@ -2,8 +2,6 @@ * 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 } from '@fluent/bundle'; -import { getBundle } from '@fxa/shared/l10n'; import { GenericTermItem, GenericTermsListItem, @@ -12,6 +10,7 @@ import { buildPaymentTerms, buildProductTerms, } from '../utils/terms-and-privacy'; +import { LocalizerRsc } from '@fxa/shared/l10n/server'; const CONTENT_SERVER_URL = 'https://accounts.stage.mozaws.net'; // TODO - Get from config once FXA-7503 lands @@ -20,7 +19,7 @@ type GenericTermsProps = { titleId: string; titleLocalizationId: string; items: GenericTermsListItem[]; - l10n: FluentBundle; + l10n: LocalizerRsc; }; function GenericTerms({ @@ -37,7 +36,7 @@ function GenericTerms({ aria-labelledby={titleId} >

    - {l10n.getMessage(titleLocalizationId)?.value?.toString() || title} + {l10n.getString(titleLocalizationId, title)}

      @@ -50,8 +49,7 @@ function GenericTerms({ rel="noreferrer" className="text-blue-500 underline" > - {l10n.getMessage(item.localizationId)?.value?.toString() || - item.text} + {l10n.getString(item.localizationId, item.text)} Opens in new window @@ -62,7 +60,7 @@ function GenericTerms({ } export interface TermsAndPrivacyProps { - locale: string; + l10n: LocalizerRsc; paymentProvider?: PaymentProvider; productName: string; termsOfServiceUrl: string; @@ -72,7 +70,7 @@ export interface TermsAndPrivacyProps { } export async function TermsAndPrivacy({ - locale, + l10n, paymentProvider, productName, termsOfServiceUrl, @@ -93,13 +91,6 @@ export async function TermsAndPrivacy({ ), ]; - // TODO - // Move to instantiation on start up. Ideally getBundle's, generateBundle, is only called once at startup, - // and then that instance is used for all requests. - // Approach 1 (Experimental): https://nextjs.org/docs/app/building-your-application/optimizing/instrumentation - // Approach 2 (Node global): https://github.com/vercel/next.js/blob/canary/examples/with-knex/knex/index.js#L13 - const l10n = await getBundle([locale]); - return (