Skip to content

Commit 7c2aa8c

Browse files
zhaozhimingjsjoeio
andauthored
feat: add i18n in login page (#5947)
* feat: add i18n in login page * fix: add word space and put the app name into the title * fix: remove duplicate replace title * fix: prettier format code * fix: fix typescript check warning * fix: add zh-cn locale file code owner * fix: use existing flag locale to the login page Co-authored-by: Joe Previte <[email protected]>
1 parent d40a974 commit 7c2aa8c

File tree

11 files changed

+108
-14
lines changed

11 files changed

+108
-14
lines changed

.github/CODEOWNERS

+2
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,5 @@
33
ci/helm-chart/ @Matthew-Beckett @alexgorbatchev
44

55
docs/install.md @GNUxeava
6+
7+
src/node/i18n/locales/zh-cn.json @zhaozhiming

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@
9595
"express": "5.0.0-alpha.8",
9696
"http-proxy": "^1.18.0",
9797
"httpolyglot": "^0.1.2",
98+
"i18next": "^22.4.6",
9899
"js-yaml": "^4.0.0",
99100
"limiter": "^1.1.5",
100101
"pem": "^1.14.2",

src/browser/pages/login.html

+4-4
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
http-equiv="Content-Security-Policy"
1111
content="style-src 'self'; script-src 'self' 'unsafe-inline'; manifest-src 'self'; img-src 'self' data:; font-src 'self' data:;"
1212
/>
13-
<title>{{APP_NAME}} login</title>
13+
<title>{{I18N_LOGIN_TITLE}}</title>
1414
<link rel="icon" href="{{CS_STATIC_BASE}}/src/browser/media/favicon-dark-support.svg" />
1515
<link rel="alternate icon" href="{{CS_STATIC_BASE}}/src/browser/media/favicon.ico" />
1616
<link rel="manifest" href="{{BASE}}/manifest.json" crossorigin="use-credentials" />
@@ -25,7 +25,7 @@
2525
<div class="card-box">
2626
<div class="header">
2727
<h1 class="main">{{WELCOME_TEXT}}</h1>
28-
<div class="sub">Please log in below. {{PASSWORD_MSG}}</div>
28+
<div class="sub">{{I18N_LOGIN_BELOW}} {{PASSWORD_MSG}}</div>
2929
</div>
3030
<div class="content">
3131
<form class="login-form" method="post">
@@ -38,11 +38,11 @@ <h1 class="main">{{WELCOME_TEXT}}</h1>
3838
autofocus
3939
class="password"
4040
type="password"
41-
placeholder="PASSWORD"
41+
placeholder="{{I18N_PASSWORD_PLACEHOLDER}}"
4242
name="password"
4343
autocomplete="current-password"
4444
/>
45-
<input class="submit -button" value="SUBMIT" type="submit" />
45+
<input class="submit -button" value="{{I18N_SUBMIT}}" type="submit" />
4646
</div>
4747
{{ERROR}}
4848
</form>

src/node/cli.ts

+8-1
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,14 @@ export const options: Options<Required<UserProvidedArgs>> = {
180180
enable: { type: "string[]" },
181181
help: { type: "boolean", short: "h", description: "Show this output." },
182182
json: { type: "boolean" },
183-
locale: { type: "string" }, // The preferred way to set the locale is via the UI.
183+
locale: {
184+
// The preferred way to set the locale is via the UI.
185+
type: "string",
186+
description: `
187+
Set vscode display language and language to show on the login page, more info see
188+
https://en.wikipedia.org/wiki/IETF_language_tag
189+
`,
190+
},
184191
open: { type: "boolean", description: "Open in browser on startup. Does not work remotely." },
185192

186193
"bind-addr": {

src/node/i18n/index.ts

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import i18next, { init } from "i18next"
2+
import * as en from "./locales/en.json"
3+
import * as zhCn from "./locales/zh-cn.json"
4+
5+
init({
6+
lng: "en",
7+
fallbackLng: "en", // language to use if translations in user language are not available.
8+
returnNull: false,
9+
lowerCaseLng: true,
10+
debug: process.env.NODE_ENV === "development",
11+
resources: {
12+
en: {
13+
translation: en,
14+
},
15+
"zh-cn": {
16+
translation: zhCn,
17+
},
18+
},
19+
})
20+
21+
export default i18next

src/node/i18n/locales/en.json

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"LOGIN_TITLE": "{{app}} login",
3+
"LOGIN_BELOW": "Please log in below.",
4+
"WELCOME": "Welcome to {{app}}",
5+
"LOGIN_PASSWORD": "Check the config file at {{configFile}} for the password.",
6+
"LOGIN_USING_ENV_PASSWORD": "Password was set from $PASSWORD.",
7+
"LOGIN_USING_HASHED_PASSWORD": "Password was set from $HASHED_PASSWORD.",
8+
"SUBMIT": "SUBMIT",
9+
"PASSWORD_PLACEHOLDER": "PASSWORD",
10+
"LOGIN_RATE_LIMIT": "Login rate limited!",
11+
"MISS_PASSWORD": "Missing password",
12+
"INCORRECT_PASSWORD": "Incorrect password"
13+
}

src/node/i18n/locales/zh-cn.json

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"LOGIN_TITLE": "{{app}} 登录",
3+
"LOGIN_BELOW": "请在下面登录。",
4+
"WELCOME": "欢迎来到 {{app}}",
5+
"LOGIN_PASSWORD": "查看配置文件 {{configFile}} 中的密码。",
6+
"LOGIN_USING_ENV_PASSWORD": "密码在 $PASSWORD 中设置。",
7+
"LOGIN_USING_HASHED_PASSWORD": "密码在 $HASHED_PASSWORD 中设置。",
8+
"SUBMIT": "提交",
9+
"PASSWORD_PLACEHOLDER": "密码",
10+
"LOGIN_RATE_LIMIT": "登录速率限制!",
11+
"MISS_PASSWORD": "缺少密码",
12+
"INCORRECT_PASSWORD": "密码不正确"
13+
}

src/node/routes/login.ts

+14-8
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { CookieKeys } from "../../common/http"
77
import { rootPath } from "../constants"
88
import { authenticated, getCookieOptions, redirect, replaceTemplates } from "../http"
99
import { getPasswordMethod, handlePasswordValidation, humanPath, sanitizeString, escapeHtml } from "../util"
10+
import i18n from "../i18n"
1011

1112
// RateLimiter wraps around the limiter library for logins.
1213
// It allows 2 logins every minute plus 12 logins every hour.
@@ -28,21 +29,26 @@ export class RateLimiter {
2829

2930
const getRoot = async (req: Request, error?: Error): Promise<string> => {
3031
const content = await fs.readFile(path.join(rootPath, "src/browser/pages/login.html"), "utf8")
32+
const locale = req.args["locale"] || "en"
33+
i18n.changeLanguage(locale)
3134
const appName = req.args["app-name"] || "code-server"
32-
const welcomeText = req.args["welcome-text"] || `Welcome to ${appName}`
33-
let passwordMsg = `Check the config file at ${humanPath(os.homedir(), req.args.config)} for the password.`
35+
const welcomeText = req.args["welcome-text"] || (i18n.t("WELCOME", { app: appName }) as string)
36+
let passwordMsg = i18n.t("LOGIN_PASSWORD", { configFile: humanPath(os.homedir(), req.args.config) })
3437
if (req.args.usingEnvPassword) {
35-
passwordMsg = "Password was set from $PASSWORD."
38+
passwordMsg = i18n.t("LOGIN_USING_ENV_PASSWORD")
3639
} else if (req.args.usingEnvHashedPassword) {
37-
passwordMsg = "Password was set from $HASHED_PASSWORD."
40+
passwordMsg = i18n.t("LOGIN_USING_HASHED_PASSWORD")
3841
}
3942

4043
return replaceTemplates(
4144
req,
4245
content
43-
.replace(/{{APP_NAME}}/g, appName)
46+
.replace(/{{I18N_LOGIN_TITLE}}/g, i18n.t("LOGIN_TITLE", { app: appName }))
4447
.replace(/{{WELCOME_TEXT}}/g, welcomeText)
4548
.replace(/{{PASSWORD_MSG}}/g, passwordMsg)
49+
.replace(/{{I18N_LOGIN_BELOW}}/g, i18n.t("LOGIN_BELOW"))
50+
.replace(/{{I18N_PASSWORD_PLACEHOLDER}}/g, i18n.t("PASSWORD_PLACEHOLDER"))
51+
.replace(/{{I18N_SUBMIT}}/g, i18n.t("SUBMIT"))
4652
.replace(/{{ERROR}}/, error ? `<div class="error">${escapeHtml(error.message)}</div>` : ""),
4753
)
4854
}
@@ -70,11 +76,11 @@ router.post<{}, string, { password: string; base?: string }, { to?: string }>("/
7076
try {
7177
// Check to see if they exceeded their login attempts
7278
if (!limiter.canTry()) {
73-
throw new Error("Login rate limited!")
79+
throw new Error(i18n.t("LOGIN_RATE_LIMIT") as string)
7480
}
7581

7682
if (!password) {
77-
throw new Error("Missing password")
83+
throw new Error(i18n.t("MISS_PASSWORD") as string)
7884
}
7985

8086
const passwordMethod = getPasswordMethod(hashedPasswordFromArgs)
@@ -108,7 +114,7 @@ router.post<{}, string, { password: string; base?: string }, { to?: string }>("/
108114
}),
109115
)
110116

111-
throw new Error("Incorrect password")
117+
throw new Error(i18n.t("INCORRECT_PASSWORD") as string)
112118
} catch (error: any) {
113119
const renderedHtml = await getRoot(req, error)
114120
res.send(renderedHtml)

test/unit/node/routes/login.test.ts

+11
Original file line numberDiff line numberDiff line change
@@ -138,5 +138,16 @@ describe("login", () => {
138138
expect(resp.status).toBe(200)
139139
expect(htmlContent).toContain(`Welcome to ${appName}`)
140140
})
141+
142+
it("should return correct welcome text when locale is set to non-English", async () => {
143+
process.env.PASSWORD = previousEnvPassword
144+
const locale = "zh-cn"
145+
const codeServer = await integration.setup([`--locale=${locale}`], "")
146+
const resp = await codeServer.fetch("/login", { method: "GET" })
147+
148+
const htmlContent = await resp.text()
149+
expect(resp.status).toBe(200)
150+
expect(htmlContent).toContain(`欢迎来到 code-server`)
151+
})
141152
})
142153
})

tsconfig.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@
2222
"./test/node_modules/@types",
2323
"./lib/vscode/src/vs/server/@types"
2424
],
25-
"downlevelIteration": true
25+
"downlevelIteration": true,
26+
"resolveJsonModule": true
2627
},
2728
"include": ["./src/**/*"],
2829
"exclude": ["/test", "/lib", "/ci", "/doc"]

yarn.lock

+19
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,13 @@
22
# yarn lockfile v1
33

44

5+
"@babel/runtime@^7.20.6":
6+
version "7.20.7"
7+
resolved "https://registry.npmmirror.com/@babel/runtime/-/runtime-7.20.7.tgz#fcb41a5a70550e04a7b708037c7c32f7f356d8fd"
8+
integrity sha512-UF0tvkUtxwAgZ5W/KrkHf0Rn0fdnLDU9ScxBrEVNUprE/MzirjK4MJUX1/BVDv00Sv8cljtukVK1aky++X1SjQ==
9+
dependencies:
10+
regenerator-runtime "^0.13.11"
11+
512
"@coder/logger@^3.0.0":
613
version "3.0.0"
714
resolved "https://registry.yarnpkg.com/@coder/logger/-/logger-3.0.0.tgz#fd4d2332ca375412c75cb5ba7767d3290b106dec"
@@ -1814,6 +1821,13 @@ https-proxy-agent@5, https-proxy-agent@^5.0.0:
18141821
agent-base "6"
18151822
debug "4"
18161823

1824+
i18next@^22.4.6:
1825+
version "22.4.6"
1826+
resolved "https://registry.npmmirror.com/i18next/-/i18next-22.4.6.tgz#876352c3ba81bdfedc38eeda124e2bbd05f46988"
1827+
integrity sha512-9Tm1ezxWyzV+306CIDMBbYBitC1jedQyYuuLtIv7oxjp2ohh8eyxP9xytIf+2bbQfhH784IQKPSYp+Zq9+YSbw==
1828+
dependencies:
1829+
"@babel/runtime" "^7.20.6"
1830+
18171831
18181832
version "0.4.24"
18191833
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
@@ -2873,6 +2887,11 @@ [email protected]:
28732887
resolved "https://registry.yarnpkg.com/readline-transform/-/readline-transform-1.0.0.tgz#3157f97428acaec0f05a5c1ff2c3120f4e6d904b"
28742888
integrity sha512-7KA6+N9IGat52d83dvxnApAWN+MtVb1MiVuMR/cf1O4kYsJG+g/Aav0AHcHKsb6StinayfPLne0+fMX2sOzAKg==
28752889

2890+
regenerator-runtime@^0.13.11:
2891+
version "0.13.11"
2892+
resolved "https://registry.npmmirror.com/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz#f6dca3e7ceec20590d07ada785636a90cdca17f9"
2893+
integrity sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==
2894+
28762895
regexp.prototype.flags@^1.4.3:
28772896
version "1.4.3"
28782897
resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz#87cab30f80f66660181a3bb7bf5981a872b367ac"

0 commit comments

Comments
 (0)