diff --git a/app/components/Authenticated.js b/app/components/Authenticated.js index 86b43ef5..c2d489a5 100644 --- a/app/components/Authenticated.js +++ b/app/components/Authenticated.js @@ -6,6 +6,7 @@ import { Redirect } from "react-router-dom"; import { isCustomSubdomain } from "shared/utils/domains"; import LoadingIndicator from "components/LoadingIndicator"; import useStores from "../hooks/useStores"; +import { changeLanguage } from "../utils/language"; import env from "env"; type Props = { @@ -20,11 +21,7 @@ const Authenticated = ({ children }: Props) => { // Watching for language changes here as this is the earliest point we have // the user available and means we can start loading translations faster React.useEffect(() => { - if (language && i18n.language !== language) { - // Languages are stored in en_US format in the database, however the - // frontend translation framework (i18next) expects en-US - i18n.changeLanguage(language.replace("_", "-")); - } + changeLanguage(language, i18n); }, [i18n, language]); if (auth.authenticated) { diff --git a/app/scenes/Login/Provider.js b/app/scenes/Login/Provider.js index 27eb6e5f..05884d7c 100644 --- a/app/scenes/Login/Provider.js +++ b/app/scenes/Login/Provider.js @@ -1,6 +1,7 @@ // @flow import { EmailIcon } from "outline-icons"; import * as React from "react"; +import { withTranslation, type TFunction } from "react-i18next"; import styled from "styled-components"; import AuthLogo from "components/AuthLogo"; import ButtonLarge from "components/ButtonLarge"; @@ -13,6 +14,7 @@ type Props = { authUrl: string, isCreate: boolean, onEmailSuccess: (email: string) => void, + t: TFunction, }; type State = { @@ -56,7 +58,7 @@ class Provider extends React.Component { }; render() { - const { isCreate, id, name, authUrl } = this.props; + const { isCreate, id, name, authUrl, t } = this.props; if (id === "email") { if (isCreate) { @@ -84,12 +86,12 @@ class Provider extends React.Component { short /> - Sign In → + {t("Sign In")} → ) : ( } fullwidth> - Continue with Email + {t("Continue with Email")} )} @@ -104,7 +106,9 @@ class Provider extends React.Component { icon={} fullwidth > - Continue with {name} + {t("Continue with {{ authProviderName }}", { + authProviderName: name, + })} ); @@ -122,4 +126,4 @@ const Form = styled.form` justify-content: space-between; `; -export default Provider; +export default withTranslation()(Provider); diff --git a/app/scenes/Login/index.js b/app/scenes/Login/index.js index f150e51a..333d8cc8 100644 --- a/app/scenes/Login/index.js +++ b/app/scenes/Login/index.js @@ -3,7 +3,13 @@ import { find } from "lodash"; import { observer } from "mobx-react"; import { BackIcon, EmailIcon } from "outline-icons"; import * as React from "react"; -import { Redirect, Link, type Location } from "react-router-dom"; +import { + Trans, + withTranslation, + type TFunction, + useTranslation, +} from "react-i18next"; +import { Link, type Location, Redirect } from "react-router-dom"; import styled from "styled-components"; import { setCookie } from "tiny-cookie"; import ButtonLarge from "components/ButtonLarge"; @@ -14,6 +20,7 @@ import HelpText from "components/HelpText"; import OutlineLogo from "components/OutlineLogo"; import PageTitle from "components/PageTitle"; import TeamLogo from "components/TeamLogo"; +import { changeLanguage, detectLanguage } from "../../utils/language"; import Notices from "./Notices"; import Provider from "./Provider"; import env from "env"; @@ -22,10 +29,12 @@ import useStores from "hooks/useStores"; type Props = {| location: Location, + t: TFunction, |}; -function Login({ location }: Props) { +function Login({ location, t }: Props) { const query = useQuery(); + const { i18n } = useTranslation(); const { auth } = useStores(); const { config } = auth; const [emailLinkSentTo, setEmailLinkSentTo] = React.useState(""); @@ -43,6 +52,13 @@ function Login({ location }: Props) { auth.fetchConfig(); }, [auth]); + // TODO: Persist detected language to new user + // Try to detect the user's language and show the login page on its idiom + // if translation is available + React.useEffect(() => { + changeLanguage(detectLanguage(), i18n); + }, [i18n]); + React.useEffect(() => { const entries = Object.fromEntries(query.entries()); @@ -73,11 +89,11 @@ function Login({ location }: Props) { env.DEPLOYMENT === "hosted" && (config.hostname ? ( - Back to home + {t("Back to home")} ) : ( - Back to website + {t("Back to website")} )); @@ -88,15 +104,17 @@ function Login({ location }: Props) { - - Check your email + {t("Check your email")} - A magic sign-in link has been sent to the email{" "} - {emailLinkSentTo}, no password needed. + }} + />
- Back to login + {t("Back to login")}
@@ -118,13 +136,19 @@ function Login({ location }: Props) { {isCreate ? ( <> - Create an account + {t("Create an account")} - Get started by choosing a sign-in method for your new team below… + {t( + "Get started by choosing a sign-in method for your new team below…" + )} ) : ( - Login to {config.name || "Outline"} + + {t("Login to {{ authProviderName }}", { + authProviderName: config.name || "Outline", + })} + )} @@ -139,7 +163,9 @@ function Login({ location }: Props) { {hasMultipleProviders && ( <> - You signed in with {defaultProvider.name} last time. + {t("You signed in with {{ authProviderName }} last time.", { + authProviderName: defaultProvider.name, + })} @@ -164,7 +190,9 @@ function Login({ location }: Props) { {isCreate && ( - Already have an account? Go to login. + + Already have an account? Go to login. + )} @@ -250,4 +278,4 @@ const Centered = styled(Flex)` margin: 0 auto; `; -export default observer(Login); +export default withTranslation()(observer(Login)); diff --git a/app/utils/language.js b/app/utils/language.js index 722f359b..050ae490 100644 --- a/app/utils/language.js +++ b/app/utils/language.js @@ -5,3 +5,11 @@ export function detectLanguage() { const region = (r || ln).toUpperCase(); return `${ln}_${region}`; } + +export function changeLanguage(toLanguageString, i18n) { + if (toLanguageString && i18n.language !== toLanguageString) { + // Languages are stored in en_US format in the database, however the + // frontend translation framework (i18next) expects en-US + i18n.changeLanguage(toLanguageString.replace("_", "-")); + } +} diff --git a/package.json b/package.json index 23f4d9bd..2c4ebecb 100644 --- a/package.json +++ b/package.json @@ -212,4 +212,4 @@ "js-yaml": "^3.13.1" }, "version": "0.57.0" -} \ No newline at end of file +} diff --git a/shared/i18n/locales/en_US/translation.json b/shared/i18n/locales/en_US/translation.json index 964d87c2..f23f0aa5 100644 --- a/shared/i18n/locales/en_US/translation.json +++ b/shared/i18n/locales/en_US/translation.json @@ -419,6 +419,19 @@ "Blockquote": "Blockquote", "Horizontal divider": "Horizontal divider", "Inline code": "Inline code", + "Back to home": "Back to home", + "Back to website": "Back to website", + "Check your email": "Check your email", + "A magic sign-in link has been sent to the email {{ emailLinkSentTo }}, no password needed.": "A magic sign-in link has been sent to the email {{ emailLinkSentTo }}, no password needed.", + "Back to login": "Back to login", + "Create an account": "Create an account", + "Get started by choosing a sign-in method for your new team below…": "Get started by choosing a sign-in method for your new team below…", + "Login to {{ authProviderName }}": "Login to {{ authProviderName }}", + "You signed in with {{ authProviderName }} last time.": "You signed in with {{ authProviderName }} last time.", + "Already have an account? Go to <1>login.": "Already have an account? Go to <1>login.", + "Sign In": "Sign In", + "Continue with Email": "Continue with Email", + "Continue with {{ authProviderName }}": "Continue with {{ authProviderName }}", "Any collection": "Any collection", "Any time": "Any time", "Past day": "Past day",