From ec3d744890aa117d9f212306c27ad336d28c31ce Mon Sep 17 00:00:00 2001 From: Alano Terblanche <18033717+Benehiko@users.noreply.github.com> Date: Tue, 10 Oct 2023 17:52:57 +0200 Subject: [PATCH] feat: allow custom languages in `IntlProvider` --- README.md | 53 ++++ examples/nextjs-spa/src/components/layout.tsx | 43 +++- examples/preact-spa/src/main.tsx | 42 +++- examples/react-spa/src/Login.tsx | 1 - examples/react-spa/src/Recovery.tsx | 1 - examples/react-spa/src/Registration.tsx | 1 - examples/react-spa/src/main.tsx | 43 +++- src/react-components/provider.tsx | 227 ++++++++++++++++-- 8 files changed, 386 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index f6fc60e7f..6159fb726 100644 --- a/README.md +++ b/README.md @@ -82,6 +82,59 @@ have explicitly told our React app to use through the `VITE_ORY_SDK_URL` export. Now you can see Ory Elements in action by opening in your browser! +## Adding Translations + +Ory Elements supports translations out-of-the-box with the `IntlProvider`. The +`IntlProvider` is required by Ory Elements so that the default languages can be +mapped correctly, specifically English. + +The `IntlProvider` has the ability to accept custom translations through a +`CustomLanguageFormats` object. You can specify to the `` that you +would like to use a `CustomTranslations` type instead of the +`SupportedLanguages` which will require providing the `customTranslations` prop. + +When providing a language, it is important to note that it will be merged with +an existing supported language, with your provided values taking precedent. This +is to reduce the work needed to get up and running and provide the ability to +just modify one key from an already supported language, rather than modifying +the entire translation file :) + +For example, I want to adjust the English translation to say `Email` instead of +`ID` when a Login card is shown. So I provide the key-value pair +`"identities.messages.1070004": "Email"`. By Default this value is `ID`. Ory +Elements will now use the updated value `Email` instead of `ID` for this +specific label, but will still keep the other defaults in-tact. + +Another scenario is when we add partial keys to an unsupported language such as +`af` (Afrikaans). I add my key-value only for one entry +`"identities.messages.1070004": "E-posadres"`, however, the language has no +default inside Ory Elements. As a safe-guard we fall-back to English for the +rest of the labels. + +```tsx +import { ThemeProvider, IntlProvider, CustomTranslations } from "@ory/elements" + +const RootComponent = () => { + const myCustomTranslations: CustomLanguageFormats = { + en: { + "login.title": "Login", + }, + } + + return ( + + + customTranslations={myCustomTranslations} + locale="en" + defaultLocale="en" + > + // children + + + ) +} +``` + ## End-to-end Testing with Playwright Ory Elements provides an end-to-end library based on diff --git a/examples/nextjs-spa/src/components/layout.tsx b/examples/nextjs-spa/src/components/layout.tsx index 98f85ffc4..ae82a8ca2 100644 --- a/examples/nextjs-spa/src/components/layout.tsx +++ b/examples/nextjs-spa/src/components/layout.tsx @@ -1,4 +1,10 @@ -import { IntlProvider, Nav, ThemeProvider } from "@ory/elements" +import { + CustomLanguageFormats, + CustomTranslations, + IntlProvider, + Nav, + ThemeProvider, +} from "@ory/elements" import Head from "next/head" interface LayoutProps { @@ -6,9 +12,42 @@ interface LayoutProps { } export default function Layout({ children }: LayoutProps) { + // adds custom translations labels to the default translations + // this merges the custom translations with the default translations + // if a custom language is provided, but no standard translation + // exists, the english translation will be merged instead for missing values. + // + // For example, if you provide a custom translation for the "login.title" label + // in the "af" language (Afrikaans), but no standard translation exists for "af", + // the english translation will be used for the remaining labels. + // + // You can also contribute your custom translations to the Ory Elements project + // by submitting a pull request to the following repository: + // https://github.com/ory/elements + const customTranslations: CustomLanguageFormats = { + en: { + "login.title": "Login", + "identities.messages.1070004": "Email", + }, + nl: { + "login.title": "Inloggen", + "identities.messages.1070004": "E-mail", + }, + af: { + "login.title": "Meld aan", + "identities.messages.1070004": "E-posadres", + }, + } return ( - + {/* We dont need to pass any custom translations */} + {/* */} + {/* We pass custom translations */} + + customTranslations={customTranslations} + locale="af" + defaultLocale="en" + > Next.js w/ Elements diff --git a/examples/preact-spa/src/main.tsx b/examples/preact-spa/src/main.tsx index 895f93a66..8e785e918 100644 --- a/examples/preact-spa/src/main.tsx +++ b/examples/preact-spa/src/main.tsx @@ -11,7 +11,12 @@ import { Recovery } from "./recovery" import { Register } from "./register" import { Settings } from "./settings" import { Verification } from "./verification" -import { IntlProvider, ThemeProvider } from "@ory/elements-preact" +import { + CustomLanguageFormats, + CustomTranslations, + IntlProvider, + ThemeProvider, +} from "@ory/elements-preact" // Ory Elements // optional fontawesome icons @@ -27,9 +32,42 @@ import "@ory/elements-preact/assets/jetbrains-mono-font.css" import "@ory/elements-preact/style.css" const Main = () => { + // adds custom translations labels to the default translations + // this merges the custom translations with the default translations + // if a custom language is provided, but no standard translation + // exists, the english translation will be merged instead for missing values. + // + // For example, if you provide a custom translation for the "login.title" label + // in the "af" language (Afrikaans), but no standard translation exists for "af", + // the english translation will be used for the remaining labels. + // + // You can also contribute your custom translations to the Ory Elements project + // by submitting a pull request to the following repository: + // https://github.com/ory/elements + const customTranslations: CustomLanguageFormats = { + en: { + "login.title": "Login", + "identities.messages.1070004": "Email", + }, + nl: { + "login.title": "Inloggen", + "identities.messages.1070004": "E-mail", + }, + af: { + "login.title": "Meld aan", + "identities.messages.1070004": "E-posadres", + }, + } return ( - + {/* We dont need to pass any custom translations */} + {/* */} + {/* We pass custom translations */} + + customTranslations={customTranslations} + locale="af" + defaultLocale="en" + > diff --git a/examples/react-spa/src/Login.tsx b/examples/react-spa/src/Login.tsx index 9da5e0ff0..34f3c19a9 100644 --- a/examples/react-spa/src/Login.tsx +++ b/examples/react-spa/src/Login.tsx @@ -107,7 +107,6 @@ export const Login = (): JSX.Element => { return flow ? ( // we render the login form using Ory Elements { return flow ? ( // We create a dynamic Recovery form based on the flow using Ory Elements { return flow ? ( // create a registration form that dynamically renders based on the flow data using Ory Elements {/* We add the Ory themes here */} - + {/* We dont need to pass any custom translations */} + {/* */} + {/* We pass custom translations */} + + locale="af" + defaultLocale="en" + customTranslations={customTranslations} + > } /> } /> diff --git a/src/react-components/provider.tsx b/src/react-components/provider.tsx index 3b3d4073b..d4244159a 100644 --- a/src/react-components/provider.tsx +++ b/src/react-components/provider.tsx @@ -9,6 +9,7 @@ import { themeProviderStyle, } from "../theme/theme-provider.css" import * as locales from "./../locales" +import { merge } from "lodash" export interface ThemeProviderProps { theme?: "light" | "dark" @@ -37,20 +38,214 @@ export const ThemeProvider = ({ ) -export const IntlProvider = ({ - locale = "en", +export type TranslationFile = { + [K in keyof typeof locales.en]: string +} + +export const CountryCodes = [ + "ab", + "aa", + "af", + "sq", + "am", + "ar", + "hy", + "as", + "ay", + "az", + "ba", + "eu", + "bn", + "dz", + "bh", + "bi", + "br", + "bg", + "my", + "be", + "km", + "ca", + "zh", + "co", + "hr", + "cs", + "da", + "nl", + "en", + "eo", + "et", + "fo", + "fj", + "fi", + "fr", + "fy", + "gd", + "gl", + "ka", + "de", + "el", + "kl", + "gn", + "gu", + "ha", + "iw", + "hi", + "hu", + "is", + "in", + "ia", + "ie", + "ik", + "ga", + "it", + "ja", + "jw", + "kn", + "ks", + "kk", + "rw", + "ky", + "rn", + "ko", + "ku", + "lo", + "la", + "lv", + "ln", + "lt", + "mk", + "mg", + "ms", + "ml", + "mt", + "mi", + "mr", + "mo", + "mn", + "na", + "ne", + "no", + "oc", + "or", + "om", + "ps", + "fa", + "pl", + "pt", + "pa", + "qu", + "rm", + "ro", + "ru", + "sm", + "sg", + "sa", + "sr", + "sh", + "st", + "tn", + "sn", + "sd", + "si", + "ss", + "sk", + "sl", + "so", + "es", + "su", + "sw", + "sv", + "tl", + "tg", + "ta", + "tt", + "te", + "th", + "bo", + "ti", + "to", + "ts", + "tr", + "tk", + "tw", + "uk", + "ur", + "uz", + "vi", + "vo", + "cy", + "wo", + "xh", + "ji", + "yo", + "zu", +] as const + +export type CustomLanguageFormats = { + [k in (typeof CountryCodes)[number]]?: Partial +} + +export interface CustomTranslations { + customTranslations: Partial + locale?: (typeof CountryCodes)[number] + defaultLocale?: (typeof CountryCodes)[number] +} + +const isCustomTranslations = (o: unknown): o is CustomTranslations => { + return (o as CustomTranslations).customTranslations !== undefined +} + +export type SupportedLanguageFormats = { + [k in keyof typeof locales]: TranslationFile +} + +type locale = keyof typeof locales + +export interface SupportedTranslations { + locale?: locale + defaultLocale?: locale +} + +export type IntlProviderProps = Type extends CustomTranslations + ? PropsWithChildren + : PropsWithChildren + +export const IntlProvider = < + T extends SupportedTranslations | CustomTranslations = SupportedTranslations, +>({ children, -}: PropsWithChildren<{ - locale?: keyof typeof locales -}>) => ( - {chunks}, - }} - > - {children} - -) + ...props +}: IntlProviderProps) => { + let translation = locales.en + + if (props.locale && props.locale in locales) { + translation = locales[props.locale as locale] + } + + const intlProps = isCustomTranslations(props) + ? { + locale: props.locale ?? "en", + defaultLocale: props.defaultLocale, + messages: merge( + {}, + translation, + props.customTranslations[props.locale ?? "en"], + ), + } + : { + locale: props.locale ?? "en", + defaultLocale: props.defaultLocale, + messages: translation, + } + + return ( + {chunks}, + }} + > + {children} + + ) +}