mirror of https://github.com/coder/code-server.git
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 <jjprevite@gmail.com>
This commit is contained in:
parent
d40a9742c0
commit
7c2aa8c417
|
@ -3,3 +3,5 @@
|
|||
ci/helm-chart/ @Matthew-Beckett @alexgorbatchev
|
||||
|
||||
docs/install.md @GNUxeava
|
||||
|
||||
src/node/i18n/locales/zh-cn.json @zhaozhiming
|
||||
|
|
|
@ -95,6 +95,7 @@
|
|||
"express": "5.0.0-alpha.8",
|
||||
"http-proxy": "^1.18.0",
|
||||
"httpolyglot": "^0.1.2",
|
||||
"i18next": "^22.4.6",
|
||||
"js-yaml": "^4.0.0",
|
||||
"limiter": "^1.1.5",
|
||||
"pem": "^1.14.2",
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
http-equiv="Content-Security-Policy"
|
||||
content="style-src 'self'; script-src 'self' 'unsafe-inline'; manifest-src 'self'; img-src 'self' data:; font-src 'self' data:;"
|
||||
/>
|
||||
<title>{{APP_NAME}} login</title>
|
||||
<title>{{I18N_LOGIN_TITLE}}</title>
|
||||
<link rel="icon" href="{{CS_STATIC_BASE}}/src/browser/media/favicon-dark-support.svg" />
|
||||
<link rel="alternate icon" href="{{CS_STATIC_BASE}}/src/browser/media/favicon.ico" />
|
||||
<link rel="manifest" href="{{BASE}}/manifest.json" crossorigin="use-credentials" />
|
||||
|
@ -25,7 +25,7 @@
|
|||
<div class="card-box">
|
||||
<div class="header">
|
||||
<h1 class="main">{{WELCOME_TEXT}}</h1>
|
||||
<div class="sub">Please log in below. {{PASSWORD_MSG}}</div>
|
||||
<div class="sub">{{I18N_LOGIN_BELOW}} {{PASSWORD_MSG}}</div>
|
||||
</div>
|
||||
<div class="content">
|
||||
<form class="login-form" method="post">
|
||||
|
@ -38,11 +38,11 @@
|
|||
autofocus
|
||||
class="password"
|
||||
type="password"
|
||||
placeholder="PASSWORD"
|
||||
placeholder="{{I18N_PASSWORD_PLACEHOLDER}}"
|
||||
name="password"
|
||||
autocomplete="current-password"
|
||||
/>
|
||||
<input class="submit -button" value="SUBMIT" type="submit" />
|
||||
<input class="submit -button" value="{{I18N_SUBMIT}}" type="submit" />
|
||||
</div>
|
||||
{{ERROR}}
|
||||
</form>
|
||||
|
|
|
@ -180,7 +180,14 @@ export const options: Options<Required<UserProvidedArgs>> = {
|
|||
enable: { type: "string[]" },
|
||||
help: { type: "boolean", short: "h", description: "Show this output." },
|
||||
json: { type: "boolean" },
|
||||
locale: { type: "string" }, // The preferred way to set the locale is via the UI.
|
||||
locale: {
|
||||
// The preferred way to set the locale is via the UI.
|
||||
type: "string",
|
||||
description: `
|
||||
Set vscode display language and language to show on the login page, more info see
|
||||
https://en.wikipedia.org/wiki/IETF_language_tag
|
||||
`,
|
||||
},
|
||||
open: { type: "boolean", description: "Open in browser on startup. Does not work remotely." },
|
||||
|
||||
"bind-addr": {
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
import i18next, { init } from "i18next"
|
||||
import * as en from "./locales/en.json"
|
||||
import * as zhCn from "./locales/zh-cn.json"
|
||||
|
||||
init({
|
||||
lng: "en",
|
||||
fallbackLng: "en", // language to use if translations in user language are not available.
|
||||
returnNull: false,
|
||||
lowerCaseLng: true,
|
||||
debug: process.env.NODE_ENV === "development",
|
||||
resources: {
|
||||
en: {
|
||||
translation: en,
|
||||
},
|
||||
"zh-cn": {
|
||||
translation: zhCn,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
export default i18next
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"LOGIN_TITLE": "{{app}} login",
|
||||
"LOGIN_BELOW": "Please log in below.",
|
||||
"WELCOME": "Welcome to {{app}}",
|
||||
"LOGIN_PASSWORD": "Check the config file at {{configFile}} for the password.",
|
||||
"LOGIN_USING_ENV_PASSWORD": "Password was set from $PASSWORD.",
|
||||
"LOGIN_USING_HASHED_PASSWORD": "Password was set from $HASHED_PASSWORD.",
|
||||
"SUBMIT": "SUBMIT",
|
||||
"PASSWORD_PLACEHOLDER": "PASSWORD",
|
||||
"LOGIN_RATE_LIMIT": "Login rate limited!",
|
||||
"MISS_PASSWORD": "Missing password",
|
||||
"INCORRECT_PASSWORD": "Incorrect password"
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"LOGIN_TITLE": "{{app}} 登录",
|
||||
"LOGIN_BELOW": "请在下面登录。",
|
||||
"WELCOME": "欢迎来到 {{app}}",
|
||||
"LOGIN_PASSWORD": "查看配置文件 {{configFile}} 中的密码。",
|
||||
"LOGIN_USING_ENV_PASSWORD": "密码在 $PASSWORD 中设置。",
|
||||
"LOGIN_USING_HASHED_PASSWORD": "密码在 $HASHED_PASSWORD 中设置。",
|
||||
"SUBMIT": "提交",
|
||||
"PASSWORD_PLACEHOLDER": "密码",
|
||||
"LOGIN_RATE_LIMIT": "登录速率限制!",
|
||||
"MISS_PASSWORD": "缺少密码",
|
||||
"INCORRECT_PASSWORD": "密码不正确"
|
||||
}
|
|
@ -7,6 +7,7 @@ import { CookieKeys } from "../../common/http"
|
|||
import { rootPath } from "../constants"
|
||||
import { authenticated, getCookieOptions, redirect, replaceTemplates } from "../http"
|
||||
import { getPasswordMethod, handlePasswordValidation, humanPath, sanitizeString, escapeHtml } from "../util"
|
||||
import i18n from "../i18n"
|
||||
|
||||
// RateLimiter wraps around the limiter library for logins.
|
||||
// It allows 2 logins every minute plus 12 logins every hour.
|
||||
|
@ -28,21 +29,26 @@ export class RateLimiter {
|
|||
|
||||
const getRoot = async (req: Request, error?: Error): Promise<string> => {
|
||||
const content = await fs.readFile(path.join(rootPath, "src/browser/pages/login.html"), "utf8")
|
||||
const locale = req.args["locale"] || "en"
|
||||
i18n.changeLanguage(locale)
|
||||
const appName = req.args["app-name"] || "code-server"
|
||||
const welcomeText = req.args["welcome-text"] || `Welcome to ${appName}`
|
||||
let passwordMsg = `Check the config file at ${humanPath(os.homedir(), req.args.config)} for the password.`
|
||||
const welcomeText = req.args["welcome-text"] || (i18n.t("WELCOME", { app: appName }) as string)
|
||||
let passwordMsg = i18n.t("LOGIN_PASSWORD", { configFile: humanPath(os.homedir(), req.args.config) })
|
||||
if (req.args.usingEnvPassword) {
|
||||
passwordMsg = "Password was set from $PASSWORD."
|
||||
passwordMsg = i18n.t("LOGIN_USING_ENV_PASSWORD")
|
||||
} else if (req.args.usingEnvHashedPassword) {
|
||||
passwordMsg = "Password was set from $HASHED_PASSWORD."
|
||||
passwordMsg = i18n.t("LOGIN_USING_HASHED_PASSWORD")
|
||||
}
|
||||
|
||||
return replaceTemplates(
|
||||
req,
|
||||
content
|
||||
.replace(/{{APP_NAME}}/g, appName)
|
||||
.replace(/{{I18N_LOGIN_TITLE}}/g, i18n.t("LOGIN_TITLE", { app: appName }))
|
||||
.replace(/{{WELCOME_TEXT}}/g, welcomeText)
|
||||
.replace(/{{PASSWORD_MSG}}/g, passwordMsg)
|
||||
.replace(/{{I18N_LOGIN_BELOW}}/g, i18n.t("LOGIN_BELOW"))
|
||||
.replace(/{{I18N_PASSWORD_PLACEHOLDER}}/g, i18n.t("PASSWORD_PLACEHOLDER"))
|
||||
.replace(/{{I18N_SUBMIT}}/g, i18n.t("SUBMIT"))
|
||||
.replace(/{{ERROR}}/, error ? `<div class="error">${escapeHtml(error.message)}</div>` : ""),
|
||||
)
|
||||
}
|
||||
|
@ -70,11 +76,11 @@ router.post<{}, string, { password: string; base?: string }, { to?: string }>("/
|
|||
try {
|
||||
// Check to see if they exceeded their login attempts
|
||||
if (!limiter.canTry()) {
|
||||
throw new Error("Login rate limited!")
|
||||
throw new Error(i18n.t("LOGIN_RATE_LIMIT") as string)
|
||||
}
|
||||
|
||||
if (!password) {
|
||||
throw new Error("Missing password")
|
||||
throw new Error(i18n.t("MISS_PASSWORD") as string)
|
||||
}
|
||||
|
||||
const passwordMethod = getPasswordMethod(hashedPasswordFromArgs)
|
||||
|
@ -108,7 +114,7 @@ router.post<{}, string, { password: string; base?: string }, { to?: string }>("/
|
|||
}),
|
||||
)
|
||||
|
||||
throw new Error("Incorrect password")
|
||||
throw new Error(i18n.t("INCORRECT_PASSWORD") as string)
|
||||
} catch (error: any) {
|
||||
const renderedHtml = await getRoot(req, error)
|
||||
res.send(renderedHtml)
|
||||
|
|
|
@ -138,5 +138,16 @@ describe("login", () => {
|
|||
expect(resp.status).toBe(200)
|
||||
expect(htmlContent).toContain(`Welcome to ${appName}`)
|
||||
})
|
||||
|
||||
it("should return correct welcome text when locale is set to non-English", async () => {
|
||||
process.env.PASSWORD = previousEnvPassword
|
||||
const locale = "zh-cn"
|
||||
const codeServer = await integration.setup([`--locale=${locale}`], "")
|
||||
const resp = await codeServer.fetch("/login", { method: "GET" })
|
||||
|
||||
const htmlContent = await resp.text()
|
||||
expect(resp.status).toBe(200)
|
||||
expect(htmlContent).toContain(`欢迎来到 code-server`)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -22,7 +22,8 @@
|
|||
"./test/node_modules/@types",
|
||||
"./lib/vscode/src/vs/server/@types"
|
||||
],
|
||||
"downlevelIteration": true
|
||||
"downlevelIteration": true,
|
||||
"resolveJsonModule": true
|
||||
},
|
||||
"include": ["./src/**/*"],
|
||||
"exclude": ["/test", "/lib", "/ci", "/doc"]
|
||||
|
|
19
yarn.lock
19
yarn.lock
|
@ -2,6 +2,13 @@
|
|||
# yarn lockfile v1
|
||||
|
||||
|
||||
"@babel/runtime@^7.20.6":
|
||||
version "7.20.7"
|
||||
resolved "https://registry.npmmirror.com/@babel/runtime/-/runtime-7.20.7.tgz#fcb41a5a70550e04a7b708037c7c32f7f356d8fd"
|
||||
integrity sha512-UF0tvkUtxwAgZ5W/KrkHf0Rn0fdnLDU9ScxBrEVNUprE/MzirjK4MJUX1/BVDv00Sv8cljtukVK1aky++X1SjQ==
|
||||
dependencies:
|
||||
regenerator-runtime "^0.13.11"
|
||||
|
||||
"@coder/logger@^3.0.0":
|
||||
version "3.0.0"
|
||||
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:
|
|||
agent-base "6"
|
||||
debug "4"
|
||||
|
||||
i18next@^22.4.6:
|
||||
version "22.4.6"
|
||||
resolved "https://registry.npmmirror.com/i18next/-/i18next-22.4.6.tgz#876352c3ba81bdfedc38eeda124e2bbd05f46988"
|
||||
integrity sha512-9Tm1ezxWyzV+306CIDMBbYBitC1jedQyYuuLtIv7oxjp2ohh8eyxP9xytIf+2bbQfhH784IQKPSYp+Zq9+YSbw==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.20.6"
|
||||
|
||||
iconv-lite@0.4.24:
|
||||
version "0.4.24"
|
||||
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
|
||||
|
@ -2873,6 +2887,11 @@ readline-transform@1.0.0:
|
|||
resolved "https://registry.yarnpkg.com/readline-transform/-/readline-transform-1.0.0.tgz#3157f97428acaec0f05a5c1ff2c3120f4e6d904b"
|
||||
integrity sha512-7KA6+N9IGat52d83dvxnApAWN+MtVb1MiVuMR/cf1O4kYsJG+g/Aav0AHcHKsb6StinayfPLne0+fMX2sOzAKg==
|
||||
|
||||
regenerator-runtime@^0.13.11:
|
||||
version "0.13.11"
|
||||
resolved "https://registry.npmmirror.com/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz#f6dca3e7ceec20590d07ada785636a90cdca17f9"
|
||||
integrity sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==
|
||||
|
||||
regexp.prototype.flags@^1.4.3:
|
||||
version "1.4.3"
|
||||
resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz#87cab30f80f66660181a3bb7bf5981a872b367ac"
|
||||
|
|
Loading…
Reference in New Issue