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 find from "lodash/find";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { BackIcon, EmailIcon } from "outline-icons";
|
import { EmailIcon } from "outline-icons";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { Trans, useTranslation } from "react-i18next";
|
import { Trans, useTranslation } from "react-i18next";
|
||||||
import { useLocation, Link, Redirect } from "react-router-dom";
|
import { useLocation, Link, Redirect } from "react-router-dom";
|
||||||
@@ -11,6 +11,7 @@ import { UserPreference } from "@shared/types";
|
|||||||
import { parseDomain } from "@shared/utils/domains";
|
import { parseDomain } from "@shared/utils/domains";
|
||||||
import { Config } from "~/stores/AuthStore";
|
import { Config } from "~/stores/AuthStore";
|
||||||
import ButtonLarge from "~/components/ButtonLarge";
|
import ButtonLarge from "~/components/ButtonLarge";
|
||||||
|
import ChangeLanguage from "~/components/ChangeLanguage";
|
||||||
import Fade from "~/components/Fade";
|
import Fade from "~/components/Fade";
|
||||||
import Flex from "~/components/Flex";
|
import Flex from "~/components/Flex";
|
||||||
import Heading from "~/components/Heading";
|
import Heading from "~/components/Heading";
|
||||||
@@ -27,28 +28,10 @@ import useStores from "~/hooks/useStores";
|
|||||||
import { draggableOnDesktop } from "~/styles";
|
import { draggableOnDesktop } from "~/styles";
|
||||||
import Desktop from "~/utils/Desktop";
|
import Desktop from "~/utils/Desktop";
|
||||||
import isCloudHosted from "~/utils/isCloudHosted";
|
import isCloudHosted from "~/utils/isCloudHosted";
|
||||||
import { changeLanguage, detectLanguage } from "~/utils/language";
|
import { detectLanguage } from "~/utils/language";
|
||||||
import AuthenticationProvider from "./AuthenticationProvider";
|
import AuthenticationProvider from "./components/AuthenticationProvider";
|
||||||
import Notices from "./Notices";
|
import BackButton from "./components/BackButton";
|
||||||
|
import Notices from "./components/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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
children?: (config?: Config) => React.ReactNode;
|
children?: (config?: Config) => React.ReactNode;
|
||||||
@@ -59,7 +42,7 @@ function Login({ children }: Props) {
|
|||||||
const query = useQuery();
|
const query = useQuery();
|
||||||
const notice = query.get("notice");
|
const notice = query.get("notice");
|
||||||
|
|
||||||
const { t, i18n } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { auth } = useStores();
|
const { auth } = useStores();
|
||||||
const { config } = auth;
|
const { config } = auth;
|
||||||
const [error, setError] = React.useState(null);
|
const [error, setError] = React.useState(null);
|
||||||
@@ -97,13 +80,6 @@ function Login({ children }: Props) {
|
|||||||
auth.fetchConfig().catch(setError);
|
auth.fetchConfig().catch(setError);
|
||||||
}, [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(() => {
|
|
||||||
void changeLanguage(detectLanguage(), i18n);
|
|
||||||
}, [i18n]);
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const entries = Object.fromEntries(query.entries());
|
const entries = Object.fromEntries(query.entries());
|
||||||
const existing = getCookie("signupQueryParams");
|
const existing = getCookie("signupQueryParams");
|
||||||
@@ -134,7 +110,8 @@ function Login({ children }: Props) {
|
|||||||
if (error) {
|
if (error) {
|
||||||
return (
|
return (
|
||||||
<Background>
|
<Background>
|
||||||
<Header />
|
<BackButton />
|
||||||
|
<ChangeLanguage locale={detectLanguage()} />
|
||||||
<Centered align="center" justify="center" column auto>
|
<Centered align="center" justify="center" column auto>
|
||||||
<PageTitle title={t("Login")} />
|
<PageTitle title={t("Login")} />
|
||||||
<Heading centered>{t("Error")}</Heading>
|
<Heading centered>{t("Error")}</Heading>
|
||||||
@@ -165,7 +142,8 @@ function Login({ children }: Props) {
|
|||||||
if (isCloudHosted && isCustomDomain && !config.name) {
|
if (isCloudHosted && isCustomDomain && !config.name) {
|
||||||
return (
|
return (
|
||||||
<Background>
|
<Background>
|
||||||
<Header config={config} />
|
<BackButton config={config} />
|
||||||
|
<ChangeLanguage locale={detectLanguage()} />
|
||||||
<Centered align="center" justify="center" column auto>
|
<Centered align="center" justify="center" column auto>
|
||||||
<PageTitle title={t("Custom domain setup")} />
|
<PageTitle title={t("Custom domain setup")} />
|
||||||
<Heading centered>{t("Almost there")}…</Heading>
|
<Heading centered>{t("Almost there")}…</Heading>
|
||||||
@@ -182,7 +160,8 @@ function Login({ children }: Props) {
|
|||||||
if (Desktop.isElectron() && notice === "domain-required") {
|
if (Desktop.isElectron() && notice === "domain-required") {
|
||||||
return (
|
return (
|
||||||
<Background>
|
<Background>
|
||||||
<Header config={config} />
|
<BackButton config={config} />
|
||||||
|
<ChangeLanguage locale={detectLanguage()} />
|
||||||
|
|
||||||
<Centered
|
<Centered
|
||||||
as="form"
|
as="form"
|
||||||
@@ -225,7 +204,7 @@ function Login({ children }: Props) {
|
|||||||
if (emailLinkSentTo) {
|
if (emailLinkSentTo) {
|
||||||
return (
|
return (
|
||||||
<Background>
|
<Background>
|
||||||
<Header config={config} />
|
<BackButton config={config} />
|
||||||
<Centered align="center" justify="center" column auto>
|
<Centered align="center" justify="center" column auto>
|
||||||
<PageTitle title={t("Check your email")} />
|
<PageTitle title={t("Check your email")} />
|
||||||
<CheckEmailIcon size={38} />
|
<CheckEmailIcon size={38} />
|
||||||
@@ -248,7 +227,9 @@ function Login({ children }: Props) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Background>
|
<Background>
|
||||||
<Header config={config} />
|
<BackButton config={config} />
|
||||||
|
<ChangeLanguage locale={detectLanguage()} />
|
||||||
|
|
||||||
<Centered align="center" justify="center" gap={12} column auto>
|
<Centered align="center" justify="center" gap={12} column auto>
|
||||||
<PageTitle
|
<PageTitle
|
||||||
title={config.name ? `${config.name} – ${t("Login")}` : t("Login")}
|
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`
|
const Or = styled.hr`
|
||||||
margin: 1em 0;
|
margin: 1em 0;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|||||||
@@ -2,24 +2,34 @@ import { i18n } from "i18next";
|
|||||||
import { unicodeCLDRtoBCP47 } from "@shared/utils/date";
|
import { unicodeCLDRtoBCP47 } from "@shared/utils/date";
|
||||||
import Desktop from "./Desktop";
|
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() {
|
export function detectLanguage() {
|
||||||
const [ln, r] = navigator.language.split("-");
|
const [ln, r] = navigator.language.split("-");
|
||||||
const region = (r || ln).toUpperCase();
|
const region = (r || ln).toUpperCase();
|
||||||
return `${ln}_${region}`;
|
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(
|
export async function changeLanguage(
|
||||||
toLanguageString: string | null | undefined,
|
locale: string | null | undefined,
|
||||||
i18n: i18n
|
i18n: i18n
|
||||||
) {
|
) {
|
||||||
// Languages are stored in en_US format in the database, however the
|
// Languages are stored in en_US format in the database, however the
|
||||||
// frontend translation framework (i18next) expects en-US
|
// frontend translation framework (i18next) expects en-US
|
||||||
const locale = toLanguageString
|
const localeBCP = locale ? unicodeCLDRtoBCP47(locale) : undefined;
|
||||||
? unicodeCLDRtoBCP47(toLanguageString)
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
if (locale && i18n.languages?.[0] !== locale) {
|
if (localeBCP && i18n.languages?.[0] !== localeBCP) {
|
||||||
await i18n.changeLanguage(locale);
|
await i18n.changeLanguage(localeBCP);
|
||||||
await Desktop.bridge?.setSpellCheckerLanguages(["en-US", locale]);
|
await Desktop.bridge?.setSpellCheckerLanguages(["en-US", localeBCP]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -663,6 +663,21 @@
|
|||||||
"Continue with Email": "Continue with Email",
|
"Continue with Email": "Continue with Email",
|
||||||
"Continue with {{ authProviderName }}": "Continue with {{ authProviderName }}",
|
"Continue with {{ authProviderName }}": "Continue with {{ authProviderName }}",
|
||||||
"Back to home": "Back to home",
|
"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",
|
"Login": "Login",
|
||||||
"Error": "Error",
|
"Error": "Error",
|
||||||
"Failed to load configuration.": "Failed to load configuration.",
|
"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.",
|
"You signed in with {{ authProviderName }} last time.": "You signed in with {{ authProviderName }} last time.",
|
||||||
"Or": "Or",
|
"Or": "Or",
|
||||||
"Already have an account? Go to <1>login</1>.": "Already have an account? Go to <1>login</1>.",
|
"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 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