diff --git a/README.md b/README.md index f6fc60e7f..1765a88de 100644 --- a/README.md +++ b/README.md @@ -82,6 +82,126 @@ 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! +## Internalization (i18n) + +Ory Elements supports translations out-of-the-box with the `IntlProvider`. The +`IntlProvider` is required by Ory Elements and maps American English as the +default language. + +```tsx +import { ThemeProvider } from "@ory/elements" + +const RootComponent = () => { + return ( + + // children + + ) +} +``` + +To switch the language the UI should use, you can pass in the language code +through the `locale` prop. + +```tsx +import { ThemeProvider } from "@ory/elements" + +const RootComponent = () => { + return ( + + + // children + + + ) +} +``` + +The `IntlProvider` has the ability to accept custom translations through a +`CustomLanguageFormats` type. You can specify to the `` that you +would like to use a `CustomTranslations` instead of the +`SupportedLanguages (default)` type which will require providing the +`customTranslations` prop. + +More information on the Ory messages can be found +[in the docs](https://www.ory.sh/docs/kratos/concepts/ui-user-interface#ui-message-codes) + +When providing a translation, you can merge an existing supported locale from +Ory Elements so that you do not need to provide all keys for the entire +tranlsation 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"`. Another unsupported language such as +`af` (Afrikaans) is also added only for one entry +`"identities.messages.1070004": "E-posadres"`. We merge the supported `en` +locale from Ory Elements so that we don't need to provide all key-value pairs. + +```tsx +import { + ThemeProvider, + IntlProvider, + CustomTranslations, + locales, +} from "@ory/elements" + +const RootComponent = () => { + const myCustomTranslations: CustomLanguageFormats = { + ...locales, + en: { + ...locales.en, + "identities.messages.1070004": "Email", + }, + af: { + // fallback to English on other labels + ...locales.en, + "identities.messages.1070004": "E-posadres", + }, + } + + return ( + + + customTranslations={myCustomTranslations} + locale="af" + defaultLocale="en" + > + // children + + + ) +} +``` + +It is of course also possible to provide the `IntlProvider` directly from the +[react-intl](https://formatjs.io/docs/react-intl/) library to format messages +and provide translations. The default translations of Ory Elements are located +in the `src/locales` directory. + +```tsx +import { IntlProvider } from "react-intl" +import { locales } from "@ory/elements" + +const customMessages = { + ...locales, + de: { + ...locales.de, + "login.title": "Login", + }, +} + +const Main = () => { + return ( + + + + {/* ... */} + + + ) +} +``` + ## End-to-end Testing with Playwright Ory Elements provides an end-to-end library based on @@ -331,51 +451,6 @@ const Main = () => { } ``` -### Internalization (i18n) - -Ory Elements uses [react-intl](https://formatjs.io/docs/react-intl/) to format -messages and provide translations. The default language is american English, but -you can provide your own translations by using the `IntlProvider` component. The -default translations of Ory Elements are located in the `src/locales` directory. -They can be loaded using the `IntlProvider` from Ory Elements. Please note that -it is necessary to wrap all Ory Element components either in the `IntlProvider` -from `react-intl` or Ory Elements. - -```tsx -import { IntlProvider } from "@ory/elements" - -const Main = () => { - return ( - - - - {/* ... */} - - - ) -} -``` - -Custom translations can be provided using the `IntlProvider` from `react-intl`. -For reference, it is best to start with the auto-generated English defaults, as -they include all keys. More information on the Kratos messages can be found -[in the docs](https://www.ory.sh/docs/kratos/concepts/ui-user-interface#ui-message-codes). - -```tsx -import { IntlProvider } from "react-intl" - -const Main = () => { - return ( - - - - {/* ... */} - - - ) -} -``` - ### Theme CSS in Express.js For Express.js the library also exports a helper function which registers all diff --git a/examples/nextjs-spa/src/components/layout.tsx b/examples/nextjs-spa/src/components/layout.tsx index 98f85ffc4..3e16dd641 100644 --- a/examples/nextjs-spa/src/components/layout.tsx +++ b/examples/nextjs-spa/src/components/layout.tsx @@ -1,4 +1,11 @@ -import { IntlProvider, Nav, ThemeProvider } from "@ory/elements" +import { + CustomLanguageFormats, + CustomTranslations, + IntlProvider, + Nav, + ThemeProvider, + locales, +} from "@ory/elements" import Head from "next/head" interface LayoutProps { @@ -6,9 +13,39 @@ interface LayoutProps { } export default function Layout({ children }: LayoutProps) { + // adds custom translations labels to the default translations + // + // 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: { + ...locales.en, + "login.title": "Login", + "identities.messages.1070004": "Email", + }, + nl: { + ...locales.nl, + "login.title": "Inloggen", + "identities.messages.1070004": "E-mail", + }, + af: { + // merging English since no default Afrikaans translations are available + ...locales.en, + "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/nextjs-spa/src/pages/registration.tsx b/examples/nextjs-spa/src/pages/registration.tsx index 81bed7bd2..54d22e42d 100644 --- a/examples/nextjs-spa/src/pages/registration.tsx +++ b/examples/nextjs-spa/src/pages/registration.tsx @@ -98,7 +98,6 @@ const Registration: NextPageWithLayout = () => { // create a registration form that dynamically renders based on the flow data using Ory Elements { forgotPasswordURL: "/recovery", signupURL: "/registration", }} - title={"Login"} includeScripts={true} onSubmit={({ body }) => submitFlow(body as UpdateLoginFlowBody)} /> diff --git a/examples/preact-spa/src/main.tsx b/examples/preact-spa/src/main.tsx index 895f93a66..514cf80a6 100644 --- a/examples/preact-spa/src/main.tsx +++ b/examples/preact-spa/src/main.tsx @@ -11,7 +11,13 @@ 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, + locales, +} from "@ory/elements-preact" // Ory Elements // optional fontawesome icons @@ -27,9 +33,39 @@ 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 + // + // 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: { + ...locales.en, + "login.title": "Login", + "identities.messages.1070004": "Email", + }, + nl: { + ...locales.nl, + "login.title": "Inloggen", + "identities.messages.1070004": "E-mail", + }, + af: { + // merging English since no default Afrikaans translations are available + ...locales.en, + "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/preact-spa/src/recovery.tsx b/examples/preact-spa/src/recovery.tsx index 06dc1cda3..d48180ae1 100644 --- a/examples/preact-spa/src/recovery.tsx +++ b/examples/preact-spa/src/recovery.tsx @@ -64,7 +64,6 @@ export const Recovery = () => { return flow ? ( { { additionalProps={{ signupURL: "/registration", }} - title="Verification" // submit the verification form data to Ory onSubmit={({ body }) => submitFlow(body as UpdateVerificationFlowBody)} /> 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..edc109eee 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,212 @@ export const ThemeProvider = ({ ) -export const IntlProvider = ({ - locale = "en", +export type TranslationFile = { + [K in keyof typeof locales.en]: string +} + +// ISO 639-1 language codes +// https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes +export const LanguageCodes = [ + "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 LanguageCodes)[number]]?: Partial +} + +export interface CustomTranslations { + customTranslations: Partial + locale: (typeof LanguageCodes)[number] + defaultLocale: (typeof LanguageCodes)[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, + defaultLocale: props.defaultLocale, + messages: props.customTranslations[props.locale], + } + : { + locale: props.locale ?? "en", + defaultLocale: props.defaultLocale ?? "en", + messages: translation, + } + + return ( + {chunks}, + }} + > + {children} + + ) +}