fix: App switches back to default installation language when navigating to root

This commit is contained in:
Tom Moor
2023-09-07 22:48:19 -04:00
parent d8bfb0fe5d
commit 5c8bcc11b4
7 changed files with 117 additions and 77 deletions

View File

@@ -0,0 +1,17 @@
import * as React from "react";
import { useTranslation } from "react-i18next";
import { changeLanguage } from "~/utils/language";
type Props = {
locale: string;
};
export default function ChangeLanguage({ locale }: Props) {
const { i18n } = useTranslation();
React.useEffect(() => {
void changeLanguage(locale, i18n);
}, [locale, i18n]);
return null;
}

View File

@@ -0,0 +1,51 @@
import { BackIcon } from "outline-icons";
import * as React from "react";
import { useTranslation } from "react-i18next";
import styled from "styled-components";
import { parseDomain } from "@shared/utils/domains";
import { Config } from "~/stores/AuthStore";
import env from "~/env";
import Desktop from "~/utils/Desktop";
import isCloudHosted from "~/utils/isCloudHosted";
type Props = {
config?: Config;
};
export default function BackButton({ config }: Props) {
const { t } = useTranslation();
const isSubdomain = !!config?.hostname;
if (!isCloudHosted || parseDomain(window.location.origin).custom) {
return null;
}
if (Desktop.isElectron() && !isSubdomain) {
return null;
}
return (
<Link href={isSubdomain ? env.URL : "https://www.getoutline.com"}>
<BackIcon /> {Desktop.isElectron() ? t("Back") : t("Back to home")}
</Link>
);
}
const Link = styled.a`
display: flex;
align-items: center;
color: inherit;
padding: ${Desktop.isElectron() ? "48px 32px" : "32px"};
font-weight: 500;
position: absolute;
svg {
transition: transform 100ms ease-in-out;
}
&:hover {
svg {
transform: translateX(-4px);
}
}
`;

View File

@@ -1,6 +1,6 @@
import find from "lodash/find";
import { observer } from "mobx-react";
import { BackIcon, EmailIcon } from "outline-icons";
import { EmailIcon } from "outline-icons";
import * as React from "react";
import { Trans, useTranslation } from "react-i18next";
import { useLocation, Link, Redirect } from "react-router-dom";
@@ -11,6 +11,7 @@ import { UserPreference } from "@shared/types";
import { parseDomain } from "@shared/utils/domains";
import { Config } from "~/stores/AuthStore";
import ButtonLarge from "~/components/ButtonLarge";
import ChangeLanguage from "~/components/ChangeLanguage";
import Fade from "~/components/Fade";
import Flex from "~/components/Flex";
import Heading from "~/components/Heading";
@@ -27,28 +28,10 @@ import useStores from "~/hooks/useStores";
import { draggableOnDesktop } from "~/styles";
import Desktop from "~/utils/Desktop";
import isCloudHosted from "~/utils/isCloudHosted";
import { changeLanguage, detectLanguage } from "~/utils/language";
import AuthenticationProvider from "./AuthenticationProvider";
import Notices from "./Notices";
function Header({ config }: { config?: Config | undefined }) {
const { t } = useTranslation();
const isSubdomain = !!config?.hostname;
if (!isCloudHosted || parseDomain(window.location.origin).custom) {
return null;
}
if (Desktop.isElectron() && !isSubdomain) {
return null;
}
return (
<Back href={isSubdomain ? env.URL : "https://www.getoutline.com"}>
<BackIcon /> {Desktop.isElectron() ? t("Back") : t("Back to home")}
</Back>
);
}
import { detectLanguage } from "~/utils/language";
import AuthenticationProvider from "./components/AuthenticationProvider";
import BackButton from "./components/BackButton";
import Notices from "./components/Notices";
type Props = {
children?: (config?: Config) => React.ReactNode;
@@ -59,7 +42,7 @@ function Login({ children }: Props) {
const query = useQuery();
const notice = query.get("notice");
const { t, i18n } = useTranslation();
const { t } = useTranslation();
const { auth } = useStores();
const { config } = auth;
const [error, setError] = React.useState(null);
@@ -97,13 +80,6 @@ function Login({ children }: Props) {
auth.fetchConfig().catch(setError);
}, [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(() => {
void changeLanguage(detectLanguage(), i18n);
}, [i18n]);
React.useEffect(() => {
const entries = Object.fromEntries(query.entries());
const existing = getCookie("signupQueryParams");
@@ -134,7 +110,8 @@ function Login({ children }: Props) {
if (error) {
return (
<Background>
<Header />
<BackButton />
<ChangeLanguage locale={detectLanguage()} />
<Centered align="center" justify="center" column auto>
<PageTitle title={t("Login")} />
<Heading centered>{t("Error")}</Heading>
@@ -165,7 +142,8 @@ function Login({ children }: Props) {
if (isCloudHosted && isCustomDomain && !config.name) {
return (
<Background>
<Header config={config} />
<BackButton config={config} />
<ChangeLanguage locale={detectLanguage()} />
<Centered align="center" justify="center" column auto>
<PageTitle title={t("Custom domain setup")} />
<Heading centered>{t("Almost there")}</Heading>
@@ -182,7 +160,8 @@ function Login({ children }: Props) {
if (Desktop.isElectron() && notice === "domain-required") {
return (
<Background>
<Header config={config} />
<BackButton config={config} />
<ChangeLanguage locale={detectLanguage()} />
<Centered
as="form"
@@ -225,7 +204,7 @@ function Login({ children }: Props) {
if (emailLinkSentTo) {
return (
<Background>
<Header config={config} />
<BackButton config={config} />
<Centered align="center" justify="center" column auto>
<PageTitle title={t("Check your email")} />
<CheckEmailIcon size={38} />
@@ -248,7 +227,9 @@ function Login({ children }: Props) {
return (
<Background>
<Header config={config} />
<BackButton config={config} />
<ChangeLanguage locale={detectLanguage()} />
<Centered align="center" justify="center" gap={12} column auto>
<PageTitle
title={config.name ? `${config.name} ${t("Login")}` : t("Login")}
@@ -370,25 +351,6 @@ const Note = styled(Text)`
}
`;
const Back = styled.a`
display: flex;
align-items: center;
color: inherit;
padding: ${Desktop.isElectron() ? "48px 32px" : "32px"};
font-weight: 500;
position: absolute;
svg {
transition: transform 100ms ease-in-out;
}
&:hover {
svg {
transform: translateX(-4px);
}
}
`;
const Or = styled.hr`
margin: 1em 0;
position: relative;

View File

@@ -2,24 +2,34 @@ import { i18n } from "i18next";
import { unicodeCLDRtoBCP47 } from "@shared/utils/date";
import Desktop from "./Desktop";
/**
* Detects the user's language based on the browser's language settings.
*
* @returns The user's language in CLDR format (en_US)
*/
export function detectLanguage() {
const [ln, r] = navigator.language.split("-");
const region = (r || ln).toUpperCase();
return `${ln}_${region}`;
}
/**
* Changes the language of the app, and updates the spellchecker language
* if running in the desktop shell.
*
* @param locale The locale to change to, in CLDR format (en_US)
* @param i18n The i18n instance to use
*/
export async function changeLanguage(
toLanguageString: string | null | undefined,
locale: string | null | undefined,
i18n: i18n
) {
// Languages are stored in en_US format in the database, however the
// frontend translation framework (i18next) expects en-US
const locale = toLanguageString
? unicodeCLDRtoBCP47(toLanguageString)
: undefined;
const localeBCP = locale ? unicodeCLDRtoBCP47(locale) : undefined;
if (locale && i18n.languages?.[0] !== locale) {
await i18n.changeLanguage(locale);
await Desktop.bridge?.setSpellCheckerLanguages(["en-US", locale]);
if (localeBCP && i18n.languages?.[0] !== localeBCP) {
await i18n.changeLanguage(localeBCP);
await Desktop.bridge?.setSpellCheckerLanguages(["en-US", localeBCP]);
}
}

View File

@@ -663,6 +663,21 @@
"Continue with Email": "Continue with Email",
"Continue with {{ authProviderName }}": "Continue with {{ authProviderName }}",
"Back to home": "Back to home",
"The domain associated with your email address has not been allowed for this workspace.": "The domain associated with your email address has not been allowed for this workspace.",
"Unable to sign-in. Please navigate to your workspace's custom URL, then try to sign-in again.<1></1>If you were invited to a workspace, you will find a link to it in the invite email.": "Unable to sign-in. Please navigate to your workspace's custom URL, then try to sign-in again.<1></1>If you were invited to a workspace, you will find a link to it in the invite email.",
"Sorry, a new account cannot be created with a personal Gmail address.<1></1>Please use a Google Workspaces account instead.": "Sorry, a new account cannot be created with a personal Gmail address.<1></1>Please use a Google Workspaces account instead.",
"The workspace associated with your user is scheduled for deletion and cannot at accessed at this time.": "The workspace associated with your user is scheduled for deletion and cannot at accessed at this time.",
"The workspace you authenticated with is not authorized on this installation. Try another?": "The workspace you authenticated with is not authorized on this installation. Try another?",
"We could not read the user info supplied by your identity provider.": "We could not read the user info supplied by your identity provider.",
"Your account uses email sign-in, please sign-in with email to continue.": "Your account uses email sign-in, please sign-in with email to continue.",
"An email sign-in link was recently sent, please check your inbox or try again in a few minutes.": "An email sign-in link was recently sent, please check your inbox or try again in a few minutes.",
"Authentication failed we were unable to sign you in at this time. Please try again.": "Authentication failed we were unable to sign you in at this time. Please try again.",
"Authentication failed you do not have permission to access this workspace.": "Authentication failed you do not have permission to access this workspace.",
"Sorry, it looks like that sign-in link is no longer valid, please try requesting another.": "Sorry, it looks like that sign-in link is no longer valid, please try requesting another.",
"Your account has been suspended. To re-activate your account, please contact a workspace admin.": "Your account has been suspended. To re-activate your account, please contact a workspace admin.",
"Authentication failed this login method was disabled by a team admin.": "Authentication failed this login method was disabled by a team admin.",
"The workspace you are trying to join requires an invite before you can create an account.<1></1>Please request an invite from your workspace admin and try again.": "The workspace you are trying to join requires an invite before you can create an account.<1></1>Please request an invite from your workspace admin and try again.",
"Sorry, your domain is not allowed. Please try again with an allowed workspace domain.": "Sorry, your domain is not allowed. Please try again with an allowed workspace domain.",
"Login": "Login",
"Error": "Error",
"Failed to load configuration.": "Failed to load configuration.",
@@ -681,21 +696,6 @@
"You signed in with {{ authProviderName }} last time.": "You signed in with {{ authProviderName }} last time.",
"Or": "Or",
"Already have an account? Go to <1>login</1>.": "Already have an account? Go to <1>login</1>.",
"The domain associated with your email address has not been allowed for this workspace.": "The domain associated with your email address has not been allowed for this workspace.",
"Unable to sign-in. Please navigate to your workspace's custom URL, then try to sign-in again.<1></1>If you were invited to a workspace, you will find a link to it in the invite email.": "Unable to sign-in. Please navigate to your workspace's custom URL, then try to sign-in again.<1></1>If you were invited to a workspace, you will find a link to it in the invite email.",
"Sorry, a new account cannot be created with a personal Gmail address.<1></1>Please use a Google Workspaces account instead.": "Sorry, a new account cannot be created with a personal Gmail address.<1></1>Please use a Google Workspaces account instead.",
"The workspace associated with your user is scheduled for deletion and cannot at accessed at this time.": "The workspace associated with your user is scheduled for deletion and cannot at accessed at this time.",
"The workspace you authenticated with is not authorized on this installation. Try another?": "The workspace you authenticated with is not authorized on this installation. Try another?",
"We could not read the user info supplied by your identity provider.": "We could not read the user info supplied by your identity provider.",
"Your account uses email sign-in, please sign-in with email to continue.": "Your account uses email sign-in, please sign-in with email to continue.",
"An email sign-in link was recently sent, please check your inbox or try again in a few minutes.": "An email sign-in link was recently sent, please check your inbox or try again in a few minutes.",
"Authentication failed we were unable to sign you in at this time. Please try again.": "Authentication failed we were unable to sign you in at this time. Please try again.",
"Authentication failed you do not have permission to access this workspace.": "Authentication failed you do not have permission to access this workspace.",
"Sorry, it looks like that sign-in link is no longer valid, please try requesting another.": "Sorry, it looks like that sign-in link is no longer valid, please try requesting another.",
"Your account has been suspended. To re-activate your account, please contact a workspace admin.": "Your account has been suspended. To re-activate your account, please contact a workspace admin.",
"Authentication failed this login method was disabled by a team admin.": "Authentication failed this login method was disabled by a team admin.",
"The workspace you are trying to join requires an invite before you can create an account.<1></1>Please request an invite from your workspace admin and try again.": "The workspace you are trying to join requires an invite before you can create an account.<1></1>Please request an invite from your workspace admin and try again.",
"Sorry, your domain is not allowed. Please try again with an allowed workspace domain.": "Sorry, your domain is not allowed. Please try again with an allowed workspace domain.",
"Any collection": "Any collection",
"Any time": "Any time",
"Past day": "Past day",