Skip to content

Commit

Permalink
feat(web): react-typed-i18n页面国际化 (#833)
Browse files Browse the repository at this point in the history
### 实现方案:

使用react-typed-i18n实现国际化
https://github.com/ddadaal/react-typed-i18n
利用Provider外包UI组件

> 语言切换时保存语言类别languageId到cookies,

> 在_app.tsx的getInitialProps中获取languageId传给MyApp定义Provider的初始语言信息id

_app.tsx
```
function MyApp({ Component, pageProps, extra }: Props) {  

...    
return(
  <>
  <Head />
  <Provider initialLanguage={{
        id: extra.languageId,
        definitions: extra.languageId === "en" ? en : zh_cn,
      }}
      >
         <Component  />
   </Provider>
)
```

> 对于蚂蚁组件的国际化与之前无差别,只需将languageId作为蚂蚁国际化locale同样从getInitialProps传入MyApp`

`<AntdConfigProvider color={primaryColor} locale="zh_cn">`

> 在Component内部利用createI18n所提供的useI18n的翻译函数或者Localized组件进行翻译,支持强类型化

/component.ts
```
const i18n = useI18n();
...
return (
  <Section
      title={useI18n.translateToString("dashboard.job.title")}
      extra={(
        <Link href="/user/runningJobs">
          <Localized id="dashboard.job.extra"></Localized>
        </Link>
      )}
)
```
因为采用Provider方案,包括路由导航栏在内的所有UI国际化渲染均类似,实现简单

优点:
1.实现简单,结构清晰
2.翻译文本资源key的prefix单独定义,在同一个翻译函数下简单使用不同的prefix
3.Localized使页面组件不需要再次引入翻译函数就可以直接进行翻译

缺点:
一次引入所有翻译资源可能使加载速度变慢
但是目前简单测试一两个页面没有感觉到什么影响
甚至因为没有全局对象异步等待等问题语言切换时显示速度很快

### 翻译文本资源整理
**配置文件参考下列表单(demo示例和登录页面配置)**
https://jgf29kqp7z.feishu.cn/wiki/BFFUwHLgaiYxK5k9QGwci4X0nMe
其他页面展示文本参考下表各系统zh_cn.ts和en.ts工作表
https://jgf29kqp7z.feishu.cn/wiki/RFXDw06rCiodzMkbOYqcEAclnkg



配置文件中可国际化配置由原来的字符串类型变更为字符串或i18n对象
eg:
xxxx.config
    # 允许兼容原来的字符串类型
    # title: "开源算力中心门户和管理平台"
    # 当想实现国际化功能时,不再配置原来的字符串类型,配置成下列展示的国际化类型
    # 包含默认值(字符串类型),英文文本(字符串类型),中文文本(字符串类型)
    title:
      i18n:
        default: "开源算力中心门户和管理平台"
        en: "Open-source Compute Center Portal and Management Platform"
        zh_cn: "开源算力中心门户和管理平台"



**修改移动web后端中文文字到前端并实现国际化**
主要包括以下错误信息的文字提示:

管理系统
1.修改作业时限时,时长错误信息  "设置作业时限需要大于该作业的运行时长。"
2.个人信息: 修改密码时错误信息: 来源于common配置文件中的配置的密码错误的错误信息
3.各创建租户和创建用户页面
  用户Id错误信息: 来源于mis.yaml配置的创建用户id错误信息
  密码错误信息: 来源于common配置文件中的配置的密码错误的错误信息
4.在账户中添加用户的原`”用户已经存在于此账户中!“`因为是grpc返回错误,直接使用后端错误信息元数据的英文展示,不做国际化

门户系统
1.个人信息中修改密码的错误信息: 来源于common配置文件中的配置的密码错误的错误信息

---------

Co-authored-by: OYX-1 <13121812323@163.com>
Co-authored-by: Chen Junda <ddadaal@outlook.com>
Co-authored-by: ZihanChen821 <827625357@qq.com>
Co-authored-by: Miracle575 <longsijie@icode.pku.edu.cn>
Co-authored-by: 何童崇 <792998983@qq.com>
  • Loading branch information
6 people authored Sep 25, 2023
1 parent 66ecc77 commit ccbde14
Show file tree
Hide file tree
Showing 254 changed files with 7,506 additions and 1,863 deletions.
5 changes: 5 additions & 0 deletions .changeset/hungry-candles-fold.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@scow/config": patch
---

使配置文件中文本配置项兼容国际化类型,实现自定义配置文本的国际化展示
12 changes: 12 additions & 0 deletions .changeset/nice-kings-begin.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
"@scow/portal-server": patch
"@scow/portal-web": minor
"@scow/mis-web": minor
"@scow/lib-server": patch
"@scow/auth": minor
"@scow/lib-web": minor
"@scow/docs": patch
"@scow/test-adapter": patch
---

实现 SCOW 门户系统与管理系统的页面国际化功能
5 changes: 5 additions & 0 deletions .changeset/proud-garlics-repair.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@scow/grpc-api": patch
---

修改交互式应用的 html 配置表单的 lable 与 placeholder 的 grpc 类型为 i18nStringType
4 changes: 3 additions & 1 deletion apps/auth/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"liquidjs": "10.8.4",
"@scow/config": "workspace:*",
"@scow/lib-config": "workspace:*",
"@scow/lib-server": "workspace:*",
"@scow/lib-ssh": "workspace:*",
"@scow/utils": "workspace:*",
"@sinclair/typebox": "0.31.1",
Expand All @@ -43,7 +44,8 @@
"pino": "8.15.0",
"nodemailer": "6.9.4",
"qrcode": "1.5.3",
"speakeasy": "2.0.0"
"speakeasy": "2.0.0",
"react-typed-i18n": "2.3.0"
},
"devDependencies": {
"@types/asn1": "0.2.1",
Expand Down
7 changes: 7 additions & 0 deletions apps/auth/src/auth/bindOtpHtml.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,12 @@
*/

import { DEFAULT_PRIMARY_COLOR } from "@scow/config/build/ui";
import { getLanguageCookie } from "@scow/lib-server";
import { FastifyReply, FastifyRequest } from "fastify";
import { join } from "path";
import { config, FAVICON_URL } from "src/config/env";
import { uiConfig } from "src/config/ui";
import { AuthTextsType, languages } from "src/i18n";


function parseHostname(req: FastifyRequest): string | undefined {
Expand Down Expand Up @@ -48,7 +50,12 @@ export async function renderBindOtpHtml(

const hostname = parseHostname(req);

// 获取当前语言ID及对应的绑定OTP页面文本
const languageId = getLanguageCookie(req.raw);
const authTexts: AuthTextsType = languages[languageId];

return rep.status(err ? 401 : 200).view("/otp/bindOtp.liquid", {
authTexts: authTexts,
cssUrl: join(config.BASE_PATH, config.AUTH_BASE_PATH, "/public/assets/tailwind.min.css"),
faviconUrl: join(config.BASE_PATH, FAVICON_URL),
backgroundColor: uiConfig.primaryColor?.defaultColor ?? DEFAULT_PRIMARY_COLOR,
Expand Down
17 changes: 15 additions & 2 deletions apps/auth/src/auth/loginHtml.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,14 @@
*/

import { DEFAULT_PRIMARY_COLOR } from "@scow/config/build/ui";
import { getI18nConfigCurrentText, getLanguageCookie } from "@scow/lib-server/build/i18n";
import { FastifyReply, FastifyRequest } from "fastify";
import { join } from "path";
import { createCaptcha } from "src/auth/captcha";
import { authConfig, OtpStatusOptions, ScowLogoType } from "src/config/auth";
import { config, FAVICON_URL, LOGO_URL } from "src/config/env";
import { uiConfig } from "src/config/ui";
import { AuthTextsType, languages } from "src/i18n";


export async function serveLoginHtml(
Expand All @@ -36,8 +38,19 @@ export async function serveLoginHtml(
? await createCaptcha(req.server)
: undefined;

// 获取当前语言ID及对应的登录页面文本
const languageId = getLanguageCookie(req.raw);
const authTexts: AuthTextsType = languages[languageId];

// 获取sloganI18nText
const sloganTitle = getI18nConfigCurrentText(authConfig.ui?.slogan.title, languageId);
const sloganTextArr = authConfig.ui?.slogan.texts.map((text) => {
return getI18nConfigCurrentText(text, languageId);
});

return rep.status(
verifyCaptchaFail ? 400 : err ? 401 : 200).view("login.liquid", {
authTexts: authTexts,
cssUrl: join(config.BASE_PATH, config.AUTH_BASE_PATH, "/public/assets/tailwind.min.css"),
eyeImagePath: join(config.BASE_PATH, config.AUTH_BASE_PATH, "/public/assets/icons/eye.png"),
eyeCloseImagePath: join(config.BASE_PATH, config.AUTH_BASE_PATH, "/public/assets/icons/eye-close.png"),
Expand All @@ -51,8 +64,8 @@ export async function serveLoginHtml(
logoLink: authConfig.ui?.logo.customLogoLink ?? "",
callbackUrl,
sloganColor: authConfig.ui?.slogan.color || "white",
sloganTitle: authConfig.ui?.slogan.title || "",
sloganTextArr: authConfig.ui?.slogan.texts || [],
sloganTitle: sloganTitle || "",
sloganTextArr: sloganTextArr || [],
footerTextColor: authConfig.ui?.footerTextColor || "white",
themeColor: uiConfig.primaryColor?.defaultColor ?? DEFAULT_PRIMARY_COLOR,
err,
Expand Down
18 changes: 16 additions & 2 deletions apps/auth/src/config/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,20 @@ export enum ScowLogoType {
"light" = "light",
}

// 创建配置文件中显示文字项的配置类型
export const createI18nStringSchema = (description: string, defaultValue?: string) => {
return Type.Union([
Type.String(),
Type.Object({
i18n: Type.Object({
default: Type.String({ description: "国际化类型默认值" }),
en: Type.Optional(Type.String({ description: "国际化类型英文值" })),
zh_cn: Type.Optional(Type.String({ description: "国际化类型简体中文值" })),
}),
}),
], { description, default: defaultValue });
};

export const LdapConfigSchema = Type.Object({
url: Type.String({ description: "LDAP地址" }),
searchBase: Type.String({ description: "从哪个节点搜索登录用户对应的LDAP节点" }),
Expand Down Expand Up @@ -166,8 +180,8 @@ export const UiConfigSchema = Type.Object({
}, { default: {} }),
slogan: Type.Object({
color: Type.String({ description: "默认标语文字颜色", default: "white" }),
title: Type.String({ description: "默认标语标题", default: "" }),
texts: Type.Array(Type.String(), { description: "默认 slogan 正文数组", default: []}),
title: createI18nStringSchema("默认标语标题", ""),
texts: Type.Array(createI18nStringSchema(""), { description: "默认 slogan 正文数组", default: []}),
}, { default: {} }),
footerTextColor: Type.String({ description: "默认 footer 文字颜色", default: "white" }),
});
Expand Down
46 changes: 46 additions & 0 deletions apps/auth/src/i18n/en.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/**
* Copyright (c) 2022 Peking University and Peking University Institute for Computing and Digital Economy
* SCOW is licensed under Mulan PSL v2.
* You can use this software according to the terms and conditions of the Mulan PSL v2.
* You may obtain a copy of Mulan PSL v2 at:
* http://license.coscl.org.cn/MulanPSL2
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
* See the Mulan PSL v2 for more details.
*/

export default {
login: {
login: "Log In",
accountPasswordLogin: "Account Password Login",
username: "Username",
password: "Password",
otpVCode: "OTP Verification Code",
inputVCode: "Please enter the verification code",
refreshError: "Refresh failed, please click to retry.",
invalidVCode: "Invalid verification code, please re-enter.",
invalidInput: "Invalid username / password, please check.",
invalidOtp: "Invalid OTP Verification Code, please re-enter.",
bindOtp: "Bind OTP",
},
bindOtp: {
bindOtp: "Bind OTP",
returnLogin: "Return to Login",
userName: "Username",
password: "Password",
invalidUserNamePassword: "Invalid username/password. Please check.",
confirm: "Confirm",
expiredUserInfo: "User information has expired, please bind again!",
bindLimit1: "Please complete binding within ",
bindLimit2: " minutes",
email: "Your Email ",
getBindLink: "Get Binding Link",
bindLinkSended: "Binding link has been sent. Please verify within your email.",
bindLinkFailed1: "Failed to send the binding link. Please try again in ",
bindLinkFailed2: " seconds.",
bindRequestError1: "Do not request binding link frequently. Please try again in ",
bindRequestError2: " seconds.",
reRequestLink: "You can now request the link again.",
},
};
25 changes: 25 additions & 0 deletions apps/auth/src/i18n/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/**
* Copyright (c) 2022 Peking University and Peking University Institute for Computing and Digital Economy
* SCOW is licensed under Mulan PSL v2.
* You can use this software according to the terms and conditions of the Mulan PSL v2.
* You may obtain a copy of Mulan PSL v2 at:
* http://license.coscl.org.cn/MulanPSL2
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
* See the Mulan PSL v2 for more details.
*/

import { languageDictionary } from "react-typed-i18n";


const zh_cn = () => import("./zh_cn").then((x) => x.default);
const en = () => import("./en").then((x) => x.default);

// return language type
export type AuthTextsType = Awaited<ReturnType<typeof zh_cn>>;

export const languages = languageDictionary({
zh_cn,
en,
});
46 changes: 46 additions & 0 deletions apps/auth/src/i18n/zh_cn.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/**
* Copyright (c) 2022 Peking University and Peking University Institute for Computing and Digital Economy
* SCOW is licensed under Mulan PSL v2.
* You can use this software according to the terms and conditions of the Mulan PSL v2.
* You may obtain a copy of Mulan PSL v2 at:
* http://license.coscl.org.cn/MulanPSL2
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
* See the Mulan PSL v2 for more details.
*/

export default {
login: {
login: "登录",
accountPasswordLogin: "账号密码登录",
username: "用户名",
password: "密码",
otpVCode: "OTP验证码",
inputVCode: "请输入验证码",
refreshError: "刷新失败,请点击重试",
invalidVCode: "验证码无效,请重新输入。",
invalidInput: "用户名/密码无效,请检查。",
invalidOtp: "OTP验证码无效,请重新输入。",
bindOtp: "绑定otp",
},
bindOtp: {
bindOtp: "绑定OTP",
returnLogin: "返回登录",
userName: "用户名",
password: "密码",
invalidUserNamePassword: "用户名/密码无效,请检查。",
confirm: "确认",
expiredUserInfo: "用户信息过期,请重新绑定!",
bindLimit1: "请于",
bindLimit2: "分钟内完成绑定",
email: "您的邮箱",
getBindLink: "获取绑定链接",
bindLinkSended: "绑定链接已发送,请在邮箱内进行验证",
bindLinkFailed1: "绑定链接发送失败,请在",
bindLinkFailed2: "秒后重新获取",
bindRequestError1: "请勿频繁获取绑定链接,请在",
bindRequestError2: "秒后重新获取",
reRequestLink: "现在您可以重新获取链接",
},
};
26 changes: 13 additions & 13 deletions apps/auth/views/login.liquid
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<html>

<head>
<title>登录</title>
<title>{{ authTexts.login.login }}</title>
<link href="{{ cssUrl }}" rel="stylesheet">
<link rel="icon" type="image/x-icon" href="{{ faviconUrl }}"></link>
<meta name="viewport" content="width=device-width, initial-scale=1">
Expand Down Expand Up @@ -42,31 +42,31 @@
<div class="w-1/2 h-screen ml-20 flex items-center justify-center">
<div class="w-80 max-w-md min-w-max bg-white rounded-lg py-12">
<form method="post" action="">
<div class="mb-16 text-2xl font-semibold text-center">账号密码登录</div>
<div class="mb-16 text-2xl font-semibold text-center">{{ authTexts.login.accountPasswordLogin }}</div>
<div class="px-14 flex flex-col items-center">
<div class="w-72 mb-10">
<input type='text' name="username" placeholder="用户名" required
<input type='text' name="username" placeholder="{{ authTexts.login.username }}" required
class="px-8 w-full border rounded px-3 py-2 text-gray-700 focus:outline-none" />
</div>
<div class="w-72 mb-10">
<div class="relative flex items-center">
<input id="password" name="password" placeholder="密码" type="password" required
<input id="password" name="password" placeholder="{{ authTexts.login.password }}" type="password" required
class="px-8 w-full border rounded px-3 py-2 text-gray-700 focus:outline-none" />
<div id="eye-elem" class="absolute w-5 h-5 right-3 bg-contain"></div>
</div>
</div>
{% if enableTotp %}
<div class="w-full mb-10">
<div class="flex items-center">
<input name="otpCode" placeholder="OTP验证码" type="text" required
<input name="otpCode" placeholder="{{ authTexts.login.otpVCode }}" type="text" required
class="px-8 w-full py-2 border rounded text-gray-700 focus:outline-none"/>
</div>
</div>
{% endif %}
{% if enableCaptcha %}
<div class="w-full mb-10">
<div class="flex items-center">
<input name="code" placeholder="请输入验证码" type="text" required
<input name="code" placeholder="{{ authTexts.login.inputVCode }}" type="text" required
class=" px-8 w-full border rounded px-3 py-2 text-gray-700 focus:outline-none" />
<div id="captcha" onclick="refreshCaptcha()" class="cursor-pointer">{{ code }}</div>
<script>
Expand All @@ -81,15 +81,15 @@
).then( async function (response) {
captchaDiv.innerHTML = await response.text();
}).catch(() => {
captchaDiv.textContent = "刷新失败,请点击重试"
captchaDiv.textContent = "{{ authTexts.login.refreshError }}"
});
}
</script>
</script>
</div>
</div>

{% if verifyCaptchaFail %}
<p class="my-4 text-center text-red-600">验证码无效,请重新输入。</p>
<p class="my-4 text-center text-red-600">{{ authTexts.login.invalidVCode }}</p>
{% endif %}

{% else %}
Expand All @@ -100,21 +100,21 @@
<input type="hidden" name="callbackUrl" value="{{ callbackUrl }}" />

{% if err %}
<p class="my-4 text-center text-red-600">用户名/密码无效,请检查。</p>
<p class="my-4 text-center text-red-600">{{ authTexts.login.invalidInput }}</p>
{% endif %}
{% if verifyOtpFail %}
<p class="my-4 text-center text-red-600">OTP验证码无效,请重新输入。</p>
<p class="my-4 text-center text-red-600">{{ authTexts.login.invalidOtp }}</p>
{% endif %}
<button type="submit" class="w-72 py-2 mb-14 rounded button-primary text-gray-100 focus:outline-none">
登录
{{ authTexts.login.login }}
</button>
</div>
</form>
{% if showBindOtpButton %}
<div class="px-12 mt-4">
<form action="{{ otpBasePath }}/bind" method="get">
<button type="submit" name="action" value="bindOtp" class="px text-gray-400">
绑定otp
{{ authTexts.login.bindOtp }}
</button>
<input type="hidden" name="callbackUrl" value="{{ callbackUrl }}" />
</form>
Expand Down
Loading

0 comments on commit ccbde14

Please sign in to comment.