Ban domain?
+{{t "table.admin.banDomainDialogTitle"}}
- Are you sure do you want to ban the domain "{{address}}"? + {{{t "table.admin.banDomainDialogMsg" address=address}}}
diff --git a/.example.env b/.example.env index 6e9c4d3b1..a59b5f3b8 100644 --- a/.example.env +++ b/.example.env @@ -38,6 +38,10 @@ LINK_LENGTH=6 # Default value omits o, O, 0, i, I, l, 1, and j to avoid confusion when reading the URL LINK_CUSTOM_ALPHABET=abcdefghkmnpqrstuvwxyzABCDEFGHKLMNPQRSTUVWXYZ23456789 +# Optional - Allowed locales and translations Supported locales: +# en (English), zh_CN (Simplified Chinese), zh_TW (Traditional Chinese) +LOCALES=en + # Optional - Tells the app that it's running behind a proxy server # and that it should get the IP address from that proxy server # if you're not using a proxy server then set this to false, otherwise users can override their IP address diff --git a/.gitignore b/.gitignore index 20f118b70..5281cb807 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,6 @@ docs/api/static **/.DS_Store db/* !db/.gitkeep +.history/** +**.bak +tags \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 00c1f0bed..7ddddabb4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,7 @@ "cookie-parser": "1.4.7", "cors": "2.8.5", "date-fns": "2.30.0", - "dotenv": "^16.4.7", + "dotenv": "16.4.7", "envalid": "8.0.0", "express": "4.21.2", "express-rate-limit": "7.5.0", @@ -23,6 +23,7 @@ "geoip-lite": "1.4.10", "hbs": "4.2.0", "helmet": "7.1.0", + "i18n": "0.15.1", "ioredis": "5.4.2", "isbot": "5.1.19", "jsonwebtoken": "9.0.2", @@ -553,6 +554,45 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@messageformat/core": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/@messageformat/core/-/core-3.4.0.tgz", + "integrity": "sha512-NgCFubFFIdMWJGN5WuQhHCNmzk7QgiVfrViFxcS99j7F5dDS5EP6raR54I+2ydhe4+5/XTn/YIEppFaqqVWHsw==", + "dependencies": { + "@messageformat/date-skeleton": "^1.0.0", + "@messageformat/number-skeleton": "^1.0.0", + "@messageformat/parser": "^5.1.0", + "@messageformat/runtime": "^3.0.1", + "make-plural": "^7.0.0", + "safe-identifier": "^0.4.1" + } + }, + "node_modules/@messageformat/date-skeleton": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@messageformat/date-skeleton/-/date-skeleton-1.1.0.tgz", + "integrity": "sha512-rmGAfB1tIPER+gh3p/RgA+PVeRE/gxuQ2w4snFWPF5xtb5mbWR7Cbw7wCOftcUypbD6HVoxrVdyyghPm3WzP5A==" + }, + "node_modules/@messageformat/number-skeleton": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@messageformat/number-skeleton/-/number-skeleton-1.2.0.tgz", + "integrity": "sha512-xsgwcL7J7WhlHJ3RNbaVgssaIwcEyFkBqxHdcdaiJzwTZAWEOD8BuUFxnxV9k5S0qHN3v/KzUpq0IUpjH1seRg==" + }, + "node_modules/@messageformat/parser": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@messageformat/parser/-/parser-5.1.1.tgz", + "integrity": "sha512-3p0YRGCcTUCYvBKLIxtDDyrJ0YijGIwrTRu1DT8gIviIDZru8H23+FkY6MJBzM1n9n20CiM4VeDYuBsrrwnLjg==", + "dependencies": { + "moo": "^0.5.1" + } + }, + "node_modules/@messageformat/runtime": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@messageformat/runtime/-/runtime-3.0.1.tgz", + "integrity": "sha512-6RU5ol2lDtO8bD9Yxe6CZkl0DArdv0qkuoZC+ZwowU+cdRlVE1157wjCmlA5Rsf1Xc/brACnsZa5PZpEDfTFFg==", + "dependencies": { + "make-plural": "^7.0.0" + } + }, "node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.3.tgz", @@ -1645,7 +1685,6 @@ "version": "1.4.7", "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.7.tgz", "integrity": "sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==", - "license": "MIT", "dependencies": { "cookie": "0.7.2", "cookie-signature": "1.0.6" @@ -2526,6 +2565,14 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-printf": { + "version": "1.6.10", + "resolved": "https://registry.npmjs.org/fast-printf/-/fast-printf-1.6.10.tgz", + "integrity": "sha512-GwTgG9O4FVIdShhbVF3JxOgSBY2+ePGsu2V/UONgoCPzF9VY6ZdBMKsHKCYQHZwNk3qNouUolRDsgVxcVA5G1w==", + "engines": { + "node": ">=10.0" + } + }, "node_modules/fast-safe-stringify": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", @@ -3148,6 +3195,41 @@ } } }, + "node_modules/i18n": { + "version": "0.15.1", + "resolved": "https://registry.npmjs.org/i18n/-/i18n-0.15.1.tgz", + "integrity": "sha512-yue187t8MqUPMHdKjiZGrX+L+xcUsDClGO0Cz4loaKUOK9WrGw5pgan4bv130utOwX7fHE9w2iUeHFalVQWkXA==", + "dependencies": { + "@messageformat/core": "^3.0.0", + "debug": "^4.3.3", + "fast-printf": "^1.6.9", + "make-plural": "^7.0.0", + "math-interval-parser": "^2.0.1", + "mustache": "^4.2.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/mashpie" + } + }, + "node_modules/i18n/node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, "node_modules/iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -4040,6 +4122,11 @@ "node": ">=12" } }, + "node_modules/make-plural": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/make-plural/-/make-plural-7.4.0.tgz", + "integrity": "sha512-4/gC9KVNTV6pvYg2gFeQYTW3mWaoJt7WZE5vrp1KnQDgW92JtYZnzmZT81oj/dUTqAIu0ufI2x3dkgu3bB1tYg==" + }, "node_modules/mark.js": { "version": "8.11.1", "resolved": "https://registry.npmjs.org/mark.js/-/mark.js-8.11.1.tgz", @@ -4060,6 +4147,14 @@ "node": ">= 12" } }, + "node_modules/math-interval-parser": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/math-interval-parser/-/math-interval-parser-2.0.1.tgz", + "integrity": "sha512-VmlAmb0UJwlvMyx8iPhXUDnVW1F9IrGEd9CIOmv+XL8AErCUUuozoDMrgImvnYt2A+53qVX/tPW6YJurMKYsvA==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -4237,9 +4332,7 @@ "version": "0.5.2", "resolved": "https://registry.npmjs.org/moo/-/moo-0.5.2.tgz", "integrity": "sha512-iSAJLHYKnX41mKcJKjqvnAN9sf0LMDTXDEvFv+ffuRR9a1MIuXLjMNL6EsnDHSkKLTWNqQQ5uo61P4EbU4NU+Q==", - "dev": true, - "license": "BSD-3-Clause", - "peer": true + "license": "BSD-3-Clause" }, "node_modules/ms": { "version": "2.1.3", @@ -4278,6 +4371,14 @@ "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.3" } }, + "node_modules/mustache": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz", + "integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==", + "bin": { + "mustache": "bin/mustache" + } + }, "node_modules/mysql2": { "version": "3.12.0", "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.12.0.tgz", @@ -5681,6 +5782,11 @@ ], "license": "MIT" }, + "node_modules/safe-identifier": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/safe-identifier/-/safe-identifier-0.4.2.tgz", + "integrity": "sha512-6pNbSMW6OhAi9j+N8V+U715yBQsaWJ7eyEUaOrawX+isg5ZxhUlV1NipNtgaKHmFGiABwt+ZF04Ii+3Xjkg+8w==" + }, "node_modules/safe-push-apply": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", diff --git a/package.json b/package.json index 84ffaa872..75493c4e0 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "geoip-lite": "1.4.10", "hbs": "4.2.0", "helmet": "7.1.0", + "i18n": "0.15.1", "ioredis": "5.4.2", "isbot": "5.1.19", "jsonwebtoken": "9.0.2", diff --git a/server/env.js b/server/env.js index 87f7bc373..b0a554cde 100644 --- a/server/env.js +++ b/server/env.js @@ -1,5 +1,5 @@ require("dotenv").config(); -const { cleanEnv, num, str, bool } = require("envalid"); +const { cleanEnv, num, str, bool, makeValidator } = require("envalid"); const { readFileSync } = require("node:fs"); const supportedDBClients = [ @@ -26,12 +26,34 @@ if (process.argv.includes("--production")) { process.env.NODE_ENV = "production"; } +// custom locales validator +const allowedLocales = ["en", "zh_CN","zh_TW"]; +const localesValidator = makeValidator(function (locales) { + if (typeof locales !== "string") return "en"; + return locales + .split(",") + .map(l => l.trim().toLowerCase()) + .map(function (locale) { + const index = allowedLocales.findIndex(l => l.toLowerCase() === locale); + if (index !== -1) { + return allowedLocales[index]; + } else { + throw new Error( + `Locale is not supported: ${locale}.` + + `Available locales: ${allowedLocales.join(", ")}` + ); + } + }) + .join(","); +}); + const spec = { PORT: num({ default: 3000 }), SITE_NAME: str({ example: "Kutt", default: "Kutt" }), DEFAULT_DOMAIN: str({ example: "kutt.it", default: "localhost:3000" }), LINK_LENGTH: num({ default: 6 }), LINK_CUSTOM_ALPHABET: str({ default: "abcdefghkmnpqrstuvwxyzABCDEFGHKLMNPQRSTUVWXYZ23456789" }), + LOCALES: localesValidator({ default: "en,zh_CN,zh_TW" }), TRUST_PROXY: bool({ default: true }), DB_CLIENT: str({ choices: supportedDBClients, default: "better-sqlite3" }), DB_FILENAME: str({ default: "db/data" }), diff --git a/server/handlers/helpers.handler.js b/server/handlers/helpers.handler.js index ff53454b3..6f0c948eb 100644 --- a/server/handlers/helpers.handler.js +++ b/server/handlers/helpers.handler.js @@ -1,6 +1,7 @@ const { RedisStore: RateLimitRedisStore } = require("rate-limit-redis"); const { rateLimit: expressRateLimit } = require("express-rate-limit"); const { validationResult } = require("express-validator"); +const i18n = require("i18n"); const { CustomError } = require("../utils"); const query = require("../queries"); @@ -14,7 +15,7 @@ function error(error, req, res, _next) { console.error(error.message); } - const message = error instanceof CustomError ? error.message : "An error occurred."; + const message = error instanceof CustomError ? error.message : i18n.__("backbone.errorMsg"); const statusCode = error.statusCode ?? 500; if (req.isHTML && req.viewTemplate) { @@ -25,7 +26,7 @@ function error(error, req, res, _next) { if (req.isHTML) { res.render("error", { - message: "An error occurred. Please try again later." + message: i18n.__("backbone.errorAndTryAgainMsg"), }); return; } diff --git a/server/handlers/renders.handler.js b/server/handlers/renders.handler.js index 79058fdb4..096f78754 100644 --- a/server/handlers/renders.handler.js +++ b/server/handlers/renders.handler.js @@ -1,3 +1,5 @@ +const i18n = require("i18n"); + const query = require("../queries"); const utils = require("../utils"); const env = require("../env"); @@ -14,7 +16,7 @@ async function homepage(req, res) { return; } res.render("homepage", { - title: "Free modern URL shortener", + title: i18n.__("title.homepage"), }); } @@ -25,14 +27,14 @@ async function login(req, res) { } res.render("login", { - title: "Log in or sign up" + title: i18n.__("title.loginOrSignup") }); } function logout(req, res) { utils.deleteCurrentToken(res); res.render("logout", { - title: "Logging out.." + title: i18n.__("title.logout") }); } @@ -43,37 +45,37 @@ async function createAdmin(req, res) { return; } res.render("create_admin", { - title: "Create admin account" + title: i18n.__("title.createAdmin") }); } function notFound(req, res) { res.render("404", { - title: "404 - Not found" + title: i18n.__("title.404") }); } function settings(req, res) { res.render("settings", { - title: "Settings" + title: i18n.__("title.settings") }); } function admin(req, res) { res.render("admin", { - title: "Admin" + title: i18n.__("title.admin") }); } function stats(req, res) { res.render("stats", { - title: "Stats" + title: i18n.__("title.stats") }); } async function banned(req, res) { res.render("banned", { - title: "Banned link", + title: i18n.__("title.bannedLink") }); } @@ -83,13 +85,13 @@ async function report(req, res) { return; } res.render("report", { - title: "Report abuse", + title: i18n.__("title.reportAbuse") }); } async function resetPassword(req, res) { res.render("reset_password", { - title: "Reset password", + title: i18n.__("title.resetPassword") }); } @@ -110,26 +112,26 @@ async function resetPasswordSetNewPassword(req, res) { res.render("reset_password_set_new_password", { - title: "Reset password", + title: i18n.__("title.resetPassword"), ...(res.locals.token_verified && { reset_password_token }), }); } async function verifyChangeEmail(req, res) { res.render("verify_change_email", { - title: "Verifying email", + title: i18n.__("title.verifyChangeEmail") }); } async function verify(req, res) { res.render("verify", { - title: "Verify", + title: i18n.__("title.verify") }); } async function terms(req, res) { res.render("terms", { - title: "Terms of Service", + title: i18n.__("title.termsOfService") }); } @@ -147,7 +149,7 @@ async function confirmLinkDelete(req, res) { if (!link) { return res.render("partials/links/dialog/message", { layout: false, - message: "Could not find the link." + message: i18n.__("message.confirmLinkDeleteMessage") }); } res.render("partials/links/dialog/delete", { @@ -164,7 +166,7 @@ async function confirmLinkBan(req, res) { }); if (!link) { return res.render("partials/links/dialog/message", { - message: "Could not find the link." + message: i18n.__("message.confirmLinkBanMessage") }); } res.render("partials/links/dialog/ban", { @@ -178,7 +180,7 @@ async function confirmUserDelete(req, res) { if (!user) { return res.render("partials/admin/dialog/message", { layout: false, - message: "Could not find the user." + message: i18n.__("message.confirmUserDeleteMessage") }); } res.render("partials/admin/dialog/delete_user", { @@ -193,7 +195,7 @@ async function confirmUserBan(req, res) { if (!user) { return res.render("partials/admin/dialog/message", { layout: false, - message: "Could not find the user." + message: i18n.__("message.confirmUserBanMessage") }); } res.render("partials/admin/dialog/ban_user", { @@ -225,7 +227,7 @@ async function confirmDomainDelete(req, res) { user_id: req.user.id }); if (!domain) { - throw new utils.CustomError("Could not find the domain.", 400); + throw new utils.CustomError(i18n.__("message.confirmDomainDeleteMessage"), 400); } res.render("partials/settings/domain/delete", { ...utils.sanitize.domain(domain) @@ -237,7 +239,7 @@ async function confirmDomainBan(req, res) { id: req.query.id }); if (!domain) { - throw new utils.CustomError("Could not find the domain.", 400); + throw new utils.CustomError(i18n.__("message.confirmDomainBanMessage"), 400); } const hasUser = !!domain.user_id; const hasLink = await query.link.find({ domain_id: domain.id }); @@ -254,7 +256,7 @@ async function confirmDomainDeleteAdmin(req, res) { id: req.query.id }); if (!domain) { - throw new utils.CustomError("Could not find the domain.", 400); + throw new utils.CustomError(i18n.__("message.confirmDomainDeleteAdminMessage"), 400); } const hasLink = await query.link.find({ domain_id: domain.id }); res.render("partials/admin/dialog/delete_domain", { @@ -266,7 +268,7 @@ async function confirmDomainDeleteAdmin(req, res) { async function getReportEmail(req, res) { if (!env.REPORT_EMAIL) { - throw new utils.CustomError("No report email is available.", 400); + throw new utils.CustomError(i18n.__("message.reportEmailMessage"), 400); } res.render("partials/report/email", { report_email_address: env.REPORT_EMAIL.replace("@", "[at]") @@ -275,7 +277,7 @@ async function getReportEmail(req, res) { async function getSupportEmail(req, res) { if (!env.CONTACT_EMAIL) { - throw new utils.CustomError("No support email is available.", 400); + throw new utils.CustomError(i18n.__("message.supportEmailMessage"), 400); } await utils.sleep(500); res.render("partials/support_email", { diff --git a/server/i18n.js b/server/i18n.js new file mode 100644 index 000000000..a56c983dd --- /dev/null +++ b/server/i18n.js @@ -0,0 +1,86 @@ +const { Router } = require("express"); +const path = require("node:path"); +const i18n = require("i18n"); + +const env = require("./env"); + +const localeMapping = { + zh_CN: { code: "zh_CN", name: "🇨🇳 简体中文" }, + zh_TW: { code: "zh_TW", name: "🇨🇳 繁體中文" }, + en: { code: "en", name: "🇬🇧 English" }, + en_US: { code: "en_US", name: "🇺🇸 English (US)" }, + en_GB: { code: "en_GB", name: "🇬🇧 English (UK)" }, + fr: { code: "fr", name: "🇫🇷 Français" }, + fr_FR: { code: "fr_FR", name: "🇫🇷 Français (France)" }, + de: { code: "de", name: "🇩🇪 Deutsch" }, + de_DE: { code: "de_DE", name: "🇩🇪 Deutsch (Deutschland)" }, + es: { code: "es", name: "🇪🇸 Español" }, + es_ES: { code: "es_ES", name: "🇪🇸 Español (España)" }, + ja: { code: "ja", name: "🇯🇵 日本語" }, + ja_JP: { code: "ja_JP", name: "🇯🇵 日本語 (日本)" }, + ko: { code: "ko", name: "🇰🇷 한국어" }, + ko_KR: { code: "ko_KR", name: "🇰🇷 한국어 (대한민국)" }, + ru: { code: "ru", name: "🇷🇺 Русский" }, + ru_RU: { code: "ru_RU", name: "🇷🇺 Русский (Россия)" }, + it: { code: "it", name: "🇮🇹 Italiano" }, + it_IT: { code: "it_IT", name: "🇮🇹 Italiano (Italia)" }, + pt: { code: "pt", name: "🇵🇹 Português" }, + pt_BR: { code: "pt_BR", name: "🇵🇹 Português (Brasil)" }, + ar: { code: "ar", name: "🇸🇦 العربية" }, + ar_SA: { code: "ar_SA", name: "🇸🇦 العربية (السعودية)" }, + hi: { code: "hi", name: "🇮🇳 हिन्दी" }, + hi_IN: { code: "hi_IN", name: "🇮🇳 हिन्दी (भारत)" }, +}; + +const router = new Router(); + +// configure locales and translations +const locales = env.LOCALES.split(","); +const defaultLocale = locales[0]; +i18n.configure({ + locales, + defaultLocale, + directory: path.join(__dirname, "locales"), + objectNotation: true, +}); +router.use(i18n.init); + +const localesList = i18n.getLocales().map(locale => localeMapping[locale]); + +// handle translations +function i18nHandler(req, res, next) { + const queryLang = req.query.lang; + const cookieLang = req.cookies.lang; + + let locale = defaultLocale; + + if (queryLang) { + // set local from query string if provided + locale = i18n.getLocales().find(l => l === queryLang) || defaultLocale; + } else if (!req.cookies.lang) { + // set locale from browser's language + const headerLang = req.get("Accept-Language").replace("-", "_"); + locale = i18n.getLocales().find(l => l === headerLang) || defaultLocale; + } else { + // get locale from cookie + locale = i18n.getLocales().find(l => l === cookieLang) || defaultLocale; + } + + i18n.setLocale(locale); + res.locals.currentLocale = locale; + res.locals.formattedLocales = localesList; + console.log(i18n.getLocales()); + + if (!req.cookies.lang || req.cookies.lang !== locale) { + res.cookie("lang", locale, { + maxAge: 365 * 24 * 60 * 60 * 1000, + httpOnly: true, + }); + } + + next(); +} + +router.use(i18nHandler); + +module.exports = router; \ No newline at end of file diff --git a/server/locales/en.json b/server/locales/en.json new file mode 100644 index 000000000..9930b9127 --- /dev/null +++ b/server/locales/en.json @@ -0,0 +1,425 @@ +{ + "header": { + "homepageAria": "Homepage", + "reportAbuse": "Report", + "reportAbuseAria": "Report Abuse", + "loginOrSignup": "Log in / Sign up", + "loginOrSignupAria": "Log in / Sign up", + "settings": "Settings", + "settingsAria": "Settings", + "logout": "Log out", + "logoutAria": "Log out", + "admin": "Admin", + "adminAria": "Admin", + "language": "Language", + "changeLanguage": "Change Language" + }, + "footer": { + "contactUs": "Contact Us", + "poweredBy": "Powered by", + "theDevsAria": "The Devs", + "termsOfService": "Terms of Service", + "termsOfServiceAria": "Terms of Service" + }, + "title": { + "404": "404 - Page not found", + "loginOrSignup": "Log in or Sign up", + "homepage": "Free modern URL shortener", + "logout": "Log out", + "settings": "Settings", + "admin": "Admin", + "stats": "Statistics", + "bannedLink": "Banned link", + "reportAbuse": "Report abuse", + "resetPassword": "Reset password", + "verifyChangeEmail": "Verify email change", + "verify": "Verify", + "termsOfService": "Terms of Service", + "confirmLinkDeleteMessage": "Could not find the link.", + "confirmLinkBanMessage": "Could not find the link.", + "confirmUserDeleteMessage": "Could not find the user.", + "confirmUserBanMessage": "Could not find the user.", + "createUserDialog": "Create user", + "addDomainDialog": "Add domain admin", + "addDomainForm": "Add domain form", + "confirmDomainDeleteMessage": "Could not find the domain.", + "confirmDomainBanMessage": "Could not find the domain.", + "confirmDomainDeleteAdminMessage": "Could not find the domain.", + "reportEmailMessage": "Report email address is unavailable.", + "supportEmailMessage": "Support email address is unavailable.", + "createAdmin": "Create Admin account" + }, + "login": { + "createAdminAccountMsg": "Create an Admin account first:", + "emailAddress": "Email Address:", + "emailAddressPlaceholder": "Email Address...", + "password": "Password:", + "passwordPlaceholder": "Password...", + "createAdminAccount": "Create admin account", + "verifyEmailHasBeenSent": "A verification email has been sent to you.", + "login": "Log in", + "signup": "Sign up", + "forgotPassword": "Forgot your password?", + "forgotPasswordAria": "Reset password", + "signUp": "login.signUp" + }, + "welcome": { + "welcomeAndRedirect": "Welcome. Redirecting to homepage...", + "welcomeUser": "Welcome, {{name}}", + "loggingoutAndRedirect": "Logged out. Redirecting to homepage...", + "emailVerifiedAndRedirect": "Email address is verified. Redirecting to homepage...", + "emailNotVerifiedAndTryAgain": "Couldn't verify the email address. Please try again.", + "verifiedAndRedirect": "Your account has been verified. Redirecting to homepage...", + "notVerifiedAndTryAgain": "Invalid verification. Please try again." + }, + "home": { + "slogan": "Cut your links ", + "sloganShorter": "shorter", + "pasteLongUrlPlaceholder": "Paste your long URL", + "showAdvancedOptions": "Show advanced options", + "optionDomain": "Domain:", + "customAddressPlaceholder": "Custom address...", + "optionPassword": "Password:", + "passwordPlaceholder": "Password...", + "optionExpireIn": "Expire in:", + "expireInPlaceholder": "2 minutes/hours/days", + "optionDescription": "Description:", + "descriptionPlaceholder": "Description...", + "unlockAndGo": "Unlock & Go" + }, + "table": { + "recentShortenedLinks": "Recent Shortened Links.", + "searchPlaceholder": "Search...", + "headerOriginalUrl": "Original URL", + "headerCreatedAt": "Created At", + "headerShortLink": "Short Link", + "headerViews": "Views", + "noLinks": "No links.", + "loadingLinks": "Loading links...", + "expireIn": "Expire in {{time}}", + "actions": { + "deleteDialogTitle": "Delete link?", + "deleteDialogMsg": "Are you sure do you want to delete the link \"{{link}}\"?", + "deleteSuccessMsg": "Your link \"{{link}}\" has been deleted.", + "banDialogTitle": "Ban link?", + "banDialogMsg": "Are you sure do you want to ban the link \"{{link}}\"?", + "banSuccessMsg": "The link \"{{link}}\" has been banned.", + "ban": "Ban", + "banUser": "User", + "banUserLinks": "User Links", + "banHost": "Host", + "banDomain": "Domain", + "editTarget": "Target:", + "editTargetPlaceholder": "Target URL...", + "editCustomUrlPlaceholder": "Custom URL...", + "editPassword": "Password:", + "editPasswordPlaceholder": "Password...", + "editDescription": "Description:", + "editDescriptionPlaceholder": "Description...", + "editExpireIn": "Expire in:", + "editExpireInPlaceholder": "2 minutes/hours/days", + "noLinkFound": "No link was found.", + "createDomain": "table.actions.createDomain" + }, + "options": { + "userLinks": "User link", + "userDomains": "User domain", + "owner": "Owner", + "links": "Links", + "user": "User", + "host": "Host", + "domain": "Domain" + }, + "admin": { + "searchLinkPlaceholder": "Search link...", + "clearLinkSearchAria": "Clear link search", + "searchDomainPlaceholder": "Search domain...", + "clearDomainSearchAria": "Clear domain search", + "searchUserPlaceholder": "Search user...", + "clearUserSearchAria": "Clear user search", + "filterBannedPlaceholder": "Banned...", + "filterBanned": "Banned", + "filterNotBanned": "Not Banned", + "filterAnonymousPlaceholder": "Anonymous...", + "filterAnonymous": "Anonymous", + "filterNotAnonymous": "User", + "filterDomainPlaceholder": "Domain...", + "filterWithDomain": "With Domain", + "filterWithoutDomain": "No Domain", + "viewLinks": "Links", + "viewDomains": "Domains", + "viewUsers": "Users", + "noDomains": "No domains", + "viewLinksByUser": "view links", + "anonymous": "Anonymous", + "recentCreatedUsers": "Recent created users", + "filterVerificationPlaceholder": "Verified...", + "filterVerified": "VERIFIED", + "filterNotVerified": "NOT VERIFIED", + "filterRolePlaceholder": "Role...", + "filterUser": "USER", + "filterAdmin": "ADMIN", + "filterLinksPlaceholder": "Links...", + "filterWithLinks": "With Links", + "filterWithoutLinks": "No Links", + "filterOwnerPlaceholder": "Owner...", + "filterWithOwner": "With Owner", + "filterWithoutOwner": "No Owner", + "noUsers": "No users", + "loadingUsers": "Loading users...", + "verified": "VERIFIED", + "notVerified": "NOT VERIFIED", + "user": "USER", + "admin": "ADMIN", + "headerID": "ID", + "headerEmail": "Email", + "headerRole": "Role", + "headerVerified": "Verified", + "headerTotalLinks": "Total Links", + "headerCreatedAt": "Created At", + "createUser": "Create User", + "createUserOptionRole": "Role:", + "sendVerificationEmail": "Send verification email", + "banned": "Banned", + "totalLinks": "Total Links:", + "totalDomains": "Total Domains:", + "totalUsers": "Total Users:", + "recentCreatedDomains": "Recent added domains", + "createDomain": "Create Domain", + "headerDomainAddress": "Address", + "headerHomepage": "Homepage", + "loadingDomains": "Loading domains...", + "createDomainOptionAddress": "Address:", + "createDomainOptionAddressPlaceholder": "yoursite.com", + "createDomainOptionHomepage": "Homepage (optional):", + "createDomainOptionHomepagePlaceholder": "Homepage URL...", + "createDomainOptionHomepageEmptyNotice": "If the homepage URL is left empty, yoursite.com will be redirected to {{domain}}", + "deleteDomainSuccessMsg": "Your domain \"{{domain}}\" has been deleted", + "deleteDomainDialogTitle": "Delete domain?", + "deleteDomainDialogMsg": "Are you sure you want to delete the domain \"{{domain}}\"?", + "deleteAllDomainLinksToo": "Delete all links too", + "banDomainDialogTitle": "Ban domain?", + "banDomainDialogMsg": "Are you sure you want to ban the domain \"{{address}}\"?", + "createDomainSucessMsg": "Domain \"{{domain}}\" created successfully", + "banDomainSucessMsg": "Domain \"{{domain}}\" has been banned", + "banUserSucessMsg": "User \"{{email}}\" has been banned", + "createUserSucessMsg": "User \"{{email}}\" created successfully", + "deleteUserSucessMsg": "User \"{{email}}\" has been deleted", + "deleteUserDialogTitle": "Delete user?", + "deleteUserDialogMsg": "Are you sure you want to delete the user \"{{email}}\"? All data, including the links they created, will be deleted", + "banUserDialogTitle": "Ban user?", + "banUserDialogMsg": "Are you sure you want to ban the user \"{{email}}\"?", + "noHomepage": "No homepage" + }, + "tooltip": { + "passwordProtected": "Password protected", + "banned": "Banned", + "viewLinksByDomain": "View links by this domain", + "viewLinksByUser": "View links by this user", + "viewAnonymousLinks": "View anonymous links", + "viewUser": "View user", + "viewLinks": "View links", + "viewDomains": "View domains", + "viewSystemDomains": "View sytem domains" + } + }, + "stats": { + "loadingStats": "Loading stats...", + "statsFor": "Stats for:", + "totalViews": "Total Views: ", + "yearlyViews": "Year", + "monthlyViews": "Month", + "weeklyViews": "Week", + "dailyViews": "Day", + "trackedVisitsThisDay": "tracked visits in the last day.", + "trackedVisitsThisWeek": "tracked visits in the last week.", + "trackedVisitsThisMonth": "tracked visits in the last month.", + "trackedVisitsThisYear": "tracked visits in the last year.", + "lastUpdatedAt": "Last updated at ", + "referrers": "Referrers.", + "browsers": "Browsers.", + "countries": "Countries.", + "operatingSystems": "Operating Systems." + }, + "settings": { + "customDomainDesc": "You can set a custom domain for your short URLs, so instead of {{default_domain}}/shorturl you can have yoursite.com/shorturl.", + "bannedDesc": "Link has been banned and removed because of malware or scam.", + "noticeMalwareLinks": "If you noticed a malware/scam link shortened by {{default_domain}},", + "noticeMalwareLinksSendReport": "send us a report", + "noticeMalwareLinksSendReportAria": "Send report", + "protectedLink": "Protected link.", + "enterPasswordToProceed": "Enter the password to be redirected to the link.", + "reportAbuse": "Report abuse.", + "reportAbuseDescIfEmailEnabled": "Report abuses, malware and phishing links to the email address below or use the form. We will review as soon as we can.", + "reportAbuseDescIfEmailDisabled": "Report abuses, malware and phishing links to the email address below. We will review as soon as we can.", + "resetPassword": "Reset password.", + "resetPasswordDesc": "If you forgot you password you can use the form below to get a reset password link.", + "setNewPassword": "Set your new password.", + "invalidPasswordToken": "Password token is invalid. Please try again.", + "customDomain": "Custom Domain", + "pointDomainARecord": "Point the A record of your domain to", + "ipAddress": "IP Address:", + "defaultIpAddress": "Our IP Address", + "pointDomainCnameRecord": "Or point the CNAME record of your subdomain to {{server_cname}}.", + "cloudflareDnsOnly": "If you are using Cloudflare, make sure to use DNS Only mode for your subdomain.", + "addDomainInstruction": "Then, add the domain through the form below.", + "generateKey": "Generate Key", + "regenerateKey": "Regenerate Key", + "readAPIDocs": "Read API Documentation", + "readAPIDocsAria": "API Documentation", + "APIDocsDesc": "Among other information on this site, you can use the API to create, delete, and shorten URLs. If you are unfamiliar with the API, do not generate a key.Do not share this key on the website client.", + "changePassword": "Change Password", + "changePasswordDesc": "Enter your current password and new password to change it to the new password", + "currentPassword": "Current Password:", + "currentPasswordPlaceholder": "Current password...", + "newPassword": "New Password:", + "newPasswordPlaceholder": "New password...", + "deleteAccount": "Delete Account", + "deleteAccountDesc": "Delete your account from {{domain}}", + "deleteAccountOptionPassword": "Password:", + "deleteAccountOptionPasswordPlaceholder": "Password...", + "changeEmail": "Change Email", + "changeEmailDesc": "Enter your password and a new email address to change your email address", + "changeEmailOptionPassword": "Password:", + "changeEmailOptionPasswordPlaceholder": "Password...", + "changeEmailOptionEmail": "New Email:", + "changeEmailOptionEmailPlaceholder": "john@example.com" + }, + "backbone": { + "404": "404 | Link could not be found.", + "backToHomepage": "Back to homepage", + "error": "Error!", + "errorMsg": "An error occurred.", + "errorAndTryAgainMsg": "An error occurred. Please try again later.", + "targetFor": "Target for {{link}}:", + "copy": "Copy", + "cancel": "Cancel", + "delete": "Delete", + "close": "Close", + "update": "Update", + "create": "Create" + }, + "terms": { + "title": "Terms of Service", + "paragraph_1": "By accessing the website at https://{{default_domain}}, you are agreeing to be bound by these terms of service, all applicable laws and regulations, and agree that you are responsible for compliance with any applicable local laws. If you do not agree with any of these terms, you are prohibited from using or accessing this site. The materials contained in this website are protected by applicable copyright and trademark law.", + "paragraph_2": "In no event shall {{site_name}} or its suppliers be liable for any damages (including, without limitation, damages for loss of data or profit, or due to business interruption) arising out of the use or inability to use the materials on {{default_domain}} website, even if {{site_name}} or a {{site_name}} authorized representative has been notified orally or in writing of the possibility of such damage. Because some jurisdictions do not allow limitations on implied warranties, or limitations of liability for consequential or incidental damages, these limitations may not apply to you.", + "paragraph_3": "The materials appearing on {{site_name}} website could include technical, typographical, or photographic errors. {{site_name}} does not warrant that any of the materials on its website are accurate, complete or current. {{site_name}} may make changes to the materials contained on its website at any time without notice. However, {{site_name}} does not make any commitment to update the materials.", + "paragraph_4": "{{site_name}} has not reviewed all of the sites linked to its website and is not responsible for the contents of any such linked site. The inclusion of any link does not imply endorsement by {{site_name}} of the site. Use of any such linked website is at the \"user's\" own risk.", + "paragraph_5": "{{site_name}} may revise these terms of service for its website at any time without notice. By using this website you are agreeing to be bound by the then current version of these terms of service." + }, + "timeAgo": { + "ago": " ago", + "second": "second", + "seconds": "seconds", + "minute": "minute", + "minutes": "minutes", + "hour": "hour", + "hours": "hours", + "day": "day", + "days": "days", + "week": "week", + "weeks": "weeks", + "month": "month", + "months": "months", + "year": "year", + "years": "years" + }, + "validator": { + "login": { + "invalidEmailErrorMsg": "Email is not valid.", + "invalidPasswordErrorMsg": "Password is not valid.", + "passwordLengthErrorMsg": "Password length must be between 8 and 64." + }, + "createLink": { + "targetMissingErrorMsg": "Target is missing.", + "invalidUrlErrorMsg": "URL is not valid.", + "onlyUsersCanUseThisFieldErrorMsg": "Only users can use this field.", + "maxUrlLengthErrorMsg": "Maximum URL length is 2040.", + "passwordLengthErrorMsg": "Password length must be between 3 and 64.", + "customUrlLengthErrorMsg": "Custom URL length must be between 1 and 64.", + "customUrlInvalidErrorMsg": "Custom URL is not valid.", + "preservedUrlErrorMsg": "You can't use this custom URL.", + "reuseMustBeBooleanErrorMsg": "Reuse must be boolean.", + "descriptionLengthErrorMsg": "Description length must be between 1 and 2040.", + "expireFormatInvalidErrorMsg": "Expire format is invalid. Valid examples: 1m, 8h, 42 days.", + "expireTimeErrorMsg": "Expire time should be more than 1 minute.", + "domainShouldBeStringErrorMsg": "Domain should be string.", + "cannotUseDomainErrorMsg": "You can't use this domain." + }, + "editLink": { + "maxUrlLengthErrorMsg": "Maximum URL length is 2040.", + "invalidUrlErrorMsg": "URL is not valid.", + "passwordLengthErrorMsg": "Password length must be between 3 and 64.", + "customUrlLengthErrorMsg": "Custom URL length must be between 1 and 64.", + "customUrlInvalidErrorMsg": "Custom URL is not valid", + "preservedUrlErrorMsg": "You can't use this custom URL.", + "expireFormatInvalidErrorMsg": "Expire format is invalid. Valid examples: 1m, 8h, 42 days.", + "expireTimeErrorMsg": "Expire time should be more than 1 minute.", + "descriptionLengthErrorMsg": "Description length must be between 0 and 2040." + }, + "deleteUser": { + "passwordIncorrectErrorMsg": "Password is not correct." + }, + "signup": { + "passwordInvalidErrorMsg": "Password is not valid.", + "emailInvalidErrorMsg": "Email is not valid.", + "emailLengthErrorMsg": "Email length must be max 255.", + "passwordLengthErrorMsg": "Password length must be between 8 and 64." + }, + "createUser": { + "passwordInvalidErrorMsg": "Password is not valid.", + "emailInvalidErrorMsg": "Email is not valid.", + "emailLengthErrorMsg": "Email length must be max 255.", + "emailExistsErrorMsg": "User already exists.", + "roleInvalidErrorMsg": "Role is not valid.", + "verifiedInvalidErrorMsg": "Verified should be a boolean.", + "bannedInvalidErrorMsg": "Banned should be a boolean.", + "verificationEmailInvalidErrorMsg": "Verification email should be a boolean." + }, + "changePassword": { + "passwordInvalidErrorMsg": "Password is not valid.", + "passwordLengthErrorMsg": "Password length must be between 8 and 64." + }, + "resetPassword": { + "emailInvalidErrorMsg": "Email is not valid.", + "emailLengthErrorMsg": "Email length must be max 255." + }, + "newPassword": { + "resetTokenInvalidErrorMsg": "Reset password token is invalid.", + "passwordInvalidErrorMsg": "Password is not valid.", + "passwordLengthErrorMsg": "Password length must be between 8 and 64.", + "passwordMismatchErrorMsg": "Passwords don't match." + }, + "banLink": { + "idInvalidErrorMsg": "ID is invalid.", + "hostInvalidErrorMsg": "\"host\" should be a boolean.", + "userInvalidErrorMsg": "\"user\" should be a boolean.", + "userLinksInvalidErrorMsg": "\"userLinks\" should be a boolean.", + "domainInvalidErrorMsg": "\"domain\" should be a boolean." + }, + "banDomain": { + "idInvalidErrorMsg": "ID is invalid.", + "linksInvalidErrorMsg": "\"links\" should be a boolean.", + "domainsInvalidErrorMsg": "\"domains\" should be a boolean." + }, + "banUser": { + "idInvalidErrorMsg": "ID is invalid.", + "linksInvalidErrorMsg": "\"links\" should be a boolean.", + "domainsInvalidErrorMsg": "\"domains\" should be a boolean." + }, + "addDomain": { + "domainInvalidErrorMsg": "Domain is not valid.", + "domainLengthErrorMsg": "Domain length must be between 3 and 64.", + "domainNotAllowedErrorMsg": "You can't add the default domain.", + "domainExistsErrorMsg": "You can't add this domain.", + "homepageInvalidErrorMsg": "Homepage is not valid." + }, + "addDomainAdmin": { + "domainInvalidErrorMsg": "Domain is not valid.", + "domainLengthErrorMsg": "Domain length must be between 3 and 64.", + "domainNotAllowedErrorMsg": "You can't add the default domain.", + "domainAlreadyExistsErrorMsg": "Domain already exists.", + "homepageInvalidErrorMsg": "Homepage is not valid." + } + } +} \ No newline at end of file diff --git a/server/locales/zh_CN.json b/server/locales/zh_CN.json new file mode 100644 index 000000000..fef42f775 --- /dev/null +++ b/server/locales/zh_CN.json @@ -0,0 +1,337 @@ +{ + "header": { + "homepageAria": "主页", + "reportAbuse": "举报", + "reportAbuseAria": "举报滥用", + "loginOrSignup": "登录 / 注册", + "loginOrSignupAria": "登录 / 注册", + "settings": "设置", + "settingsAria": "设置", + "logout": "退出", + "logoutAria": "退出", + "admin": "管理员", + "adminAria": "管理员", + "language": "语言", + "changeLanguage": "切换语言" + }, + "footer": { + "contactUs": "联系我们", + "poweredBy": "Powered By", + "theDevsAria": "开发者", + "termsOfService": "服务条款", + "termsOfServiceAria": "服务条款" + }, + "title": { + "404": "404 - 找不到页面", + "loginOrSignup": "登录或注册", + "homepage": "免费的现代短链服务", + "createAdmin": "创建管理员账户", + "logout": "注销", + "settings": "设置", + "admin": "管理员", + "stats": "统计", + "bannedLink": "禁止的链接", + "reportAbuse": "举报滥用", + "resetPassword": "重置密码", + "verifyChangeEmail": "验证邮箱更改", + "verify": "验证", + "termsOfService": "服务条款", + "confirmLinkDeleteMessage": "找不到该链接。", + "confirmLinkBanMessage": "找不到该链接。", + "confirmUserDeleteMessage": "找不到该用户。", + "confirmUserBanMessage": "找不到该用户。", + "createUserDialog": "创建用户", + "addDomainDialog": "添加域名管理员", + "addDomainForm": "添加域名表单", + "confirmDomainDeleteMessage": "找不到该域名。", + "confirmDomainBanMessage": "找不到该域名。", + "confirmDomainDeleteAdminMessage": "找不到该域名。", + "reportEmailMessage": "报告电子邮件地址不可用。", + "supportEmailMessage": "支持电子邮件地址不可用。" + }, + "login": { + "createAdminAccountMsg": "请先创建管理员账户:", + "emailAddress": "电子邮箱地址:", + "emailAddressPlaceholder": "输入电子邮箱...", + "password": "密码:", + "passwordPlaceholder": "输入密码...", + "createAdminAccount": "创建管理员账户", + "verifyEmailHasBeenSent": "验证邮件已发送至你的邮箱", + "login": "登录", + "signup": "注册", + "forgotPassword": "忘记密码?", + "forgotPasswordAria": "重置密码" + }, + "welcome": { + "welcomeAndRedirect": "欢迎,正在跳转至主页...", + "welcomeUser": "欢迎,{{name}}", + "loggingoutAndRedirect": "已退出登录,正在跳转至主页...", + "emailVerifiedAndRedirect": "邮箱已验证,正在跳转至主页...", + "emailNotVerifiedAndTryAgain": "无法验证电子邮件地址,请重试", + "verifiedAndRedirect": "你的账户已验证,正在跳转至主页...", + "notVerifiedAndTryAgain": "无效的验证,请重试" + }, + "home": { + "slogan": "让你的链接", + "sloganShorter": "更简洁", + "pasteLongUrlPlaceholder": "粘贴长链接", + "showAdvancedOptions": "显示更多选项", + "optionDomain": "域名:", + "customAddressPlaceholder": "自定义地址...", + "optionPassword": "密码:", + "passwordPlaceholder": "密码...", + "optionExpireIn": "过期时间:", + "expireInPlaceholder": "2分钟/小时/天", + "optionDescription": "描述:", + "descriptionPlaceholder": "描述...", + "unlockAndGo": "解锁并跳转" + }, + "table": { + "recentShortenedLinks": "最近的短链接", + "searchPlaceholder": "搜索...", + "headerOriginalUrl": "原始链接", + "headerCreatedAt": "创建时间", + "headerShortLink": "短链接", + "headerViews": "访问量", + "noLinks": "暂无链接", + "loadingLinks": "正在加载链接...", + "expireIn": "{{time}}后过期", + "actions": { + "deleteDialogTitle": "删除链接?", + "deleteDialogMsg": "确定要删除链接 \"{{link}}\" 吗?", + "deleteSuccessMsg": "你的链接 \"{{link}}\" 已被删除", + "banDialogTitle": "禁用链接?", + "banDialogMsg": "确定要禁用链接 \"{{link}}\" 吗?", + "banSuccessMsg": "链接 \"{{link}}\" 已被禁用", + "ban": "禁用", + "banUser": "用户", + "banUserLinks": "用户链接", + "banHost": "主机", + "banDomain": "域名", + "editTarget": "目标:", + "editTargetPlaceholder": "目标链接...", + "editCustomUrlPlaceholder": "自定义链接...", + "editPassword": "密码:", + "editPasswordPlaceholder": "密码...", + "editDescription": "描述:", + "editDescriptionPlaceholder": "描述...", + "editExpireIn": "过期时间:", + "editExpireInPlaceholder": "2分钟/小时/天", + "noLinkFound": "未找到任何链接" + }, + "options": { + "userLinks": "用户链接", + "userDomains": "用户域名", + "owner": "所有者", + "links": "链接", + "user": "用户", + "host": "主机", + "domain": "域名" + }, + "admin": { + "searchLinkPlaceholder": "搜索链接...", + "clearLinkSearchAria": "清除链接搜索", + "searchDomainPlaceholder": "搜索域名...", + "clearDomainSearchAria": "清除域名搜索", + "searchUserPlaceholder": "搜索用户...", + "clearUserSearchAria": "清除用户搜索", + "filterBannedPlaceholder": "是否禁用", + "filterBanned": "已禁用", + "filterNotBanned": "未禁用", + "filterAnonymousPlaceholder": "是否匿名", + "filterAnonymous": "匿名", + "filterNotAnonymous": "已注册用户", + "filterDomainPlaceholder": "是否有域名", + "filterWithDomain": "有域名", + "filterWithoutDomain": "无域名", + "viewLinks": "链接", + "viewDomains": "域名", + "viewUsers": "用户", + "noDomains": "暂无域名", + "viewLinksByUser": "查看链接", + "anonymous": "匿名", + "recentCreatedUsers": "最近创建的用户", + "filterVerificationPlaceholder": "是否已验证", + "filterVerified": "已验证", + "filterNotVerified": "未验证", + "filterRolePlaceholder": "选择角色", + "filterUser": "普通用户", + "filterAdmin": "管理员", + "filterLinksPlaceholder": "是否有链接", + "filterWithLinks": "有链接", + "filterWithoutLinks": "无链接", + "filterOwnerPlaceholder": "选择所有者", + "filterWithOwner": "用户", + "filterWithoutOwner": "系统", + "noUsers": "暂无用户", + "loadingUsers": "正在加载用户...", + "verified": "已验证", + "notVerified": "未验证", + "user": "普通用户", + "admin": "管理员", + "headerID": "ID", + "headerEmail": "邮箱", + "headerRole": "角色", + "headerVerified": "验证", + "headerTotalLinks": "链接数", + "headerCreatedAt": "创建时间", + "createUser": "创建用户", + "createUserOptionRole": "角色:", + "sendVerificationEmail": "发送验证邮件", + "banned": "禁用", + "totalLinks": "链接数:", + "totalDomains": "域名数:", + "totalUsers": "用户数:", + "recentCreatedDomains": "最近创建的域名", + "createDomain": "创建域名", + "headerDomainAddress": "域名地址", + "headerHomepage": "主页", + "noHomepage": "暂无主页", + "loadingDomains": "正在加载域名...", + "createDomainOptionAddress": "地址:", + "createDomainOptionAddressPlaceholder": "yoursite.com", + "createDomainOptionHomepage": "主页(可选):", + "createDomainOptionHomepagePlaceholder": "主页地址...", + "createDomainOptionHomepageEmptyNotice": "如果留空主页地址,则yoursite.com将被重定向到{{domain}}", + "deleteDomainSuccessMsg": "你的域名 \"{{domain}}\" 已被删除", + "deleteDomainDialogTitle": "删除域名?", + "deleteDomainDialogMsg": "确定要删除域名 \"{{domain}}\" 吗?", + "deleteAllDomainLinksToo": "删除域名下的所有链接", + "banDomainDialogTitle": "禁用域名?", + "banDomainDialogMsg": "确定要禁用域名 \"{{address}}\" 吗?", + "createDomainSucessMsg": "域名 \"{{domain}}\" 创建成功", + "banDomainSucessMsg": "域名 \"{{domain}}\" 已被禁用", + "banUserSucessMsg": "用户 \"{{email}}\" 已被禁用", + "createUserSucessMsg": "用户 \"{{email}}\" 创建成功", + "deleteUserSucessMsg": "用户 \"{{email}}\" 已被删除", + "deleteUserDialogTitle": "删除用户?", + "deleteUserDialogMsg": "确定要删除用户 \"{{email}}\" 吗?将删除所有数据,包括他创建的链接", + "banUserDialogTitle": "禁用用户?", + "banUserDialogMsg": "确定要禁用用户 \"{{email}}\" 吗?" + }, + "tooltip": { + "passwordProtected": "密码保护", + "banned": "已禁用", + "viewLinksByDomain": "查看此域名下的链接", + "viewLinksByUser": "查看此用户的链接", + "viewAnonymousLinks": "查看匿名链接", + "viewUser": "查看用户", + "viewLinks": "查看链接", + "viewDomains": "查看域名", + "viewSystemDomains": "查看系统域名" + } + }, + "stats": { + "loadingStats": "正在加载统计信息", + "statsFor": "统计数据:", + "totalViews": "总访问量:", + "yearlyViews": "年", + "monthlyViews": "月", + "weeklyViews": "周", + "dailyViews": "天", + "trackedVisitsThisDay": "过去一天的访问量", + "trackedVisitsThisWeek": "过去一周的访问量", + "trackedVisitsThisMonth": "过去一个月的访问量", + "trackedVisitsThisYear": "过去一年的访问量", + "lastUpdatedAt": "最后更新时间 ", + "referrers": "引荐来源", + "browsers": "浏览器", + "countries": "国家", + "operatingSystems": "操作系统" + }, + "account": { + "showEmailAddress": "显示邮箱", + "urlContainingMalwareOrScam": "此链接可能包含恶意软件或欺诈:", + "sendReport": "发送举报", + "optionNewPassword": "新密码:", + "optionNewPasswordPlaceholder": "新密码...", + "optionRepeatPassword": "重复密码:", + "optionRepeatPasswordPlaceholder": "重复密码...", + "setPassword": "设置密码", + "setPasswordSuccess": "你的密码已成功更新。你现在可以使用新密码登录。", + "resetPassword": "重置密码" + }, + "settings": { + "generateKey": "生成密钥", + "regenerateKey": "重新生成密钥", + "readAPIDocs": "阅读API文档", + "readAPIDocsAria": "API文档", + "APIDocsDesc": "在本网站的其他其他信息中,你可以使用API创建,删除和缩短URL。如果你不熟悉API,请不要生成密钥。请勿在网站客户端分享此密钥。", + "changePassword": "更改密码", + "changePasswordDesc": "输入你当前的密码和新密码以将其更改为新密码", + "currentPassword": "当前密码:", + "currentPasswordPlaceholder": "当前密码...", + "newPassword": "新密码:", + "newPasswordPlaceholder": "新密码...", + "deleteAccount": "删除账户", + "deleteAccountDesc": "从 {{domain}} 删除你的账户", + "deleteAccountOptionPassword": "密码:", + "deleteAccountOptionPasswordPlaceholder": "密码...", + "changeEmail": "更改邮箱", + "changeEmailDesc": "输入你的密码和一个新的电子邮件地址,以更改你的电子邮件地址", + "changeEmailOptionPassword": "密码:", + "changeEmailOptionPasswordPlaceholder": "密码...", + "changeEmailOptionEmail": "新邮箱:", + "changeEmailOptionEmailPlaceholder": "john@example.com", + "customDomain": "自定义域名", + "pointDomainARecord": "将你的域名的 A 记录指向", + "ipAddress": "IP 地址:,", + "defaultIpAddress": "我们的 IP 地址,", + "pointDomainCnameRecord": "或将你的子域名的 CNAME 记录指向 {{server_cname}}。", + "cloudflareDnsOnly": "如果你使用的是 Cloudflare,请确保为你的子域名使用 仅 DNS 模式。", + "addDomainInstruction": "然后,通过下面的表单添加域名。", + "customDomainDesc": "你可以为你的短链接设置自定义域名,因此,你可以将 {{default_domain}}/shorturl 替换为 yoursite.com/shorturl", + "bannedDesc": "链接已被禁止和删除,因为恶意软件或欺诈", + "noticeMalwareLinks": "如果您注意到{{default_domain}}为恶意软件/欺诈链接创建了短链接,", + "noticeMalwareLinksSendReport": "向我们发送报告", + "noticeMalwareLinksSendReportAria": "发送报告", + "protectedLink": "受密码保护的链接", + "enterPasswordToProceed": "请输入要重定向到链接的密码", + "reportAbuse": "举报滥用", + "reportAbuseDescIfEmailEnabled": "向下面的电子邮件地址报告滥用,恶意软件和网络钓鱼链接(或使用该表单)。我们将尽快审查。", + "reportAbuseDescIfEmailDisabled": "向下面的电子邮件地址报告滥用,恶意软件和网络钓鱼链接。我们将尽快审查。", + "resetPassword": "重置密码", + "setNewPassword": "设置新密码", + "invalidPasswordToken": "密码令牌无效,请重试", + "resetPasswordDesc": "如果你忘记了密码,可以使用以下表单获取重置密码链接" + }, + "backbone": { + "404": "404 | 链接未找到", + "backToHomepage": "返回主页", + "error": "错误!", + "errorMsg": "发生错误", + "errorAndTryAgainMsg": "发生错误,请稍后重试", + "targetFor": "目标链接{{link}}:", + "copy": "复制", + "cancel": "取消", + "delete": "删除", + "close": "关闭", + "update": "更新", + "create": "创建" + }, + "terms": { + "title": "服务条款", + "paragraph_1": "通过访问网站 https://{{default_domain}},您同意遵守这些服务条款、所有适用的法律和法规,并同意负责遵守任何适用的地方性法律。如果您不同意这些条款,您将被禁止使用或访问本网站。本网站所包含的材料受适用的版权和商标法保护。", + "paragraph_2": "在任何情况下,{{site_name}}或其供应商对因使用或无法使用{{default_domain}}网站上的材料而导致的任何损害(包括但不限于数据丢失或利润损失,或因业务中断)不承担责任,即使{{site_name}}或{{site_name}}的授权代表已经口头或书面通知了可能发生此类损害的风险。由于某些司法管辖区不允许限制隐含保证,或不允许限制因后果性或附带损害而产生的责任,这些限制可能不适用于您。", + "paragraph_3": "出现在{{site_name}}网站上的材料可能包含技术性、排版性或摄影性的错误。{{site_name}}不保证其网站上的任何材料是准确、完整或最新的。{{site_name}}可能随时在不通知的情况下对其网站上的材料进行更改。然而,{{site_name}}不承诺更新这些材料。", + "paragraph_4": "{{site_name}}未审查其网站链接的所有网站,且对任何此类链接网站的内容不承担责任。任何链接的包含并不意味着{{site_name}}对该网站的认可。使用任何此类链接的网站风险自负。", + "paragraph_5": "{{site_name}}可能会在没有通知的情况下修订其网站的服务条款。使用本网站即表示您同意受当前版本的服务条款的约束。" + }, + "timeAgo": { + "ago": "前", + "second": "秒", + "seconds": "秒", + "minute": "分钟", + "minutes": "分钟", + "hour": "小时", + "hours": "小时", + "day": "天", + "days": "天", + "week": "周", + "weeks": "周", + "month": "月", + "months": "月", + "year": "年", + "years": "年" + } +} \ No newline at end of file diff --git a/server/locales/zh_TW.json b/server/locales/zh_TW.json new file mode 100644 index 000000000..04731b518 --- /dev/null +++ b/server/locales/zh_TW.json @@ -0,0 +1,337 @@ +{ + "header": { + "homepageAria": "主頁", + "reportAbuse": "舉報", + "reportAbuseAria": "舉報濫用", + "loginOrSignup": "登入 / 註冊", + "loginOrSignupAria": "登入 / 註冊", + "settings": "設定", + "settingsAria": "設定", + "logout": "登出", + "logoutAria": "登出", + "admin": "管理員", + "adminAria": "管理員", + "language": "語言", + "changeLanguage": "切換語言" + }, + "footer": { + "contactUs": "聯絡我們", + "poweredBy": "Powered by", + "theDevsAria": "開發者", + "termsOfService": "服務條款", + "termsOfServiceAria": "服務條款" + }, + "title": { + "404": "404 - 找不到頁面", + "loginOrSignup": "登入或註冊", + "homepage": "免費的現代短鏈服務", + "createAdmin": "創建管理員帳戶", + "logout": "登出", + "settings": "設定", + "admin": "管理員", + "stats": "統計", + "bannedLink": "禁止的鏈接", + "reportAbuse": "舉報濫用", + "resetPassword": "重置密碼", + "verifyChangeEmail": "驗證郵箱更改", + "verify": "驗證", + "termsOfService": "服務條款", + "confirmLinkDeleteMessage": "找不到該鏈接。", + "confirmLinkBanMessage": "找不到該鏈接。", + "confirmUserDeleteMessage": "找不到該用戶。", + "confirmUserBanMessage": "找不到該用戶。", + "createUserDialog": "創建用戶", + "addDomainDialog": "添加域名管理員", + "addDomainForm": "添加域名表單", + "confirmDomainDeleteMessage": "找不到該域名。", + "confirmDomainBanMessage": "找不到該域名。", + "confirmDomainDeleteAdminMessage": "找不到該域名。", + "reportEmailMessage": "報告電子郵件地址不可用。", + "supportEmailMessage": "支持電子郵件地址不可用。" + }, + "login": { + "createAdminAccountMsg": "請先創建管理員帳戶:", + "emailAddress": "電子郵件地址:", + "emailAddressPlaceholder": "輸入電子郵件...", + "password": "密碼:", + "passwordPlaceholder": "輸入密碼...", + "createAdminAccount": "創建管理員帳戶", + "verifyEmailHasBeenSent": "驗證郵件已發送至你的郵箱", + "login": "登入", + "signup": "註冊", + "forgotPassword": "忘記密碼?", + "forgotPasswordAria": "重置密碼" + }, + "welcome": { + "welcomeAndRedirect": "歡迎,正在跳轉至主頁...", + "welcomeUser": "歡迎,{{name}}", + "loggingoutAndRedirect": "已登出,正在跳轉至主頁...", + "emailVerifiedAndRedirect": "郵箱已驗證,正在跳轉至主頁...", + "emailNotVerifiedAndTryAgain": "無法驗證電子郵件地址,請重試", + "verifiedAndRedirect": "你的帳戶已驗證,正在跳轉至主頁...", + "notVerifiedAndTryAgain": "無效的驗證,請重試" + }, + "home": { + "slogan": "讓你的鏈接", + "sloganShorter": "更簡潔", + "pasteLongUrlPlaceholder": "粘貼長鏈接", + "showAdvancedOptions": "顯示更多選項", + "optionDomain": "域名:", + "customAddressPlaceholder": "自定義地址...", + "optionPassword": "密碼:", + "passwordPlaceholder": "密碼...", + "optionExpireIn": "過期時間:", + "expireInPlaceholder": "2分鐘/小時/天", + "optionDescription": "描述:", + "descriptionPlaceholder": "描述...", + "unlockAndGo": "解鎖並跳轉" + }, + "table": { + "recentShortenedLinks": "最近的短鏈接", + "searchPlaceholder": "搜索...", + "headerOriginalUrl": "原始鏈接", + "headerCreatedAt": "創建時間", + "headerShortLink": "短鏈接", + "headerViews": "訪問量", + "noLinks": "暫無鏈接", + "loadingLinks": "正在加載鏈接...", + "expireIn": "{{time}}後過期", + "actions": { + "deleteDialogTitle": "刪除鏈接?", + "deleteDialogMsg": "確定要刪除鏈接 \"{{link}}\" 嗎?", + "deleteSuccessMsg": "你的鏈接 \"{{link}}\" 已被刪除", + "banDialogTitle": "禁用鏈接?", + "banDialogMsg": "確定要禁用鏈接 \"{{link}}\" 嗎?", + "banSuccessMsg": "鏈接 \"{{link}}\" 已被禁用", + "ban": "禁用", + "banUser": "用戶", + "banUserLinks": "用戶鏈接", + "banHost": "主機", + "banDomain": "域名", + "editTarget": "目標:", + "editTargetPlaceholder": "目標鏈接...", + "editCustomUrlPlaceholder": "自定義鏈接...", + "editPassword": "密碼:", + "editPasswordPlaceholder": "密碼...", + "editDescription": "描述:", + "editDescriptionPlaceholder": "描述...", + "editExpireIn": "過期時間:", + "editExpireInPlaceholder": "2分鐘/小時/天", + "noLinkFound": "未找到任何鏈接" + }, + "options": { + "userLinks": "用戶鏈接", + "userDomains": "用戶域名", + "owner": "所有者", + "links": "鏈接", + "user": "用戶", + "host": "主機", + "domain": "域名" + }, + "admin": { + "searchLinkPlaceholder": "搜索鏈接...", + "clearLinkSearchAria": "清除鏈接搜索", + "searchDomainPlaceholder": "搜索域名...", + "clearDomainSearchAria": "清除域名搜索", + "searchUserPlaceholder": "搜索用戶...", + "clearUserSearchAria": "清除用戶搜索", + "filterBannedPlaceholder": "是否禁用", + "filterBanned": "已禁用", + "filterNotBanned": "未禁用", + "filterAnonymousPlaceholder": "是否匿名", + "filterAnonymous": "匿名", + "filterNotAnonymous": "已註冊用戶", + "filterDomainPlaceholder": "是否有域名", + "filterWithDomain": "有域名", + "filterWithoutDomain": "無域名", + "viewLinks": "鏈接", + "viewDomains": "域名", + "viewUsers": "用戶", + "noDomains": "暫無域名", + "viewLinksByUser": "查看鏈接", + "anonymous": "匿名", + "recentCreatedUsers": "最近創建的用戶", + "filterVerificationPlaceholder": "是否已驗證", + "filterVerified": "已驗證", + "filterNotVerified": "未驗證", + "filterRolePlaceholder": "選擇角色", + "filterUser": "普通用戶", + "filterAdmin": "管理員", + "filterLinksPlaceholder": "是否有鏈接", + "filterWithLinks": "有鏈接", + "filterWithoutLinks": "無鏈接", + "filterOwnerPlaceholder": "選擇所有者", + "filterWithOwner": "用戶", + "filterWithoutOwner": "系統", + "noUsers": "暫無用戶", + "loadingUsers": "正在加載用戶...", + "verified": "已驗證", + "notVerified": "未驗證", + "user": "普通用戶", + "admin": "管理員", + "headerID": "ID", + "headerEmail": "郵箱", + "headerRole": "角色", + "headerVerified": "驗證", + "headerTotalLinks": "鏈接數", + "headerCreatedAt": "創建時間", + "createUser": "創建用戶", + "createUserOptionRole": "角色:", + "sendVerificationEmail": "發送驗證郵件", + "banned": "禁用", + "totalLinks": "鏈接數:", + "totalDomains": "域名數:", + "totalUsers": "用戶數:", + "recentCreatedDomains": "最近創建的域名", + "createDomain": "創建域名", + "headerDomainAddress": "域名地址", + "headerHomepage": "首頁", + "noHomepage": "暫無首頁", + "loadingDomains": "正在加載域名...", + "createDomainOptionAddress": "地址:", + "createDomainOptionAddressPlaceholder": "yoursite.com", + "createDomainOptionHomepage": "首頁(可選):", + "createDomainOptionHomepagePlaceholder": "首頁地址...", + "createDomainOptionHomepageEmptyNotice": "如果留空首頁地址,則yoursite.com將被重定向到{{domain}}", + "deleteDomainSuccessMsg": "你的域名 \"{{domain}}\" 已被刪除", + "deleteDomainDialogTitle": "刪除域名?", + "deleteDomainDialogMsg": "確定要刪除域名 \"{{domain}}\" 嗎?", + "deleteAllDomainLinksToo": "刪除域名下的所有鏈接", + "banDomainDialogTitle": "禁用域名?", + "banDomainDialogMsg": "確定要禁用域名 \"{{address}}\" 嗎?", + "createDomainSucessMsg": "域名 \"{{domain}}\" 創建成功", + "banDomainSucessMsg": "域名 \"{{domain}}\" 已被禁用", + "banUserSucessMsg": "用戶 \"{{email}}\" 已被禁用", + "createUserSucessMsg": "用戶 \"{{email}}\" 創建成功", + "deleteUserSucessMsg": "用戶 \"{{email}}\" 已被刪除", + "deleteUserDialogTitle": "刪除用戶?", + "deleteUserDialogMsg": "確定要刪除用戶 \"{{email}}\" 嗎?將刪除所有數據,包括他創建的鏈接", + "banUserDialogTitle": "禁用用戶?", + "banUserDialogMsg": "確定要禁用用戶 \"{{email}}\" 嗎?" + }, + "tooltip": { + "passwordProtected": "密碼保護", + "banned": "已禁用", + "viewLinksByDomain": "查看此域名下的鏈接", + "viewLinksByUser": "查看此用戶的鏈接", + "viewAnonymousLinks": "查看匿名鏈接", + "viewUser": "查看用戶", + "viewLinks": "查看鏈接", + "viewDomains": "查看域名", + "viewSystemDomains": "查看系統域名" + } + }, + "stats": { + "loadingStats": "正在加載統計信息", + "statsFor": "統計數據:", + "totalViews": "總訪問量:", + "yearlyViews": "年", + "monthlyViews": "月", + "weeklyViews": "周", + "dailyViews": "天", + "trackedVisitsThisDay": "過去一天的訪問量", + "trackedVisitsThisWeek": "過去一周的訪問量", + "trackedVisitsThisMonth": "過去一個月的訪問量", + "trackedVisitsThisYear": "過去一年的訪問量", + "lastUpdatedAt": "最後更新時間 ", + "referrers": "引薦來源", + "browsers": "瀏覽器", + "countries": "國家", + "operatingSystems": "操作系統" + }, + "account": { + "showEmailAddress": "顯示郵箱", + "urlContainingMalwareOrScam": "此鏈接可能包含惡意軟件或詐騙:", + "sendReport": "發送舉報", + "optionNewPassword": "新密碼:", + "optionNewPasswordPlaceholder": "新密碼...", + "optionRepeatPassword": "重複密碼:", + "optionRepeatPasswordPlaceholder": "重複密碼...", + "setPassword": "設置密碼", + "setPasswordSuccess": "你的密碼已成功更新。你現在可以使用新密碼登錄。", + "resetPassword": "重置密碼" + }, + "settings": { + "generateKey": "生成密鑰", + "regenerateKey": "重新生成密鑰", + "readAPIDocs": "閱讀API文檔", + "readAPIDocsAria": "API文檔", + "APIDocsDesc": "在本網站的其他其他信息中,你可以使用API創建,刪除和縮短URL。如果你不熟悉API,請不要生成密鑰。請勿在網站客戶端分享此密鑰。", + "changePassword": "更改密碼", + "changePasswordDesc": "輸入你當前的密碼和新密碼以將其更改為新密碼", + "currentPassword": "當前密碼:", + "currentPasswordPlaceholder": "當前密碼...", + "newPassword": "新密碼:", + "newPasswordPlaceholder": "新密碼...", + "deleteAccount": "刪除賬號", + "deleteAccountDesc": "從 {{domain}} 刪除你的賬號", + "deleteAccountOptionPassword": "密碼:", + "deleteAccountOptionPasswordPlaceholder": "密碼...", + "changeEmail": "更改郵箱", + "changeEmailDesc": "輸入你的密碼和一個新的電子郵件地址,以更改你的電子郵件地址", + "changeEmailOptionPassword": "密碼:", + "changeEmailOptionPasswordPlaceholder": "密碼...", + "changeEmailOptionEmail": "新郵箱:", + "changeEmailOptionEmailPlaceholder": "john@example.com", + "customDomain": "自定義域名", + "pointDomainARecord": "將你的域名的 A 記錄指向", + "ipAddress": "IP 地址:,", + "defaultIpAddress": "我們的 IP 地址,", + "pointDomainCnameRecord": "或將你的子域名的 CNAME 記錄指向 {{server_cname}}。", + "cloudflareDnsOnly": "如果你使用的是 Cloudflare,請確保為你的子域名使用 僅 DNS 模式。", + "addDomainInstruction": "然後,通過下面的表單添加域名。", + "customDomainDesc": "你可以為你的短鏈接設置自定義域名,因此,你可以將 {{default_domain}}/shorturl 替換為 yoursite.com/shorturl", + "bannedDesc": "鏈接已被禁止和刪除,因為惡意軟件或詐騙", + "noticeMalwareLinks": "如果您注意到{{default_domain}}為惡意軟件/詐騙鏈接創建了短鏈接,", + "noticeMalwareLinksSendReport": "向我們發送舉報", + "noticeMalwareLinksSendReportAria": "發送舉報", + "protectedLink": "受密碼保護的鏈接", + "enterPasswordToProceed": "請輸入要重定向到鏈接的密碼", + "reportAbuse": "舉報濫用", + "reportAbuseDescIfEmailEnabled": "向下面的電子郵件地址舉報濫用,惡意軟件和網絡釣魚鏈接(或使用該表單)。我們將儘快審查。", + "reportAbuseDescIfEmailDisabled": "向下面的電子郵件地址舉報濫用,惡意軟件和網絡釣魚鏈接。我們將儘快審查。", + "resetPassword": "重置密碼", + "setNewPassword": "設置新密碼", + "invalidPasswordToken": "密碼令牌無效,請重試", + "resetPasswordDesc": "如果你忘記了密碼,可以使用以下表單獲取重置密碼鏈接" + }, + "backbone": { + "404": "404 | 鏈接未找到", + "backToHomepage": "返回首頁", + "error": "錯誤!", + "errorMsg": "發生錯誤", + "errorAndTryAgainMsg": "發生錯誤,請稍後重試", + "targetFor": "目標鏈接{{link}}:", + "copy": "複製", + "cancel": "取消", + "delete": "刪除", + "close": "關閉", + "update": "更新", + "create": "創建" + }, + "terms": { + "title": "服務條款", + "paragraph_1": "通過訪問網站 https://{{default_domain}},您同意遵守這些服務條款、所有適用的法律和法規,並同意負責遵守任何適用的地方性法律。如果您不同意這些條款,您將被禁止使用或訪問本網站。本網站所包含的材料受適用的版權和商標法保護。", + "paragraph_2": "在任何情況下,{{site_name}}或其供應商對因使用或無法使用{{default_domain}}網站上的材料而導致的任何損害(包括但不限於數據丟失或利潤損失,或因業務中斷)不承擔責任,即使{{site_name}}或{{site_name}}的授權代表已經口頭或書面通知了可能發生此類損害的風險。由於某些司法管轄區不允許限制隱含保證,或不允許限制因後果性或附帶損害而產生的責任,這些限制可能不適用於您。", + "paragraph_3": "出現在{{site_name}}網站上的材料可能包含技術性、排版性或攝影性的錯誤。{{site_name}}不保證其網站上的任何材料是準確、完整或最新的。{{site_name}}可能隨時在不通知的情況下對其網站上的材料進行更改。然而,{{site_name}}不承諾更新這些材料。", + "paragraph_4": "{{site_name}}未審查其網站鏈接的所有網站,且對任何此類鏈接網站的內容不承擔責任。任何鏈接的包含並不意味著{{site_name}}對該網站的認可。使用任何此類鏈接的網站風險自負。", + "paragraph_5": "{{site_name}}可能會在沒有通知的情況下修訂其網站的服務條款。使用本網站即表示您同意受當前版本的服務條款的約束。" + }, + "timeAgo": { + "ago": "前", + "second": "秒", + "seconds": "秒", + "minute": "分鐘", + "minutes": "分鐘", + "hour": "小時", + "hours": "小時", + "day": "天", + "days": "天", + "week": "周", + "weeks": "周", + "month": "月", + "months": "月", + "year": "年", + "years": "年" + } +} \ No newline at end of file diff --git a/server/server.js b/server/server.js index 606ed0e11..2c5659e3f 100644 --- a/server/server.js +++ b/server/server.js @@ -3,8 +3,8 @@ const env = require("./env"); const cookieParser = require("cookie-parser"); const passport = require("passport"); const express = require("express"); -const helmet = require("helmet"); const path = require("node:path"); +const helmet = require("helmet"); const hbs = require("hbs"); const helpers = require("./handlers/helpers.handler"); @@ -14,7 +14,7 @@ const locals = require("./handlers/locals.handler"); const links = require("./handlers/links.handler"); const routes = require("./routes"); const utils = require("./utils"); - +const i18n = require("./i18n"); // run the cron jobs // the app might be running in cluster mode (multiple instances) so run the cron job only on one cluster (the first one) @@ -50,7 +50,6 @@ app.use(locals.isHTML); app.use(locals.config); // template engine / serve html - app.set("view engine", "hbs"); app.set("views", [ path.join(__dirname, "../custom/views"), @@ -61,6 +60,9 @@ utils.registerHandlebarsHelpers(); // if is custom domain, redirect to the set homepage app.use(asyncHandler(links.redirectCustomDomainHomepage)); +// handle selecting locale +app.use(i18n); + // render html pages app.use("/", routes.render); @@ -76,7 +78,7 @@ app.get("*", renders.notFound); // handle errors coming from above routes app.use(helpers.error); - + app.listen(env.PORT, () => { console.log(`> Ready on http://localhost:${env.PORT}`); }); diff --git a/server/utils/utils.js b/server/utils/utils.js index 7981e0d24..3686f7743 100644 --- a/server/utils/utils.js +++ b/server/utils/utils.js @@ -10,6 +10,7 @@ const { ROLES } = require("../consts"); const knexUtils = require("./knex"); const knex = require("../knex"); const env = require("../env"); +const i18n = require("i18n"); const nanoid = customAlphabet(env.LINK_CUSTOM_ALPHABET, env.LINK_LENGTH); @@ -227,7 +228,7 @@ function getTimeAgo(dateString) { const secondsAgo = Math.round((Date.now() - Number(date)) / 1000); if (secondsAgo < MINUTE) { - return `${secondsAgo} second${secondsAgo !== 1 ? "s" : ""} ago`; + return `${secondsAgo} ${i18n.__('timeAgo.' + (secondsAgo !== 1 ? 'seconds' : 'second'))} ${i18n.__('timeAgo.ago')}`; } let divisor; @@ -248,7 +249,7 @@ function getTimeAgo(dateString) { } const count = Math.floor(secondsAgo / divisor); - return `${count} ${unit}${count > 1 ? "s" : ""} ago`; + return `${count} ${i18n.__('timeAgo.' + (count > 1 ? unit + 's' : unit))}${i18n.__('timeAgo.ago')}`; } @@ -359,6 +360,11 @@ function registerHandlebarsHelpers() { block.push(context.fn(this)); }); + hbs.registerHelper('t', function(str, options) { + return (i18n != undefined ? i18n.__(str, options.hash) : str); + }); + + hbs.registerHelper("block", function(name) { const val = (blocks[name] || []).join("\n"); blocks[name] = []; diff --git a/server/views/404.hbs b/server/views/404.hbs index 43ac87190..fb3a438ed 100644 --- a/server/views/404.hbs +++ b/server/views/404.hbs @@ -1,10 +1,10 @@ {{> header}}
{{message}}
- ← Back to homepage + ← {{t 'backbone.backToHomepage'}}- The domain "{{address}}" has been created successfully. + {{{t "table.admin.createDomainSucessMsg" domain=address}}}
diff --git a/server/views/partials/admin/dialog/ban_domain.hbs b/server/views/partials/admin/dialog/ban_domain.hbs index 6509dc16d..0b7574843 100644 --- a/server/views/partials/admin/dialog/ban_domain.hbs +++ b/server/views/partials/admin/dialog/ban_domain.hbs @@ -1,24 +1,24 @@- Are you sure do you want to ban the domain "{{address}}"? + {{{t "table.admin.banDomainDialogMsg" address=address}}}
- The domain "{{address}}" is banned. + {{{t "table.admin.banDomainSucessMsg" domain=address}}}
diff --git a/server/views/partials/admin/dialog/ban_user.hbs b/server/views/partials/admin/dialog/ban_user.hbs index f75b16744..87b09153d 100644 --- a/server/views/partials/admin/dialog/ban_user.hbs +++ b/server/views/partials/admin/dialog/ban_user.hbs @@ -1,20 +1,20 @@- Are you sure do you want to ban the user "{{email}}"? + {{{t "table.admin.banUserDialogMsg" email=email}}}
- The user "{{email}}" is banned. + {{{t "table.admin.banUserSucessMsg" email=email}}}
diff --git a/server/views/partials/admin/dialog/create_user.hbs b/server/views/partials/admin/dialog/create_user.hbs index 8d0dcca6e..d1950a4ca 100644 --- a/server/views/partials/admin/dialog/create_user.hbs +++ b/server/views/partials/admin/dialog/create_user.hbs @@ -1,5 +1,5 @@- The user "{{email}}" has been created successfully. + {{{t "table.admin.createUserSucessMsg" email=email}}}
diff --git a/server/views/partials/admin/dialog/delete_domain.hbs b/server/views/partials/admin/dialog/delete_domain.hbs index 993f7c88d..1484df281 100644 --- a/server/views/partials/admin/dialog/delete_domain.hbs +++ b/server/views/partials/admin/dialog/delete_domain.hbs @@ -1,18 +1,18 @@
- Are you sure do you want to delete the domain "{{address}}"?
+ {{{t "table.admin.deleteDomainDialogMsg" domain=address}}}
- The domain "{{address}}" has been deleted. + {{{t "table.admin.deleteDomainSuccessMsg" domain=address}}}
diff --git a/server/views/partials/admin/dialog/delete_user.hbs b/server/views/partials/admin/dialog/delete_user.hbs index 2bfa3b405..a8ba572ca 100644 --- a/server/views/partials/admin/dialog/delete_user.hbs +++ b/server/views/partials/admin/dialog/delete_user.hbs @@ -1,11 +1,10 @@
- Are you sure do you want to delete the user "{{email}}"?
- All their data including their links will be deleted.
+ {{{t "table.admin.deleteUserDialogMsg" email=email}}}
- The user "{{email}}" has been deleted. + {{{t "table.admin.deleteUserSucessMsg" email=email}}}
diff --git a/server/views/partials/admin/dialog/mesasge.hbs b/server/views/partials/admin/dialog/mesasge.hbs index 5ef8c8964..05c265100 100644 --- a/server/views/partials/admin/dialog/mesasge.hbs +++ b/server/views/partials/admin/dialog/mesasge.hbs @@ -5,7 +5,7 @@{{message}}
{{/if}} diff --git a/server/views/partials/admin/domains/actions.hbs b/server/views/partials/admin/domains/actions.hbs index 7dc944723..6e5b0da5b 100644 --- a/server/views/partials/admin/domains/actions.hbs +++ b/server/views/partials/admin/domains/actions.hbs @@ -1,6 +1,6 @@No link was found.
+{{t "table.actions.noLinkFound"}}
{{/if}}- Expires in {{relative_expire_in}} + {{t "table.expireIn" time=relative_expire_in}}
{{/if}} @@ -64,7 +64,7 @@- Total {{title}}: {{#if total includeZero=true}}{{total_formatted}}{{else}}-{{/if}} + {{#ifEquals title "users"}} + {{t "table.admin.totalUsers"}} + {{else ifEquals title "links"}} + {{t "table.admin.totalLinks"}} + {{else ifEquals title "domains"}} + {{t "table.admin.totalDomains"}} + {{else}} + {{title}} + {{/ifEquals}} + {{#if total includeZero=true}}{{total_formatted}}{{else}}-{{/if}}
- + @@ -9,10 +9,10 @@ {{> links/nav}} | ||||||||
---|---|---|---|---|---|---|---|---|
Original URL | -Created at | -Short link | -Views | +{{t "table.headerOriginalUrl"}} | +{{t "table.headerCreatedAt"}} | +{{t "table.headerShortLink"}} | +{{t "table.headerViews"}} |
{{error}} {{/if}} diff --git a/server/views/partials/report/email.hbs b/server/views/partials/report/email.hbs index 751bdb868..84ff0650c 100644 --- a/server/views/partials/report/email.hbs +++ b/server/views/partials/report/email.hbs @@ -9,7 +9,7 @@ > {{> icons/spinner}} - show email address + {{t "account.showEmailAddress"}}{{error}} {{/if}} diff --git a/server/views/partials/reset_password/new_password_form.hbs b/server/views/partials/reset_password/new_password_form.hbs index 3f4e9a466..c53b700d6 100644 --- a/server/views/partials/reset_password/new_password_form.hbs +++ b/server/views/partials/reset_password/new_password_form.hbs @@ -7,24 +7,24 @@ hx-swap="outerHTML" >- Your password is updated successfully. - You can now log in with your new password. + {{t "account.setPasswordSuccess"}} -Log in → \ No newline at end of file +{{t "login.login"}} → \ No newline at end of file diff --git a/server/views/partials/reset_password/request_form.hbs b/server/views/partials/reset_password/request_form.hbs index cac62f3c6..297749f64 100644 --- a/server/views/partials/reset_password/request_form.hbs +++ b/server/views/partials/reset_password/request_form.hbs @@ -10,7 +10,7 @@ {{else}}{{error}} {{/if}} diff --git a/server/views/partials/settings/apikey.hbs b/server/views/partials/settings/apikey.hbs index 765837b99..1fe59cb7d 100644 --- a/server/views/partials/settings/apikey.hbs +++ b/server/views/partials/settings/apikey.hbs @@ -1,11 +1,9 @@API- In additional to this website, you can use the API to create, delete and - get shortened URLs. If you're not familiar with API, don't generate the key. - DO NOT share this key on the client side of your website. - - Read API docs. + {{{t "settings.APIDocsDesc"}}} + + {{t "settings.readAPIDocs"}}
@@ -13,7 +11,7 @@
- Change email + {{t "settings.changeEmail"}}-Enter your password and a new email address to change your email address. +{{t "settings.changeEmailDesc" }} |