diff --git a/frontend/package.json b/frontend/package.json index 3336077c..07853ca3 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -63,7 +63,7 @@ "format": "yarn prettier -- --write", "lint:fix": "eslint --fix --ext .ts --ext .tsx .", "locale-extract": "formatjs extract 'src/**/*.tsx' --out-file src/locale/src/en.json", - "locale-compile": "formatjs compile-folder src/locale/src src/locale/lang --ast" + "locale-compile": "formatjs compile-folder src/locale/src src/locale/lang" }, "browserslist": { "production": [ diff --git a/frontend/public/index.html b/frontend/public/index.html index cd475284..3199d60e 100644 --- a/frontend/public/index.html +++ b/frontend/public/index.html @@ -55,6 +55,10 @@ rel="stylesheet" href="https://unpkg.com/@tabler/core@1.0.0-beta3/dist/css/tabler.min.css" /> + diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index f8dcf294..c05c53cd 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,18 +1,20 @@ import React from "react"; import Router from "components/Router"; -import { AuthProvider, HealthProvider } from "context"; +import { AuthProvider, HealthProvider, LocaleProvider } from "context"; import { intl } from "locale"; import { RawIntlProvider } from "react-intl"; function App() { return ( - - - - - + + + + + + + ); } diff --git a/frontend/src/components/Dropdown/DropdownItem.tsx b/frontend/src/components/Dropdown/DropdownItem.tsx index 8ba5a479..db29acae 100644 --- a/frontend/src/components/Dropdown/DropdownItem.tsx +++ b/frontend/src/components/Dropdown/DropdownItem.tsx @@ -45,6 +45,7 @@ export const DropdownItem: React.FC = ({ icon, href, onClick, + ...rest }) => { return divider ? (
@@ -57,7 +58,8 @@ export const DropdownItem: React.FC = ({ className, )} href={href} - onClick={onClick}> + onClick={onClick} + {...rest}> {icon && {icon}} {children} diff --git a/frontend/src/components/Flag/Flag.tsx b/frontend/src/components/Flag/Flag.tsx new file mode 100644 index 00000000..a52db3d8 --- /dev/null +++ b/frontend/src/components/Flag/Flag.tsx @@ -0,0 +1,28 @@ +import React from "react"; + +import cn from "classnames"; + +export interface FlagProps { + /** + * Additional Class + */ + className?: string; + /** + * Country code of flag + */ + country: string; + /** + * Size of the flag + */ + size?: string; +} +export const Flag: React.FC = ({ className, country, size }) => { + const classes = [ + `flag-country-${country.toLowerCase()}`, + { + [`flag-${size}`]: !!size, + }, + ]; + + return ; +}; diff --git a/frontend/src/components/Flag/index.ts b/frontend/src/components/Flag/index.ts new file mode 100644 index 00000000..62845cc1 --- /dev/null +++ b/frontend/src/components/Flag/index.ts @@ -0,0 +1 @@ +export * from "./Flag"; diff --git a/frontend/src/components/LocalePicker.tsx b/frontend/src/components/LocalePicker.tsx new file mode 100644 index 00000000..30d44d54 --- /dev/null +++ b/frontend/src/components/LocalePicker.tsx @@ -0,0 +1,67 @@ +import React, { useState } from "react"; + +import { Button, Dropdown, Flag } from "components"; +import { useLocaleState } from "context"; +import { changeLocale, getFlagCodeForLocale, getLocale, intl } from "locale"; + +export interface LocalPickerProps { + /** + * On click handler + */ + onChange?: any; +} + +export const LocalePicker: React.FC = ({ + onChange, + ...rest +}) => { + const { locale, setLocale } = useLocaleState(); + + // const [locale, setLocale] = useState(getLocale()); + const [localeShown, setLocaleShown] = useState(false); + + const handleOnChange = (e: any) => { + changeLocale(e.currentTarget.rel); + setLocale(e.currentTarget.rel); + setLocaleShown(false); + onChange && onChange(locale); + }; + + const options = [ + ["us", "en-US"], + ["de", "de-DE"], + ["ir", "fa-IR"], + ]; + + return ( +
+ + + {options.map((item) => { + return ( + } + onClick={handleOnChange}> + {intl.formatMessage({ + id: `locale-${item[1]}`, + defaultMessage: item[1], + })} + + ); + })} + +
+ ); +}; diff --git a/frontend/src/components/SiteWrapper.tsx b/frontend/src/components/SiteWrapper.tsx index b1baf755..0132cb0f 100644 --- a/frontend/src/components/SiteWrapper.tsx +++ b/frontend/src/components/SiteWrapper.tsx @@ -2,9 +2,9 @@ import React, { ReactNode } from "react"; import { Footer } from "components"; import { Avatar, Dropdown, Navigation } from "components"; +import { LocalePicker } from "components"; import { useAuthState, useUserState } from "context"; import { intl } from "locale"; -import { FormattedMessage } from "react-intl"; import styled from "styled-components"; import { NavMenu } from "./NavMenu"; @@ -45,16 +45,20 @@ function SiteWrapper({ children }: Props) { defaultMessage: "Standard User", }) } + buttons={[]} profileItems={[ - + {intl.formatMessage({ + id: "profile.title", + defaultMessage: "Profile settings", + })} , , - + {intl.formatMessage({ + id: "profile.logout", + defaultMessage: "Logout", + })} , ]} /> diff --git a/frontend/src/components/index.ts b/frontend/src/components/index.ts index 0aae858f..0afd41e0 100644 --- a/frontend/src/components/index.ts +++ b/frontend/src/components/index.ts @@ -5,9 +5,11 @@ export * from "./Badge"; export * from "./Button"; export * from "./ButtonList"; export * from "./Dropdown"; +export * from "./Flag"; export * from "./Footer"; export * from "./Loader"; export * from "./Loading"; +export * from "./LocalePicker"; export * from "./Navigation"; export * from "./NavMenu"; export * from "./Router"; diff --git a/frontend/src/context/LocaleContext.tsx b/frontend/src/context/LocaleContext.tsx new file mode 100644 index 00000000..f23f0785 --- /dev/null +++ b/frontend/src/context/LocaleContext.tsx @@ -0,0 +1,43 @@ +import React, { ReactNode, useState } from "react"; + +import { getLocale } from "locale"; + +// Context +export interface LocaleContextType { + setLocale: (locale: string) => void; + locale?: string; +} + +const initalValue = null; +const LocaleContext = React.createContext( + initalValue, +); + +// Provider +interface Props { + children?: ReactNode; +} +function LocaleProvider({ children }: Props) { + const [locale, setLocaleValue] = useState(getLocale()); + + const setLocale = async (locale: string) => { + setLocaleValue(locale); + }; + + const value = { locale, setLocale }; + + return ( + {children} + ); +} + +function useLocaleState() { + const context = React.useContext(LocaleContext); + if (!context) { + throw new Error(`useLocaleState must be used within a LocaleProvider`); + } + return context; +} + +export { LocaleProvider, useLocaleState }; +export default LocaleContext; diff --git a/frontend/src/context/index.ts b/frontend/src/context/index.ts index bc8b3f26..5e327e35 100644 --- a/frontend/src/context/index.ts +++ b/frontend/src/context/index.ts @@ -1,3 +1,4 @@ export * from "./AuthContext"; export * from "./HealthContext"; +export * from "./LocaleContext"; export * from "./UserContext"; diff --git a/frontend/src/locale/IntlProvider.tsx b/frontend/src/locale/IntlProvider.tsx index db422e7b..3c721c3b 100644 --- a/frontend/src/locale/IntlProvider.tsx +++ b/frontend/src/locale/IntlProvider.tsx @@ -1,25 +1,53 @@ import { createIntl, createIntlCache } from "react-intl"; +import langDe from "./lang/de.json"; import langEn from "./lang/en.json"; +import langFa from "./lang/fa.json"; -const loadMessages = (locale: string) => { - switch (locale) { - /* - case 'fr': - return import("./lang/fr.json"); - */ +const loadMessages = (locale?: string) => { + locale = locale || "en"; + switch (locale.substr(0, 2)) { + case "de": + return Object.assign({}, langEn, langDe); + case "fa": + return Object.assign({}, langEn, langFa); default: return langEn; } }; -export const initialLocale = "en-US"; -export const cache = createIntlCache(); +export const getFlagCodeForLocale = (locale?: string) => { + switch (locale) { + case "de-DE": + case "de": + return "de"; + case "fa-IR": + case "fa": + return "ir"; + default: + return "us"; + } +}; -const initialMessages = loadMessages(initialLocale); +export const getLocale = () => { + let loc = window.localStorage.getItem("locale"); + if (!loc) { + loc = document.documentElement.lang; + } + return loc; +}; -export const intl = createIntl( - // @ts-ignore messages file typings are correct - { locale: initialLocale, messages: initialMessages }, +const cache = createIntlCache(); + +const initialMessages = loadMessages(getLocale()); +export let intl = createIntl( + { locale: getLocale(), messages: initialMessages }, cache, ); + +export const changeLocale = (locale: string): void => { + const messages = loadMessages(locale); + intl = createIntl({ locale, messages }, cache); + window.localStorage.setItem("locale", locale); + document.documentElement.lang = locale; +}; diff --git a/frontend/src/locale/src/de.json b/frontend/src/locale/src/de.json new file mode 100644 index 00000000..c0f410af --- /dev/null +++ b/frontend/src/locale/src/de.json @@ -0,0 +1,20 @@ +{ + "accesslists.title": { + "defaultMessage": "Zugriffslisten" + }, + "auditlog.title": { + "defaultMessage": "Audit-Log" + }, + "setup.create": { + "defaultMessage": "Benutzerkonto erstellen" + }, + "setup.title": { + "defaultMessage": "Erstellen Sie Ihr erstes Konto" + }, + "user.nickname": { + "defaultMessage": "Spitzname" + }, + "user.password": { + "defaultMessage": "Passwort" + } +} diff --git a/frontend/src/locale/src/en.json b/frontend/src/locale/src/en.json index 1bcbfac8..979aadd5 100644 --- a/frontend/src/locale/src/en.json +++ b/frontend/src/locale/src/en.json @@ -1,80 +1,89 @@ { - "accesslists.title": { - "defaultMessage": "Access Lists" - }, - "auditlog.title": { - "defaultMessage": "Audit Log" - }, - "certificates.title": { - "defaultMessage": "Certificates" - }, - "column.description": { - "defaultMessage": "Description" - }, - "column.id": { - "defaultMessage": "ID" - }, - "column.name": { - "defaultMessage": "Name" - }, - "dashboard.title": { - "defaultMessage": "Dashboard" - }, - "footer.changelog": { - "defaultMessage": "Change Log" - }, - "footer.copyright": { - "defaultMessage": "Copyright © {year} jc21.com." - }, - "footer.github": { - "defaultMessage": "Github" - }, - "footer.theme": { - "defaultMessage": "Theme by Tabler" - }, - "footer.userguide": { - "defaultMessage": "User Guide" - }, - "hosts.title": { - "defaultMessage": "Hosts" - }, - "login.login": { - "defaultMessage": "Sign in" - }, - "profile.logout": { - "defaultMessage": "Logout" - }, - "profile.title": { - "defaultMessage": "Profile settings" - }, - "settings.title": { - "defaultMessage": "Settings" - }, - "setup.create": { - "defaultMessage": "Create Account" - }, - "setup.title": { - "defaultMessage": "Create your first Account" - }, - "user.email": { - "defaultMessage": "Email" - }, - "user.name": { - "defaultMessage": "Name" - }, - "user.nickname": { - "defaultMessage": "Nickname" - }, - "user.password": { - "defaultMessage": "Password" - }, - "users.admin": { - "defaultMessage": "Administrator" - }, - "users.standard": { - "defaultMessage": "Standard User" - }, - "users.title": { - "defaultMessage": "Users" - } + "accesslists.title": { + "defaultMessage": "Access Lists" + }, + "auditlog.title": { + "defaultMessage": "Audit Log" + }, + "certificates.title": { + "defaultMessage": "Certificates" + }, + "column.description": { + "defaultMessage": "Description" + }, + "column.id": { + "defaultMessage": "ID" + }, + "column.name": { + "defaultMessage": "Name" + }, + "dashboard.title": { + "defaultMessage": "Dashboard" + }, + "footer.changelog": { + "defaultMessage": "Change Log" + }, + "footer.copyright": { + "defaultMessage": "Copyright © {year} jc21.com." + }, + "footer.github": { + "defaultMessage": "Github" + }, + "footer.theme": { + "defaultMessage": "Theme by Tabler" + }, + "footer.userguide": { + "defaultMessage": "User Guide" + }, + "hosts.title": { + "defaultMessage": "Hosts" + }, + "locale-de-DE": { + "defaultMessage": "Deutsche" + }, + "locale-en-US": { + "defaultMessage": "English" + }, + "locale-fa-IR": { + "defaultMessage": "Persian" + }, + "login.login": { + "defaultMessage": "Sign in" + }, + "profile.logout": { + "defaultMessage": "Logout" + }, + "profile.title": { + "defaultMessage": "Profile settings" + }, + "settings.title": { + "defaultMessage": "Settings" + }, + "setup.create": { + "defaultMessage": "Sign up" + }, + "setup.title": { + "defaultMessage": "Create your first Account" + }, + "user.email": { + "defaultMessage": "Email" + }, + "user.name": { + "defaultMessage": "Name" + }, + "user.nickname": { + "defaultMessage": "Nickname" + }, + "user.password": { + "defaultMessage": "Password" + }, + "users.admin": { + "defaultMessage": "Administrator" + }, + "users.standard": { + "defaultMessage": "Standard User" + }, + "users.title": { + "defaultMessage": "Users" + } } diff --git a/frontend/src/locale/src/fa.json b/frontend/src/locale/src/fa.json new file mode 100644 index 00000000..f912b8d8 --- /dev/null +++ b/frontend/src/locale/src/fa.json @@ -0,0 +1,77 @@ +{ + "accesslists.title": { + "defaultMessage": "دسترسی به لیست ها" + }, + "auditlog.title": { + "defaultMessage": "گزارش حسابرسی" + }, + "certificates.title": { + "defaultMessage": "گواهینامه ها" + }, + "column.description": { + "defaultMessage": "شرح" + }, + "column.id": { + "defaultMessage": "شناسه" + }, + "column.name": { + "defaultMessage": "نام" + }, + "dashboard.title": { + "defaultMessage": "داشبورد" + }, + "footer.changelog": { + "defaultMessage": "ورود به سیستم را تغییر دهید" + }, + "footer.copyright": { + "defaultMessage": "حق چاپ © حق چاپ © {year} jc21.com" + }, + "footer.theme": { + "defaultMessage": "قالب توسط Tabler" + }, + "footer.userguide": { + "defaultMessage": "راهنمای کاربر" + }, + "hosts.title": { + "defaultMessage": "میزبان" + }, + "login.login": { + "defaultMessage": "ورود" + }, + "profile.logout": { + "defaultMessage": "خروج" + }, + "profile.title": { + "defaultMessage": "تنظیمات نمایه" + }, + "settings.title": { + "defaultMessage": "تنظیمات" + }, + "setup.create": { + "defaultMessage": "ثبت نام" + }, + "setup.title": { + "defaultMessage": "اولین حساب خود را ایجاد کنید" + }, + "user.email": { + "defaultMessage": "پست الکترونیک" + }, + "user.name": { + "defaultMessage": "نام" + }, + "user.nickname": { + "defaultMessage": "کنیه" + }, + "user.password": { + "defaultMessage": "کلمه عبور" + }, + "users.admin": { + "defaultMessage": "مدیر" + }, + "users.standard": { + "defaultMessage": "کاربر استاندارد" + }, + "users.title": { + "defaultMessage": "کاربران" + } +} diff --git a/frontend/src/pages/Setup/index.tsx b/frontend/src/pages/Setup/index.tsx index 41907591..a93dfecf 100644 --- a/frontend/src/pages/Setup/index.tsx +++ b/frontend/src/pages/Setup/index.tsx @@ -2,9 +2,9 @@ import React, { useEffect, useRef, useState, ChangeEvent } from "react"; import { createUser } from "api/npm"; import { Alert, Button } from "components"; +import { LocalePicker } from "components"; import { useAuthState, useHealthState } from "context"; import { intl } from "locale"; -import { FormattedMessage } from "react-intl"; import logo from "../../img/logo-text-vertical-grey.png"; @@ -13,6 +13,7 @@ function Setup() { const { refreshHealth } = useHealthState(); const { login } = useAuthState(); const [loading, setLoading] = useState(false); + const [renderCount, setRenderCount] = useState(0); const [errorMessage, setErrorMessage] = useState(""); const [formData, setFormData] = useState({ @@ -82,12 +83,24 @@ function Setup() { autoComplete="off" onSubmit={onSubmit}>
-

- -

+
+
+

+ {intl.formatMessage({ + id: "setup.title", + defaultMessage: "Create your first Account", + })} +

+
+
+ { + setRenderCount(renderCount + 1); + }} + /> +
+
+ {errorMessage ? ( {errorMessage} @@ -96,7 +109,10 @@ function Setup() {