Allow users to change language

This commit is contained in:
dominik-zeglen 2019-10-16 17:18:29 +02:00
parent cce276db3a
commit 54dad7cae5
18 changed files with 194 additions and 47 deletions

6
package-lock.json generated
View file

@ -4953,9 +4953,9 @@
} }
}, },
"babel-plugin-react-intl-auto": { "babel-plugin-react-intl-auto": {
"version": "2.2.0", "version": "2.3.0",
"resolved": "https://registry.npmjs.org/babel-plugin-react-intl-auto/-/babel-plugin-react-intl-auto-2.2.0.tgz", "resolved": "https://registry.npmjs.org/babel-plugin-react-intl-auto/-/babel-plugin-react-intl-auto-2.3.0.tgz",
"integrity": "sha512-L6IS4NQCr+uGw8yOJ+tBfm5R0UhrM2mZyhN+X7jCsnEhTcWopkWe7geLm7AzJC2SWFXnr7phwXlJbN4erwKRrA==", "integrity": "sha512-y9QSRHkuN2eAPlvhvvJCbWjxeXhd0kbFLksLkMgaqX7VAiWON04rTSfq4PVbvgfvs5MHdLLbGrxBEWQ129QVIA==",
"dev": true, "dev": true,
"requires": { "requires": {
"@babel/types": "^7.5.5", "@babel/types": "^7.5.5",

View file

@ -104,7 +104,7 @@
"babel-jest": "^23.6.0", "babel-jest": "^23.6.0",
"babel-loader": "^8.0.6", "babel-loader": "^8.0.6",
"babel-plugin-react-intl": "^4.1.12", "babel-plugin-react-intl": "^4.1.12",
"babel-plugin-react-intl-auto": "^2.2.0", "babel-plugin-react-intl-auto": "^2.3.0",
"codecov": "^3.5.0", "codecov": "^3.5.0",
"core-js": "^3.2.1", "core-js": "^3.2.1",
"enzyme": "^3.10.0", "enzyme": "^3.10.0",

View file

@ -29,6 +29,7 @@ import useNavigator from "@saleor/hooks/useNavigator";
import useTheme from "@saleor/hooks/useTheme"; import useTheme from "@saleor/hooks/useTheme";
import useUser from "@saleor/hooks/useUser"; import useUser from "@saleor/hooks/useUser";
import ArrowDropdown from "@saleor/icons/ArrowDropdown"; import ArrowDropdown from "@saleor/icons/ArrowDropdown";
import { staffMemberDetailsUrl } from "@saleor/staff/urls";
import Container from "../Container"; import Container from "../Container";
import AppActionContext from "./AppActionContext"; import AppActionContext from "./AppActionContext";
import AppHeaderContext from "./AppHeaderContext"; import AppHeaderContext from "./AppHeaderContext";
@ -287,10 +288,15 @@ const AppLayout = withStyles(styles, {
const menuStructure = createMenuStructure(intl); const menuStructure = createMenuStructure(intl);
const handleLogout = () => { const handleLogout = () => {
close(); setMenuState(false);
logout(); logout();
}; };
const handleViewerProfile = () => {
setMenuState(false);
navigate(staffMemberDetailsUrl(user.id));
};
const handleMenuItemClick = ( const handleMenuItemClick = (
url: string, url: string,
event: React.MouseEvent<any> event: React.MouseEvent<any>
@ -434,6 +440,15 @@ const AppLayout = withStyles(styles, {
mouseEvent="onClick" mouseEvent="onClick"
> >
<Menu> <Menu>
<MenuItem
className={classes.userMenuItem}
onClick={handleViewerProfile}
>
<FormattedMessage
defaultMessage="Account Settings"
description="button"
/>
</MenuItem>
<MenuItem <MenuItem
className={classes.userMenuItem} className={classes.userMenuItem}
onClick={handleLogout} onClick={handleLogout}

View file

@ -20,7 +20,7 @@ export const Date: React.FC<DateProps> = ({ date, plain }) => {
return ( return (
<LocaleConsumer> <LocaleConsumer>
{locale => ( {({ locale }) => (
<Consumer> <Consumer>
{currentDate => {currentDate =>
plain ? ( plain ? (

View file

@ -27,7 +27,7 @@ export const DateTime: React.StatelessComponent<DateTimeProps> = ({
<TimezoneConsumer> <TimezoneConsumer>
{tz => ( {tz => (
<LocaleConsumer> <LocaleConsumer>
{locale => ( {({ locale }) => (
<Consumer> <Consumer>
{currentDate => {currentDate =>
plain ? ( plain ? (

View file

@ -43,15 +43,6 @@ import locale_VI from "@locale/vi.json";
import locale_ZH_HANS from "@locale/zh-Hans.json"; import locale_ZH_HANS from "@locale/zh-Hans.json";
import locale_ZH_HANT from "@locale/zh-Hant.json"; import locale_ZH_HANT from "@locale/zh-Hant.json";
const defaultLocale = "en";
export type LocaleContextType = string;
export const LocaleContext = React.createContext<LocaleContextType>(
defaultLocale
);
const { Consumer: LocaleConsumer, Provider: RawLocaleProvider } = LocaleContext;
export enum Locale { export enum Locale {
AR = "ar", AR = "ar",
AZ = "az", AZ = "az",
@ -144,6 +135,51 @@ const localeData: Record<Locale, LocaleMessages> = {
[Locale.ZH_HANT]: locale_ZH_HANT [Locale.ZH_HANT]: locale_ZH_HANT
}; };
export const localeNames: Record<Locale, string> = {
[Locale.AR]: "العربيّة",
[Locale.AZ]: "Azərbaycanca",
[Locale.BG]: "български",
[Locale.BN]: "বাংলা",
[Locale.CA]: "català",
[Locale.CS]: "česky",
[Locale.DA]: "dansk",
[Locale.DE]: "Deutsch",
[Locale.EL]: "Ελληνικά",
[Locale.EN]: "English",
[Locale.ES]: "español",
[Locale.ES_CO]: "español de Colombia",
[Locale.ET]: "eesti",
[Locale.FA]: "فارسی",
[Locale.FR]: "français",
[Locale.HI]: "Hindi",
[Locale.HU]: "Magyar",
[Locale.HY]: "հայերեն",
[Locale.ID]: "Bahasa Indonesia",
[Locale.IS]: "Íslenska",
[Locale.IT]: "italiano",
[Locale.JA]: "日本語",
[Locale.KO]: "한국어",
[Locale.MN]: "Mongolian",
[Locale.NB]: "norsk (bokmål)",
[Locale.NL]: "Nederlands",
[Locale.PL]: "polski",
[Locale.PT]: "Português",
[Locale.PT_BR]: "Português Brasileiro",
[Locale.RO]: "Română",
[Locale.RU]: "Русский",
[Locale.SK]: "Slovensky",
[Locale.SL]: "Slovenščina",
[Locale.SQ]: "shqip",
[Locale.SR]: "српски",
[Locale.SV]: "svenska",
[Locale.TH]: "ภาษาไทย",
[Locale.TR]: "Türkçe",
[Locale.UK]: "Українська",
[Locale.VI]: "Tiếng Việt",
[Locale.ZH_HANS]: "简体中文",
[Locale.ZH_HANT]: "繁體中文"
};
export function getMatchingLocale(languages: readonly string[]): Locale { export function getMatchingLocale(languages: readonly string[]): Locale {
const localeEntries = Object.entries(Locale); const localeEntries = Object.entries(Locale);
@ -158,8 +194,21 @@ export function getMatchingLocale(languages: readonly string[]): Locale {
return undefined; return undefined;
} }
const defaultLocale = Locale.EN;
export interface LocaleContextType {
locale: Locale;
setLocale: (locale: Locale) => void;
}
export const LocaleContext = React.createContext<LocaleContextType>({
locale: defaultLocale,
setLocale: () => undefined
});
const { Consumer: LocaleConsumer, Provider: RawLocaleProvider } = LocaleContext;
const LocaleProvider: React.FC = ({ children }) => { const LocaleProvider: React.FC = ({ children }) => {
const [locale] = React.useState( const [locale, setLocale] = React.useState(
getMatchingLocale(navigator.languages) || defaultLocale getMatchingLocale(navigator.languages) || defaultLocale
); );
@ -175,7 +224,14 @@ const LocaleProvider: React.FC = ({ children }) => {
}} }}
key={locale} key={locale}
> >
<RawLocaleProvider value={locale}>{children}</RawLocaleProvider> <RawLocaleProvider
value={{
locale,
setLocale
}}
>
{children}
</RawLocaleProvider>
</IntlProvider> </IntlProvider>
); );
}; };

View file

@ -12,7 +12,7 @@ export interface MoneyProps {
export const Money: React.StatelessComponent<MoneyProps> = ({ money }) => ( export const Money: React.StatelessComponent<MoneyProps> = ({ money }) => (
<LocaleConsumer> <LocaleConsumer>
{locale => { {({ locale }) => {
return money.amount.toLocaleString(locale, { return money.amount.toLocaleString(locale, {
currency: money.currency, currency: money.currency,
style: "currency" style: "currency"

View file

@ -1,5 +1,6 @@
import React from "react"; import React from "react";
import { useIntl } from "react-intl"; import { useIntl } from "react-intl";
import { LocaleConsumer } from "../Locale"; import { LocaleConsumer } from "../Locale";
import IMoney from "../Money"; import IMoney from "../Money";
@ -22,7 +23,7 @@ export const MoneyRange: React.StatelessComponent<MoneyRangeProps> = ({
return ( return (
<LocaleConsumer> <LocaleConsumer>
{locale => {({ locale }) =>
from && to from && to
? intl.formatMessage( ? intl.formatMessage(
{ {

View file

@ -8,7 +8,7 @@ interface PercentProps {
const Percent: React.StatelessComponent<PercentProps> = ({ amount }) => ( const Percent: React.StatelessComponent<PercentProps> = ({ amount }) => (
<LocaleConsumer> <LocaleConsumer>
{locale => { {({ locale }) => {
return (amount / 100).toLocaleString(locale, { return (amount / 100).toLocaleString(locale, {
maximumFractionDigits: 2, maximumFractionDigits: 2,
style: "percent" style: "percent"

View file

@ -4,7 +4,7 @@ import { useContext } from "react";
import { LocaleContext } from "@saleor/components/Locale"; import { LocaleContext } from "@saleor/components/Locale";
function useDateLocalize(): (date: string) => string { function useDateLocalize(): (date: string) => string {
const locale = useContext(LocaleContext); const { locale } = useContext(LocaleContext);
return (date: string) => return (date: string) =>
moment(date) moment(date)

View file

@ -3,7 +3,7 @@ import { useContext } from "react";
import { LocaleContext } from "@saleor/components/Locale"; import { LocaleContext } from "@saleor/components/Locale";
function useLocale() { function useLocale() {
const themeInfo = useContext(LocaleContext); const localeInfo = useContext(LocaleContext);
return themeInfo; return localeInfo;
} }
export default useLocale; export default useLocale;

View file

@ -9,13 +9,16 @@ import { ConfirmButtonTransitionState } from "@saleor/components/ConfirmButton";
import Container from "@saleor/components/Container"; import Container from "@saleor/components/Container";
import Form from "@saleor/components/Form"; import Form from "@saleor/components/Form";
import Grid from "@saleor/components/Grid"; import Grid from "@saleor/components/Grid";
import { localeNames } from "@saleor/components/Locale";
import PageHeader from "@saleor/components/PageHeader"; import PageHeader from "@saleor/components/PageHeader";
import SaveButtonBar from "@saleor/components/SaveButtonBar"; import SaveButtonBar from "@saleor/components/SaveButtonBar";
import { ShopInfo_shop_permissions } from "@saleor/components/Shop/types/ShopInfo"; import { ShopInfo_shop_permissions } from "@saleor/components/Shop/types/ShopInfo";
import useLocale from "@saleor/hooks/useLocale";
import { sectionNames } from "@saleor/intl"; import { sectionNames } from "@saleor/intl";
import { getUserName, maybe } from "../../../misc"; import { getUserName, maybe } from "../../../misc";
import { PermissionEnum } from "../../../types/globalTypes"; import { PermissionEnum } from "../../../types/globalTypes";
import { StaffMemberDetails_user } from "../../types/StaffMemberDetails"; import { StaffMemberDetails_user } from "../../types/StaffMemberDetails";
import StaffPreferences from "../StaffPreferences";
import StaffProperties from "../StaffProperties/StaffProperties"; import StaffProperties from "../StaffProperties/StaffProperties";
interface FormData { interface FormData {
@ -29,6 +32,7 @@ interface FormData {
export interface StaffDetailsPageProps { export interface StaffDetailsPageProps {
canEditAvatar: boolean; canEditAvatar: boolean;
canEditPreferences: boolean;
canEditStatus: boolean; canEditStatus: boolean;
canRemove: boolean; canRemove: boolean;
disabled: boolean; disabled: boolean;
@ -44,6 +48,7 @@ export interface StaffDetailsPageProps {
const StaffDetailsPage: React.StatelessComponent<StaffDetailsPageProps> = ({ const StaffDetailsPage: React.StatelessComponent<StaffDetailsPageProps> = ({
canEditAvatar, canEditAvatar,
canEditPreferences,
canEditStatus, canEditStatus,
canRemove, canRemove,
disabled, disabled,
@ -57,6 +62,7 @@ const StaffDetailsPage: React.StatelessComponent<StaffDetailsPageProps> = ({
onSubmit onSubmit
}: StaffDetailsPageProps) => { }: StaffDetailsPageProps) => {
const intl = useIntl(); const intl = useIntl();
const { locale, setLocale } = useLocale();
const initialForm: FormData = { const initialForm: FormData = {
email: maybe(() => staffMember.email, ""), email: maybe(() => staffMember.email, ""),
@ -75,6 +81,7 @@ const StaffDetailsPage: React.StatelessComponent<StaffDetailsPageProps> = ({
lastName: maybe(() => staffMember.lastName, ""), lastName: maybe(() => staffMember.lastName, ""),
permissions: maybe(() => staffMember.permissions, []).map(perm => perm.code) permissions: maybe(() => staffMember.permissions, []).map(perm => perm.code)
}; };
return ( return (
<Form initial={initialForm} onSubmit={onSubmit} confirmLeave> <Form initial={initialForm} onSubmit={onSubmit} confirmLeave>
{({ data, change, hasChanged, submit }) => ( {({ data, change, hasChanged, submit }) => (
@ -95,26 +102,31 @@ const StaffDetailsPage: React.StatelessComponent<StaffDetailsPageProps> = ({
onImageDelete={onImageDelete} onImageDelete={onImageDelete}
/> />
</div> </div>
{canEditStatus && ( <div>
<div> {canEditPreferences && (
<AccountPermissions <StaffPreferences locale={locale} onLocaleChange={setLocale} />
data={data} )}
disabled={disabled} {canEditStatus && (
permissions={permissions} <>
onChange={change} <AccountPermissions
/> data={data}
<CardSpacer /> disabled={disabled}
<AccountStatus permissions={permissions}
data={data} onChange={change}
disabled={disabled} />
label={intl.formatMessage({ <CardSpacer />
defaultMessage: "User is active", <AccountStatus
description: "checkbox label" data={data}
})} disabled={disabled}
onChange={change} label={intl.formatMessage({
/> defaultMessage: "User is active",
</div> description: "checkbox label"
)} })}
onChange={change}
/>
</>
)}
</div>
</Grid> </Grid>
<SaveButtonBar <SaveButtonBar
disabled={disabled || !hasChanged} disabled={disabled || !hasChanged}

View file

@ -0,0 +1,52 @@
import Card from "@material-ui/core/Card";
import CardContent from "@material-ui/core/CardContent";
import React from "react";
import { useIntl } from "react-intl";
import CardTitle from "@saleor/components/CardTitle";
import { Locale, localeNames } from "@saleor/components/Locale";
import SingleAutocompleteSelectField from "@saleor/components/SingleAutocompleteSelectField";
interface StaffPreferencesProps {
locale: Locale;
onLocaleChange: (locale: Locale) => void;
}
const StaffPreferences: React.StatelessComponent<StaffPreferencesProps> = ({
locale,
onLocaleChange
}) => {
const intl = useIntl();
return (
<Card>
<CardTitle
title={intl.formatMessage({
defaultMessage: "Preferences",
description: "section header"
})}
/>
<CardContent>
<SingleAutocompleteSelectField
choices={Object.values(Locale).map(locale => ({
label: localeNames[locale],
value: locale
}))}
displayValue={localeNames[locale]}
helperText={intl.formatMessage({
defaultMessage:
"Selecting this will change the language of your dashboard"
})}
label={intl.formatMessage({
defaultMessage: "Preferred Language"
})}
name="locale"
value={locale}
onChange={event => onLocaleChange(event.target.value)}
/>
</CardContent>
</Card>
);
};
StaffPreferences.displayName = "StaffPreferences";
export default StaffPreferences;

View file

@ -0,0 +1,2 @@
export { default } from "./StaffPreferences";
export * from "./StaffPreferences";

View file

@ -126,6 +126,7 @@ export const StaffDetails: React.StatelessComponent<OrderListProps> = ({
/> />
<StaffDetailsPage <StaffDetailsPage
canEditAvatar={isUserSameAsViewer} canEditAvatar={isUserSameAsViewer}
canEditPreferences={isUserSameAsViewer}
canEditStatus={!isUserSameAsViewer} canEditStatus={!isUserSameAsViewer}
canRemove={!isUserSameAsViewer} canRemove={!isUserSameAsViewer}
disabled={loading} disabled={loading}

View file

@ -1,15 +1,20 @@
import React from "react"; import React from "react";
import { IntlProvider } from "react-intl"; import { IntlProvider } from "react-intl";
import { RawLocaleProvider } from "@saleor/components/Locale"; import { Locale, RawLocaleProvider } from "@saleor/components/Locale";
import { Provider as DateProvider } from "../components/Date/DateContext"; import { Provider as DateProvider } from "../components/Date/DateContext";
import { MessageManager } from "../components/messages"; import { MessageManager } from "../components/messages";
import ThemeProvider from "../components/Theme"; import ThemeProvider from "../components/Theme";
import { TimezoneProvider } from "../components/Timezone"; import { TimezoneProvider } from "../components/Timezone";
export const Decorator = storyFn => ( export const Decorator = storyFn => (
<IntlProvider defaultLocale="en" locale="en"> <IntlProvider defaultLocale={Locale.EN} locale={Locale.EN}>
<RawLocaleProvider value="en"> <RawLocaleProvider
value={{
locale: Locale.EN,
setLocale: () => undefined
}}
>
<DateProvider value={+new Date("2018-08-07T14:30:44+00:00")}> <DateProvider value={+new Date("2018-08-07T14:30:44+00:00")}>
<TimezoneProvider value="America/New_York"> <TimezoneProvider value="America/New_York">
<ThemeProvider isDefaultDark={false}> <ThemeProvider isDefaultDark={false}>

View file

@ -11,6 +11,7 @@ import Decorator from "../../Decorator";
const props: Omit<StaffDetailsPageProps, "classes"> = { const props: Omit<StaffDetailsPageProps, "classes"> = {
canEditAvatar: false, canEditAvatar: false,
canEditPreferences: false,
canEditStatus: true, canEditStatus: true,
canRemove: true, canRemove: true,
disabled: false, disabled: false,
@ -45,5 +46,6 @@ storiesOf("Views / Staff / Staff member details", module)
canEditStatus={false} canEditStatus={false}
canRemove={false} canRemove={false}
canEditAvatar={true} canEditAvatar={true}
canEditPreferences={true}
/> />
)); ));

View file

@ -526,6 +526,7 @@ export default (colors: IThemeColors): Theme =>
body2: { body2: {
fontSize: "1rem" fontSize: "1rem"
}, },
fontFamily,
h4: { h4: {
color: colors.font.default color: colors.font.default
}, },