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: