fix: App switches back to default installation language when navigating to root
This commit is contained in:
17
app/components/ChangeLanguage.tsx
Normal file
17
app/components/ChangeLanguage.tsx
Normal 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;
|
||||
}
|
||||
51
app/scenes/Login/components/BackButton.tsx
Normal file
51
app/scenes/Login/components/BackButton.tsx
Normal 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);
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -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;
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user