diff --git a/packages/console/package.json b/packages/console/package.json index 847946d28..b6d1c21df 100644 --- a/packages/console/package.json +++ b/packages/console/package.json @@ -17,8 +17,11 @@ "dependencies": { "@logto/phrases": "^0.1.0", "@logto/schemas": "^0.1.0", + "i18next": "^21.6.12", + "i18next-browser-languagedetector": "^6.1.3", "react": "^17.0.2", - "react-dom": "^17.0.2" + "react-dom": "^17.0.2", + "react-i18next": "^11.15.4" }, "devDependencies": { "@parcel/core": "^2.3.1", @@ -46,7 +49,11 @@ "rules": { "react/jsx-curly-brace-presence": [ "error", - { "props": "never", "children": "never", "propElementValues": "always" } + { + "props": "never", + "children": "never", + "propElementValues": "always" + } ] } }, diff --git a/packages/console/src/App.tsx b/packages/console/src/App.tsx index 096b00a83..68f8b2898 100644 --- a/packages/console/src/App.tsx +++ b/packages/console/src/App.tsx @@ -5,6 +5,9 @@ import * as styles from './App.module.scss'; import Content from './components/Content'; import Sidebar from './components/Sidebar'; import Topbar from './components/Topbar'; +import initI18n from './i18n/init'; + +void initI18n(); export const App = () => { return ( diff --git a/packages/console/src/components/Sidebar/components/Item/index.module.scss b/packages/console/src/components/Sidebar/components/Item/index.module.scss index f39aaf59b..3d4619ebc 100644 --- a/packages/console/src/components/Sidebar/components/Item/index.module.scss +++ b/packages/console/src/components/Sidebar/components/Item/index.module.scss @@ -3,7 +3,8 @@ .row { display: flex; align-items: center; - padding: _.unit(3) _.unit(6); + margin: 0 _.unit(5) 0 _.unit(2); + padding: _.unit(3) _.unit(4); color: var(--color-on-surface-variant); > div + div { diff --git a/packages/console/src/components/Sidebar/consts.tsx b/packages/console/src/components/Sidebar/consts.tsx new file mode 100644 index 000000000..4ba1595fd --- /dev/null +++ b/packages/console/src/components/Sidebar/consts.tsx @@ -0,0 +1,46 @@ +import { FC } from 'react'; +import { TFuncKey } from 'react-i18next'; + +import BarGraph from './icons/BarGraph'; +import Bolt from './icons/Bolt'; +import Box from './icons/Box'; +import Cloud from './icons/Cloud'; + +type SidebarItem = { + Icon: FC; + title: TFuncKey<'translation', 'admin_console.tabs'>; +}; + +type SidebarSection = { + title: TFuncKey<'translation', 'admin_console.tab_sections'>; + items: SidebarItem[]; +}; + +export const sections: SidebarSection[] = [ + { + title: 'overview', + items: [ + { + Icon: Bolt, + title: 'get_started', + }, + { + Icon: BarGraph, + title: 'dashboard', + }, + ], + }, + { + title: 'resource_management', + items: [ + { + Icon: Box, + title: 'applications', + }, + { + Icon: Cloud, + title: 'api_resources', + }, + ], + }, +]; diff --git a/packages/console/src/components/Sidebar/icons/Box.tsx b/packages/console/src/components/Sidebar/icons/Box.tsx new file mode 100644 index 000000000..d86b3cce8 --- /dev/null +++ b/packages/console/src/components/Sidebar/icons/Box.tsx @@ -0,0 +1,11 @@ +import React from 'react'; + +const Box = () => { + return ( + + + + ); +}; + +export default Box; diff --git a/packages/console/src/components/Sidebar/icons/Cloud.tsx b/packages/console/src/components/Sidebar/icons/Cloud.tsx new file mode 100644 index 000000000..47a310c1e --- /dev/null +++ b/packages/console/src/components/Sidebar/icons/Cloud.tsx @@ -0,0 +1,11 @@ +import React from 'react'; + +const Cloud = () => { + return ( + + + + ); +}; + +export default Cloud; diff --git a/packages/console/src/components/Sidebar/index.tsx b/packages/console/src/components/Sidebar/index.tsx index 74e630c36..e0f60f09d 100644 --- a/packages/console/src/components/Sidebar/index.tsx +++ b/packages/console/src/components/Sidebar/index.tsx @@ -1,22 +1,28 @@ import React from 'react'; +import { useTranslation } from 'react-i18next'; import Item from './components/Item'; import Section from './components/Section'; -import BarGraph from './icons/BarGraph'; -import Bolt from './icons/Bolt'; +import { sections } from './consts'; import * as styles from './index.module.scss'; const Sidebar = () => { + const { t: tSection } = useTranslation(undefined, { + keyPrefix: 'admin_console.tab_sections', + }); + const { t: tItem } = useTranslation(undefined, { + keyPrefix: 'admin_console.tabs', + }); + return (
-
- } title="Get Started" /> - } title="Dashboard" /> -
-
- } title="Get Started" /> - } title="Dashboard" /> -
+ {sections.map(({ title, items }) => ( +
+ {items.map(({ title, Icon }) => ( + } /> + ))} +
+ ))}
); }; diff --git a/packages/console/src/i18n/init.ts b/packages/console/src/i18n/init.ts new file mode 100644 index 000000000..142bc4129 --- /dev/null +++ b/packages/console/src/i18n/init.ts @@ -0,0 +1,18 @@ +import resources from '@logto/phrases'; +import i18next from 'i18next'; +import LanguageDetector from 'i18next-browser-languagedetector'; +import { initReactI18next } from 'react-i18next'; + +const initI18n = async () => + i18next + .use(initReactI18next) + .use(LanguageDetector) + .init({ + resources, + fallbackLng: 'en', + interpolation: { + escapeValue: false, + }, + }); + +export default initI18n; diff --git a/packages/console/src/include.d/react-i18next.d.ts b/packages/console/src/include.d/react-i18next.d.ts new file mode 100644 index 000000000..adbe43039 --- /dev/null +++ b/packages/console/src/include.d/react-i18next.d.ts @@ -0,0 +1,11 @@ +// https://react.i18next.com/latest/typescript#create-a-declaration-file + +// eslint-disable-next-line import/no-unassigned-import +import 'react-i18next'; +import en from '@logto/phrases/lib/locales/en.js'; + +declare module 'react-i18next' { + interface CustomTypeOptions { + resources: typeof en; + } +} diff --git a/packages/phrases/src/index.ts b/packages/phrases/src/index.ts index 27faba2dd..7255520c1 100644 --- a/packages/phrases/src/index.ts +++ b/packages/phrases/src/index.ts @@ -7,6 +7,7 @@ import { Resource } from './types'; export type LogtoErrorCode = NormalizeKeyPaths; export type LogtoErrorI18nKey = `errors:${LogtoErrorCode}`; export type Languages = keyof Resource; +export type I18nKey = NormalizeKeyPaths; const resource: Resource = { en, diff --git a/packages/phrases/src/locales/en.ts b/packages/phrases/src/locales/en.ts index fffe0b516..420a42ee0 100644 --- a/packages/phrases/src/locales/en.ts +++ b/packages/phrases/src/locales/en.ts @@ -12,6 +12,18 @@ const translation = { loading: 'Creating Account...', have_account: 'Already have an account?', }, + admin_console: { + tab_sections: { + overview: 'Overview', + resource_management: 'Resource Management', + }, + tabs: { + get_started: 'Get Started', + dashboard: 'Dashboard', + applications: 'Applications', + api_resources: 'API Resources', + }, + }, }; const errors = { diff --git a/packages/phrases/src/locales/zh-cn.ts b/packages/phrases/src/locales/zh-cn.ts index 4dc2287e8..58d933d86 100644 --- a/packages/phrases/src/locales/zh-cn.ts +++ b/packages/phrases/src/locales/zh-cn.ts @@ -14,6 +14,18 @@ const translation = { loading: '创建中...', have_account: '已经有账户?', }, + admin_console: { + tab_sections: { + overview: '概览', + resource_management: '资源管理', + }, + tabs: { + get_started: '开始使用', + dashboard: '仪表盘', + applications: '应用', + api_resources: 'API 资源', + }, + }, }; const errors = { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7826357ad..3ed2f3ae5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -31,6 +31,8 @@ importers: '@types/react': ^17.0.14 '@types/react-dom': ^17.0.9 eslint: ^8.10.0 + i18next: ^21.6.12 + i18next-browser-languagedetector: ^6.1.3 lint-staged: ^11.1.1 parcel: ^2.3.1 postcss: ^8.4.6 @@ -38,13 +40,17 @@ importers: prettier: ^2.3.2 react: ^17.0.2 react-dom: ^17.0.2 + react-i18next: ^11.15.4 stylelint: ^13.13.1 typescript: ^4.5.5 dependencies: '@logto/phrases': link:../phrases '@logto/schemas': link:../schemas + i18next: 21.6.12 + i18next-browser-languagedetector: 6.1.3 react: 17.0.2 react-dom: 17.0.2_react@17.0.2 + react-i18next: 11.15.4_2c37a602a29bb6bd53f3de707a8cfcc5 devDependencies: '@parcel/core': 2.3.1 '@parcel/transformer-sass': 2.3.1_@parcel+core@2.3.1 @@ -6904,6 +6910,12 @@ packages: '@babel/runtime': 7.16.3 dev: false + /i18next/21.6.12: + resolution: {integrity: sha512-xlGTPdu2g5PZEUIE6TA1mQ9EIAAv9nMFONzgwAIrKL/KTmYYWufQNGgOmp5Og1PvgUji+6i1whz0rMdsz1qaKw==} + dependencies: + '@babel/runtime': 7.16.3 + dev: false + /iconv-lite/0.4.24: resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} engines: {node: '>=0.10.0'} @@ -11209,6 +11221,27 @@ packages: scheduler: 0.20.2 dev: false + /react-i18next/11.15.4_2c37a602a29bb6bd53f3de707a8cfcc5: + resolution: {integrity: sha512-jKJNAcVcbPGK+yrTcXhLblgPY16n6NbpZZL3Mk8nswj1v3ayIiUBVDU09SgqnT+DluyQBS97hwSvPU5yVFG0yg==} + peerDependencies: + i18next: '>= 19.0.0' + react: '>= 16.8.0' + react-dom: '*' + react-native: '*' + peerDependenciesMeta: + react-dom: + optional: true + react-native: + optional: true + dependencies: + '@babel/runtime': 7.16.3 + html-escaper: 2.0.2 + html-parse-stringify: 3.0.1 + i18next: 21.6.12 + react: 17.0.2 + react-dom: 17.0.2_react@17.0.2 + dev: false + /react-i18next/11.15.4_3fb644aa30122a07f960d67fa51d6dc1: resolution: {integrity: sha512-jKJNAcVcbPGK+yrTcXhLblgPY16n6NbpZZL3Mk8nswj1v3ayIiUBVDU09SgqnT+DluyQBS97hwSvPU5yVFG0yg==} peerDependencies: