feat: Auto detect language on login page access (#2338)
* feat: Auto detect language on login page access * fix: Apply tommoor suggested changes * fix: QOL improvements for translators * fix: consistency fix provider -> authProviderName Co-authored-by: Tom Moor <tom.moor@gmail.com>
This commit is contained in:
@ -6,6 +6,7 @@ import { Redirect } from "react-router-dom";
|
|||||||
import { isCustomSubdomain } from "shared/utils/domains";
|
import { isCustomSubdomain } from "shared/utils/domains";
|
||||||
import LoadingIndicator from "components/LoadingIndicator";
|
import LoadingIndicator from "components/LoadingIndicator";
|
||||||
import useStores from "../hooks/useStores";
|
import useStores from "../hooks/useStores";
|
||||||
|
import { changeLanguage } from "../utils/language";
|
||||||
import env from "env";
|
import env from "env";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@ -20,11 +21,7 @@ const Authenticated = ({ children }: Props) => {
|
|||||||
// Watching for language changes here as this is the earliest point we have
|
// Watching for language changes here as this is the earliest point we have
|
||||||
// the user available and means we can start loading translations faster
|
// the user available and means we can start loading translations faster
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (language && i18n.language !== language) {
|
changeLanguage(language, i18n);
|
||||||
// Languages are stored in en_US format in the database, however the
|
|
||||||
// frontend translation framework (i18next) expects en-US
|
|
||||||
i18n.changeLanguage(language.replace("_", "-"));
|
|
||||||
}
|
|
||||||
}, [i18n, language]);
|
}, [i18n, language]);
|
||||||
|
|
||||||
if (auth.authenticated) {
|
if (auth.authenticated) {
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
// @flow
|
// @flow
|
||||||
import { EmailIcon } from "outline-icons";
|
import { EmailIcon } from "outline-icons";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
import { withTranslation, type TFunction } from "react-i18next";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
import AuthLogo from "components/AuthLogo";
|
import AuthLogo from "components/AuthLogo";
|
||||||
import ButtonLarge from "components/ButtonLarge";
|
import ButtonLarge from "components/ButtonLarge";
|
||||||
@ -13,6 +14,7 @@ type Props = {
|
|||||||
authUrl: string,
|
authUrl: string,
|
||||||
isCreate: boolean,
|
isCreate: boolean,
|
||||||
onEmailSuccess: (email: string) => void,
|
onEmailSuccess: (email: string) => void,
|
||||||
|
t: TFunction,
|
||||||
};
|
};
|
||||||
|
|
||||||
type State = {
|
type State = {
|
||||||
@ -56,7 +58,7 @@ class Provider extends React.Component<Props, State> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { isCreate, id, name, authUrl } = this.props;
|
const { isCreate, id, name, authUrl, t } = this.props;
|
||||||
|
|
||||||
if (id === "email") {
|
if (id === "email") {
|
||||||
if (isCreate) {
|
if (isCreate) {
|
||||||
@ -84,12 +86,12 @@ class Provider extends React.Component<Props, State> {
|
|||||||
short
|
short
|
||||||
/>
|
/>
|
||||||
<ButtonLarge type="submit" disabled={this.state.isSubmitting}>
|
<ButtonLarge type="submit" disabled={this.state.isSubmitting}>
|
||||||
Sign In →
|
{t("Sign In")} →
|
||||||
</ButtonLarge>
|
</ButtonLarge>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<ButtonLarge type="submit" icon={<EmailIcon />} fullwidth>
|
<ButtonLarge type="submit" icon={<EmailIcon />} fullwidth>
|
||||||
Continue with Email
|
{t("Continue with Email")}
|
||||||
</ButtonLarge>
|
</ButtonLarge>
|
||||||
)}
|
)}
|
||||||
</Form>
|
</Form>
|
||||||
@ -104,7 +106,9 @@ class Provider extends React.Component<Props, State> {
|
|||||||
icon={<AuthLogo providerName={id} />}
|
icon={<AuthLogo providerName={id} />}
|
||||||
fullwidth
|
fullwidth
|
||||||
>
|
>
|
||||||
Continue with {name}
|
{t("Continue with {{ authProviderName }}", {
|
||||||
|
authProviderName: name,
|
||||||
|
})}
|
||||||
</ButtonLarge>
|
</ButtonLarge>
|
||||||
</Wrapper>
|
</Wrapper>
|
||||||
);
|
);
|
||||||
@ -122,4 +126,4 @@ const Form = styled.form`
|
|||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export default Provider;
|
export default withTranslation()<Provider>(Provider);
|
||||||
|
@ -3,7 +3,13 @@ import { find } from "lodash";
|
|||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { BackIcon, EmailIcon } from "outline-icons";
|
import { BackIcon, EmailIcon } from "outline-icons";
|
||||||
import * as React from "react";
|
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 styled from "styled-components";
|
||||||
import { setCookie } from "tiny-cookie";
|
import { setCookie } from "tiny-cookie";
|
||||||
import ButtonLarge from "components/ButtonLarge";
|
import ButtonLarge from "components/ButtonLarge";
|
||||||
@ -14,6 +20,7 @@ import HelpText from "components/HelpText";
|
|||||||
import OutlineLogo from "components/OutlineLogo";
|
import OutlineLogo from "components/OutlineLogo";
|
||||||
import PageTitle from "components/PageTitle";
|
import PageTitle from "components/PageTitle";
|
||||||
import TeamLogo from "components/TeamLogo";
|
import TeamLogo from "components/TeamLogo";
|
||||||
|
import { changeLanguage, detectLanguage } from "../../utils/language";
|
||||||
import Notices from "./Notices";
|
import Notices from "./Notices";
|
||||||
import Provider from "./Provider";
|
import Provider from "./Provider";
|
||||||
import env from "env";
|
import env from "env";
|
||||||
@ -22,10 +29,12 @@ import useStores from "hooks/useStores";
|
|||||||
|
|
||||||
type Props = {|
|
type Props = {|
|
||||||
location: Location,
|
location: Location,
|
||||||
|
t: TFunction,
|
||||||
|};
|
|};
|
||||||
|
|
||||||
function Login({ location }: Props) {
|
function Login({ location, t }: Props) {
|
||||||
const query = useQuery();
|
const query = useQuery();
|
||||||
|
const { i18n } = useTranslation();
|
||||||
const { auth } = useStores();
|
const { auth } = useStores();
|
||||||
const { config } = auth;
|
const { config } = auth;
|
||||||
const [emailLinkSentTo, setEmailLinkSentTo] = React.useState("");
|
const [emailLinkSentTo, setEmailLinkSentTo] = React.useState("");
|
||||||
@ -43,6 +52,13 @@ function Login({ location }: Props) {
|
|||||||
auth.fetchConfig();
|
auth.fetchConfig();
|
||||||
}, [auth]);
|
}, [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(() => {
|
React.useEffect(() => {
|
||||||
const entries = Object.fromEntries(query.entries());
|
const entries = Object.fromEntries(query.entries());
|
||||||
|
|
||||||
@ -73,11 +89,11 @@ function Login({ location }: Props) {
|
|||||||
env.DEPLOYMENT === "hosted" &&
|
env.DEPLOYMENT === "hosted" &&
|
||||||
(config.hostname ? (
|
(config.hostname ? (
|
||||||
<Back href={env.URL}>
|
<Back href={env.URL}>
|
||||||
<BackIcon color="currentColor" /> Back to home
|
<BackIcon color="currentColor" /> {t("Back to home")}
|
||||||
</Back>
|
</Back>
|
||||||
) : (
|
) : (
|
||||||
<Back href="https://www.getoutline.com">
|
<Back href="https://www.getoutline.com">
|
||||||
<BackIcon color="currentColor" /> Back to website
|
<BackIcon color="currentColor" /> {t("Back to website")}
|
||||||
</Back>
|
</Back>
|
||||||
));
|
));
|
||||||
|
|
||||||
@ -88,15 +104,17 @@ function Login({ location }: Props) {
|
|||||||
<Centered align="center" justify="center" column auto>
|
<Centered align="center" justify="center" column auto>
|
||||||
<PageTitle title="Check your email" />
|
<PageTitle title="Check your email" />
|
||||||
<CheckEmailIcon size={38} color="currentColor" />
|
<CheckEmailIcon size={38} color="currentColor" />
|
||||||
|
<Heading centered>{t("Check your email")}</Heading>
|
||||||
<Heading centered>Check your email</Heading>
|
|
||||||
<Note>
|
<Note>
|
||||||
A magic sign-in link has been sent to the email{" "}
|
<Trans
|
||||||
<em>{emailLinkSentTo}</em>, no password needed.
|
defaults="A magic sign-in link has been sent to the email <em>{{ emailLinkSentTo }}</em>, no password needed."
|
||||||
|
values={{ emailLinkSentTo: emailLinkSentTo }}
|
||||||
|
components={{ em: <em /> }}
|
||||||
|
/>
|
||||||
</Note>
|
</Note>
|
||||||
<br />
|
<br />
|
||||||
<ButtonLarge onClick={handleReset} fullwidth neutral>
|
<ButtonLarge onClick={handleReset} fullwidth neutral>
|
||||||
Back to login
|
{t("Back to login")}
|
||||||
</ButtonLarge>
|
</ButtonLarge>
|
||||||
</Centered>
|
</Centered>
|
||||||
</Background>
|
</Background>
|
||||||
@ -118,13 +136,19 @@ function Login({ location }: Props) {
|
|||||||
|
|
||||||
{isCreate ? (
|
{isCreate ? (
|
||||||
<>
|
<>
|
||||||
<Heading centered>Create an account</Heading>
|
<Heading centered>{t("Create an account")}</Heading>
|
||||||
<GetStarted>
|
<GetStarted>
|
||||||
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…"
|
||||||
|
)}
|
||||||
</GetStarted>
|
</GetStarted>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<Heading centered>Login to {config.name || "Outline"}</Heading>
|
<Heading centered>
|
||||||
|
{t("Login to {{ authProviderName }}", {
|
||||||
|
authProviderName: config.name || "Outline",
|
||||||
|
})}
|
||||||
|
</Heading>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Notices />
|
<Notices />
|
||||||
@ -139,7 +163,9 @@ function Login({ location }: Props) {
|
|||||||
{hasMultipleProviders && (
|
{hasMultipleProviders && (
|
||||||
<>
|
<>
|
||||||
<Note>
|
<Note>
|
||||||
You signed in with {defaultProvider.name} last time.
|
{t("You signed in with {{ authProviderName }} last time.", {
|
||||||
|
authProviderName: defaultProvider.name,
|
||||||
|
})}
|
||||||
</Note>
|
</Note>
|
||||||
<Or />
|
<Or />
|
||||||
</>
|
</>
|
||||||
@ -164,7 +190,9 @@ function Login({ location }: Props) {
|
|||||||
|
|
||||||
{isCreate && (
|
{isCreate && (
|
||||||
<Note>
|
<Note>
|
||||||
Already have an account? Go to <Link to="/">login</Link>.
|
<Trans>
|
||||||
|
Already have an account? Go to <Link to="/">login</Link>.
|
||||||
|
</Trans>
|
||||||
</Note>
|
</Note>
|
||||||
)}
|
)}
|
||||||
</Centered>
|
</Centered>
|
||||||
@ -250,4 +278,4 @@ const Centered = styled(Flex)`
|
|||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export default observer(Login);
|
export default withTranslation()<Login>(observer(Login));
|
||||||
|
@ -5,3 +5,11 @@ export function detectLanguage() {
|
|||||||
const region = (r || ln).toUpperCase();
|
const region = (r || ln).toUpperCase();
|
||||||
return `${ln}_${region}`;
|
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("_", "-"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -212,4 +212,4 @@
|
|||||||
"js-yaml": "^3.13.1"
|
"js-yaml": "^3.13.1"
|
||||||
},
|
},
|
||||||
"version": "0.57.0"
|
"version": "0.57.0"
|
||||||
}
|
}
|
||||||
|
@ -419,6 +419,19 @@
|
|||||||
"Blockquote": "Blockquote",
|
"Blockquote": "Blockquote",
|
||||||
"Horizontal divider": "Horizontal divider",
|
"Horizontal divider": "Horizontal divider",
|
||||||
"Inline code": "Inline code",
|
"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 <em>{{ emailLinkSentTo }}</em>, no password needed.": "A magic sign-in link has been sent to the email <em>{{ emailLinkSentTo }}</em>, 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</1>.": "Already have an account? Go to <1>login</1>.",
|
||||||
|
"Sign In": "Sign In",
|
||||||
|
"Continue with Email": "Continue with Email",
|
||||||
|
"Continue with {{ authProviderName }}": "Continue with {{ authProviderName }}",
|
||||||
"Any collection": "Any collection",
|
"Any collection": "Any collection",
|
||||||
"Any time": "Any time",
|
"Any time": "Any time",
|
||||||
"Past day": "Past day",
|
"Past day": "Past day",
|
||||||
|
Reference in New Issue
Block a user