From 00e00d65296c1c4a9a47088d2157b0a16809b821 Mon Sep 17 00:00:00 2001 From: monster Date: Wed, 24 Jan 2024 21:30:00 +0800 Subject: [PATCH 1/4] Add language localization support --- src/containers/Login.tsx | 54 ++++++++++++++++++++++++++++++++------ src/locales/en-US.json | 10 +++++++ src/locales/zh-CN.json | 10 +++++++ src/utils/Language.ts | 48 +++++++++++++++++++++++++++++++++ src/utils/StorageHelper.ts | 11 ++++++++ 5 files changed, 125 insertions(+), 8 deletions(-) create mode 100644 src/locales/en-US.json create mode 100644 src/locales/zh-CN.json create mode 100644 src/utils/Language.ts diff --git a/src/containers/Login.tsx b/src/containers/Login.tsx index a41b70b1..48ec82b2 100644 --- a/src/containers/Login.tsx +++ b/src/containers/Login.tsx @@ -1,9 +1,14 @@ import { LockOutlined } from '@ant-design/icons' -import { Button, Card, Collapse, Input, Radio, Row } from 'antd' +import { Button, Card, Collapse, Input, Radio, Row, Select } from 'antd' import React, { ReactComponentElement } from 'react' import { Redirect, RouteComponentProps } from 'react-router' import ApiManager from '../api/ApiManager' import ErrorFactory from '../utils/ErrorFactory' +import { + currentLanguageOption, + languagesOptions, + localize, +} from '../utils/Language' import StorageHelper from '../utils/StorageHelper' import Toaster from '../utils/Toaster' import Utils from '../utils/Utils' @@ -76,7 +81,25 @@ export default class Login extends ApiComponent, any> { transform: 'translate(-50%,-50%)', }} > - + { + StorageHelper.setLanguageInLocalStorage( + value + ) + window.location.reload() + }} + /> + } + > { self.setState({ passwordEntered: `${e.target.value}` }) }} - placeholder="Password" + placeholder={localize('login_form.password', 'Password')} autoFocus /> {self.props.hasOtp ? ( @@ -193,12 +216,18 @@ class NormalLoginForm extends React.Component< htmlType="submit" className="login-form-button" > - Login + {localize('login_form.login', 'Login')} - + { console.log(e.target.value) @@ -209,13 +238,22 @@ class NormalLoginForm extends React.Component< value={self.state.loginOption} > - No session persistence (Most Secure) + {localize( + 'login_form.no_session_persistence', + 'No session persistence (Most Secure)' + )} - Use sessionStorage + {localize( + 'login_form.use_session_storage', + 'Use sessionStorage' + )} - Use localStorage (Most Persistent) + {localize( + 'login_form.use_local_storage', + 'Use localStorage (Most Persistent)' + )} diff --git a/src/locales/en-US.json b/src/locales/en-US.json new file mode 100644 index 00000000..0ac992d2 --- /dev/null +++ b/src/locales/en-US.json @@ -0,0 +1,10 @@ +{ + "login_form.cap_rover": "CapRover Login", + "login_form.login": "Login", + "login_form.no_session_persistence": "No session persistence (Most Secure)", + "login_form.password": "Password", + "login_form.placeholder": "Password", + "login_form.remember_me": "Remember Me", + "login_form.use_local_storage": "Use localStorage (Most Persistent)", + "login_form.use_session_storage": "Use sessionStorage" +} diff --git a/src/locales/zh-CN.json b/src/locales/zh-CN.json new file mode 100644 index 00000000..cd3ad0f8 --- /dev/null +++ b/src/locales/zh-CN.json @@ -0,0 +1,10 @@ +{ + "login_form.cap_rover": "CapRover 登录", + "login_form.login": "登录", + "login_form.no_session_persistence": "不储存会话(最安全)", + "login_form.password": "密码", + "login_form.placeholder": "密码", + "login_form.remember_me": "记住我", + "login_form.use_local_storage": "使用 LocalStorage 存储(最持久)", + "login_form.use_session_storage": "使用 SessionStorage 存储" +} diff --git a/src/utils/Language.ts b/src/utils/Language.ts new file mode 100644 index 00000000..33daae42 --- /dev/null +++ b/src/utils/Language.ts @@ -0,0 +1,48 @@ +import { Locale } from 'antd/es/locale-provider' +import enUS from 'antd/es/locale/en_US' +import zhCN from 'antd/es/locale/zh_CN' +import enUSMessages from '../locales/en-US.json' +import zhCNMessages from '../locales/zh-CN.json' + +import StorageHelper from './StorageHelper' + +export interface LanguageOption { + label: string + value: string + alias?: string[] + antdLocale: Locale + messages: Record +} + +const languagesOptions: LanguageOption[] = [ + // en-US should be the first option + { + label: 'English', + value: 'en-US', + alias: ['en'], + antdLocale: enUS, + messages: enUSMessages, + }, + { + label: '简体中文', + value: 'zh-CN', + alias: ['zh'], + antdLocale: zhCN, + messages: zhCNMessages, + }, +] + +const defaultLanguageOptions = languagesOptions[0] + +const currentLanguage = StorageHelper.getLanguageFromLocalStorage() + +const currentLanguageOption: LanguageOption = + languagesOptions.find((o) => { + return o.value === currentLanguage || o.alias?.includes(currentLanguage) + }) || defaultLanguageOptions + +export function localize(key: string, message: string) { + return currentLanguageOption.messages[key] || message +} + +export { currentLanguageOption, languagesOptions } diff --git a/src/utils/StorageHelper.ts b/src/utils/StorageHelper.ts index 17d5c7da..cea4b0b0 100644 --- a/src/utils/StorageHelper.ts +++ b/src/utils/StorageHelper.ts @@ -12,6 +12,8 @@ const sessionStorage = window.sessionStorage const AUTH_KEY = 'CAPROVER_AUTH_KEY' const SIDER_COLLAPSED_STATE = 'CAPROVER_SIDER_COLLAPSED_STATE' const DARK_MODE = 'CAPROVER_DARK_MODE' +const LANGUAGE = 'CAPROVER_LANGUAGE' + class StorageHelper { getAuthKeyFromStorage() { const localStorageAuth = localStorage.getItem(AUTH_KEY) @@ -59,6 +61,15 @@ class StorageHelper { : window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches } + + setLanguageInLocalStorage(language: string) { + localStorage.setItem(LANGUAGE, language) + } + + getLanguageFromLocalStorage(): string { + const language = localStorage.getItem(LANGUAGE) + return language ? language : navigator.language + } } const instance = new StorageHelper() From 6af210d276bf09f87cd795d2bed6bae9e8fa90cd Mon Sep 17 00:00:00 2001 From: monster Date: Wed, 24 Jan 2024 21:32:19 +0800 Subject: [PATCH 2/4] Add ConfigProvider for localization support --- src/App.tsx | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index e8766bd5..8d677530 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,3 +1,4 @@ +import { ConfigProvider } from 'antd' import { Component } from 'react' import { ThemeSwitcherProvider, @@ -11,6 +12,7 @@ import Login from './containers/Login' import PageRoot from './containers/PageRoot' import reducers from './redux/reducers' import CrashReporter from './utils/CrashReporter' +import { currentLanguageOption } from './utils/Language' import StorageHelper from './utils/StorageHelper' CrashReporter.getInstance().init() @@ -61,9 +63,11 @@ class App extends Component<{}, AppState> { defaultTheme={this.state.isDarkMode ? 'dark' : 'light'} insertionPoint="styles-insertion-point" > - - - + + + + + ) } From 0b97d01559708593a3e1c2d79c7b4df410e09df6 Mon Sep 17 00:00:00 2001 From: monster Date: Wed, 24 Jan 2024 21:44:56 +0800 Subject: [PATCH 3/4] feat: add format script --- scripts/generate.mjs | 113 +++++++++++++++++++++++++++++++++++++++++ src/locales/en-US.json | 1 - src/locales/zh-CN.json | 1 - 3 files changed, 113 insertions(+), 2 deletions(-) create mode 100644 scripts/generate.mjs diff --git a/scripts/generate.mjs b/scripts/generate.mjs new file mode 100644 index 00000000..d1f02aed --- /dev/null +++ b/scripts/generate.mjs @@ -0,0 +1,113 @@ +import parser from '@babel/parser' +import traverse from '@babel/traverse' +import * as types from '@babel/types' +import fs from 'fs/promises' +import globby from 'globby' +import path, { resolve } from 'path' +import prettier from 'prettier' + +const __dirname = path.dirname(new URL(import.meta.url).pathname) +const ProjectSourceRoot = path.resolve(__dirname, '..') + +async function formatJSON(jsonContent) { + const prettierConfig = JSON.parse( + await fs.readFile(path.resolve(ProjectSourceRoot, '.prettierrc.json')) + ) + return prettier.format(JSON.stringify(jsonContent), { + ...prettierConfig, + parser: 'json', + }) +} + +async function getMessages(filePath) { + const content = await fs.readFile(filePath, 'utf8') + const plugins = ['typescript', 'decorators-legacy'] + if (filePath.endsWith('tsx')) { + plugins.push('jsx') + } + const result = [] + + try { + const ast = parser.parse(content, { + plugins: plugins, + sourceType: 'unambiguous', + }) + traverse.default(ast, { + enter(path) { + const node = path.node + if (types.isCallExpression(node)) { + const callee = node.callee + if ( + types.isIdentifier(callee) && + callee.name === 'localize' + ) { + const args = node.arguments.map((o) => { + if (types.isStringLiteral(o)) { + return o.value + } + }) + const [key, message] = args + result.push([key, message]) + } + } + }, + }) + } catch (error) { + console.log(filePath) + } + + return result +} + +async function generate() { + const sourceCode = await globby(['**/**.ts', '**/**.tsx'], { + absolute: true, + cwd: resolve(ProjectSourceRoot, 'src'), + }) + const localsPath = resolve(ProjectSourceRoot, 'src/locales') + const files = (await fs.readdir(localsPath)).filter((p) => + p.endsWith('.json') + ) + const enUSMessages = {} + for (const iterator of sourceCode) { + const messages = await getMessages(iterator) + messages.forEach(([key, message]) => { + if (enUSMessages[key]) { + if (enUSMessages[key] !== message) { + throw new Error(key) + } + } else { + enUSMessages[key] = message + } + }) + } + + const sortedKeys = Object.keys(enUSMessages).sort((a, b) => + a.localeCompare(b) + ) + + files + .filter((file) => path.extname(file) === '.json') + .filter((file) => file !== 'en-US.json') + .map((file) => path.resolve(localsPath, file)) + .forEach(async (file) => { + const messages = JSON.parse(await fs.readFile(file, 'utf-8')) + const result = {} + sortedKeys.forEach((key) => { + result[key] = messages[key] || '' + }) + await fs.writeFile(file, await formatJSON(result)) + }) + + const sortedData = {} + sortedKeys.forEach((key) => { + sortedData[key] = enUSMessages[key] + }) + + await fs.writeFile( + path.resolve(localsPath, 'en-US.json'), + await formatJSON(sortedData) + ) +} + +generate() diff --git a/src/locales/en-US.json b/src/locales/en-US.json index 0ac992d2..d3d51d87 100644 --- a/src/locales/en-US.json +++ b/src/locales/en-US.json @@ -3,7 +3,6 @@ "login_form.login": "Login", "login_form.no_session_persistence": "No session persistence (Most Secure)", "login_form.password": "Password", - "login_form.placeholder": "Password", "login_form.remember_me": "Remember Me", "login_form.use_local_storage": "Use localStorage (Most Persistent)", "login_form.use_session_storage": "Use sessionStorage" diff --git a/src/locales/zh-CN.json b/src/locales/zh-CN.json index cd3ad0f8..b1e0f7d0 100644 --- a/src/locales/zh-CN.json +++ b/src/locales/zh-CN.json @@ -3,7 +3,6 @@ "login_form.login": "登录", "login_form.no_session_persistence": "不储存会话(最安全)", "login_form.password": "密码", - "login_form.placeholder": "密码", "login_form.remember_me": "记住我", "login_form.use_local_storage": "使用 LocalStorage 存储(最持久)", "login_form.use_session_storage": "使用 SessionStorage 存储" From 60e1e13ac0c02ec7a08fd9a470446f29c2930cda Mon Sep 17 00:00:00 2001 From: monster Date: Wed, 24 Jan 2024 21:47:04 +0800 Subject: [PATCH 4/4] fix: generate script --- package.json | 3 ++- scripts/{generate.mjs => generate-locales.mjs} | 0 2 files changed, 2 insertions(+), 1 deletion(-) rename scripts/{generate.mjs => generate-locales.mjs} (100%) diff --git a/package.json b/package.json index d906fb04..9b0059ed 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,8 @@ "test": "craco test", "test:e2e--open": "cypress open", "test:e2e": "cypress run", - "eject": "react-scripts eject" + "eject": "react-scripts eject", + "generate-locales": "node scripts/generate-locales.mjs" }, "eslintConfig": { "extends": [ diff --git a/scripts/generate.mjs b/scripts/generate-locales.mjs similarity index 100% rename from scripts/generate.mjs rename to scripts/generate-locales.mjs