Skip to content

Commit 5f11fac

Browse files
committed
feat: add i18n in login page
1 parent 9e9cbd8 commit 5f11fac

File tree

11 files changed

+108
-12
lines changed

11 files changed

+108
-12
lines changed

Diff for: 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",

Diff for: 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>{{APP_NAME}} {{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>

Diff for: src/node/cli.ts

+8
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ export interface UserProvidedArgs extends UserProvidedCodeArgs {
8888
verbose?: boolean
8989
"app-name"?: string
9090
"welcome-text"?: string
91+
"lng"?: string
9192
/* Positional arguments. */
9293
_?: string[]
9394
}
@@ -264,6 +265,13 @@ export const options: Options<Required<UserProvidedArgs>> = {
264265
`,
265266
deprecated: true,
266267
},
268+
"lng": {
269+
type: "string",
270+
description: `
271+
Language show on login page, more infomations to read up on
272+
https://en.wikipedia.org/wiki/IETF_language_tag.
273+
`,
274+
},
267275
}
268276

269277
export const optionDescriptions = (opts: Partial<Options<Required<UserProvidedArgs>>> = options): string[] => {

Diff for: src/node/i18n/index.ts

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import i18next from "i18next"
2+
import * as en from "./locales/en.json"
3+
import * as zhCn from "./locales/zh-cn.json"
4+
5+
i18next.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

Diff for: src/node/i18n/locales/en.json

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"LOGIN_TITLE": "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+
}

Diff for: src/node/i18n/locales/zh-cn.json

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"LOGIN_TITLE": "登录",
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+
}

Diff for: src/node/routes/login.ts

+14-7
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,13 +29,15 @@ 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 lng = req.args["lng"] || "en"
33+
i18n.changeLanguage(lng)
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(
@@ -43,6 +46,10 @@ const getRoot = async (req: Request, error?: Error): Promise<string> => {
4346
.replace(/{{APP_NAME}}/g, appName)
4447
.replace(/{{WELCOME_TEXT}}/g, welcomeText)
4548
.replace(/{{PASSWORD_MSG}}/g, passwordMsg)
49+
.replace(/{{I18N_LOGIN_TITLE}}/g, i18n.t("LOGIN_TITLE"))
50+
.replace(/{{I18N_LOGIN_BELOW}}/g, i18n.t("LOGIN_BELOW"))
51+
.replace(/{{I18N_PASSWORD_PLACEHOLDER}}/g, i18n.t("PASSWORD_PLACEHOLDER"))
52+
.replace(/{{I18N_SUBMIT}}/g, i18n.t("SUBMIT"))
4653
.replace(/{{ERROR}}/, error ? `<div class="error">${escapeHtml(error.message)}</div>` : ""),
4754
)
4855
}
@@ -70,11 +77,11 @@ router.post<{}, string, { password: string; base?: string }, { to?: string }>("/
7077
try {
7178
// Check to see if they exceeded their login attempts
7279
if (!limiter.canTry()) {
73-
throw new Error("Login rate limited!")
80+
throw new Error(i18n.t("LOGIN_RATE_LIMIT") as string)
7481
}
7582

7683
if (!password) {
77-
throw new Error("Missing password")
84+
throw new Error(i18n.t("MISS_PASSWORD") as string)
7885
}
7986

8087
const passwordMethod = getPasswordMethod(hashedPasswordFromArgs)
@@ -108,7 +115,7 @@ router.post<{}, string, { password: string; base?: string }, { to?: string }>("/
108115
}),
109116
)
110117

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

Diff for: test/unit/node/cli.test.ts

+2
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ describe("parser", () => {
7070
"--verbose",
7171
["--app-name", "custom instance name"],
7272
["--welcome-text", "welcome to code"],
73+
["--lng", "zh-cn"],
7374
"2",
7475

7576
["--locale", "ja"],
@@ -131,6 +132,7 @@ describe("parser", () => {
131132
verbose: true,
132133
"app-name": "custom instance name",
133134
"welcome-text": "welcome to code",
135+
"lng": "zh-cn",
134136
version: true,
135137
"bind-addr": "192.169.0.1:8080",
136138
})

Diff for: 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 lng is set to non-English", async () => {
143+
process.env.PASSWORD = previousEnvPassword
144+
const lng = "zh-cn"
145+
const codeServer = await integration.setup([`--lng=${lng}`], "")
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
})

Diff for: 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"]

Diff for: 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)