diff --git a/server/server.js b/server/server.js index e37ed19cb..caff352a2 100644 --- a/server/server.js +++ b/server/server.js @@ -75,6 +75,7 @@ const gracefulShutdown = require("http-graceful-shutdown"); log.debug("server", "Importing prometheus-api-metrics"); const prometheusAPIMetrics = require("prometheus-api-metrics"); const { passwordStrength } = require("check-password-strength"); +const TranslatableError = require("./translatable-error"); log.debug("server", "Importing 2FA Modules"); const notp = require("notp"); @@ -673,7 +674,7 @@ let needSetup = false; socket.on("setup", async (username, password, callback) => { try { if (passwordStrength(password).value === "Too weak") { - throw new Error("Password is too weak. It should contain alphabetic and numeric characters. It must be at least 6 characters in length."); + throw new TranslatableError("passwordTooWeak"); } if ((await R.knex("user").count("id as count").first()).count !== 0) { @@ -697,6 +698,7 @@ let needSetup = false; callback({ ok: false, msg: e.message, + msgi18n: !!e.msgi18n, }); } }); @@ -1410,7 +1412,7 @@ let needSetup = false; } if (passwordStrength(password.newPassword).value === "Too weak") { - throw new Error("Password is too weak. It should contain alphabetic and numeric characters. It must be at least 6 characters in length."); + throw new TranslatableError("passwordTooWeak"); } let user = await doubleCheckPassword(socket, password.currentPassword); @@ -1429,6 +1431,7 @@ let needSetup = false; callback({ ok: false, msg: e.message, + msgi18n: !!e.msgi18n, }); } }); diff --git a/server/translatable-error.js b/server/translatable-error.js new file mode 100644 index 000000000..52528d37d --- /dev/null +++ b/server/translatable-error.js @@ -0,0 +1,17 @@ +class TranslatableError extends Error { + /** + * Error whose message is a translation key. + * @augments Error + */ + /** + * Create a TranslatableError. + * @param {string} key - Translation key present in src/lang/en.json + */ + constructor(key) { + super(key); + this.msgi18n = true; + this.key = key; + Error.captureStackTrace(this, this.constructor); + } +} +module.exports = TranslatableError; diff --git a/src/lang/en.json b/src/lang/en.json index fa391d93b..99c0300dd 100644 --- a/src/lang/en.json +++ b/src/lang/en.json @@ -1304,6 +1304,7 @@ "Show this Maintenance Message on which Status Pages": "Show this Maintenance Message on which Status Pages", "Endpoint": "Endpoint", "Details": "Details", + "passwordTooWeak": "Password is too weak. It should contain alphabetic and numeric characters. It must be at least 6 characters in length.", "TLS Alerts": "TLS Alerts", "Expected TLS Alert": "Expected TLS Alert", "None (Successful Connection)": "None (Successful Connection)", diff --git a/test/backend-test/check-translations.test.js b/test/backend-test/check-translations.test.js index b120dcc00..08da2e3fd 100644 --- a/test/backend-test/check-translations.test.js +++ b/test/backend-test/check-translations.test.js @@ -20,6 +20,20 @@ function* walk(dir) { } } +/** + * Fallback to get start/end indices of a key within a line. + * @param {string} line - Line of text to search in. + * @param {string} key - Key to find. + * @returns {[number, number]} Array [start, end] representing the indices of the key in the line. + */ +function getStartEnd(line, key) { + let start = line.indexOf(key); + if (start === -1) { + start = 0; + } + return [ start, start + key.length ]; +} + describe("Check Translations", () => { it("should not have missing translation keys", () => { const enTranslations = JSON.parse(fs.readFileSync("src/lang/en.json", "utf-8")); @@ -28,28 +42,53 @@ describe("Check Translations", () => { /// this check is just to save on maintainer energy to explain this on every review ^^ const translationRegex = /\$t\(['"](?.*?)['"]\s*[,)]|i18n-t[^>]*\s+keypath="(?[^"]+)"/gd; + // detect server-side TranslatableError usage: new TranslatableError("key") + const translatableErrorRegex = /new\s+TranslatableError\(\s*['"](?[^'"]+)['"]\s*\)/g; + const missingKeys = []; - for (const filePath of walk("src")) { - if (filePath.endsWith(".vue") || filePath.endsWith(".js")) { - const lines = fs.readFileSync(filePath, "utf-8").split("\n"); - lines.forEach((line, lineNum) => { - let match; - while ((match = translationRegex.exec(line)) !== null) { - const key = match.groups.key1 || match.groups.key2; - if (key && !enTranslations[key]) { - const [ start, end ] = match.groups.key1 ? match.indices.groups.key1 : match.indices.groups.key2; - missingKeys.push({ - filePath, - lineNum: lineNum + 1, - key, - line: line, - start, - end, - }); + const roots = [ "src", "server" ]; + + for (const root of roots) { + for (const filePath of walk(root)) { + if (filePath.endsWith(".vue") || filePath.endsWith(".js")) { + const lines = fs.readFileSync(filePath, "utf-8").split("\n"); + lines.forEach((line, lineNum) => { + let match; + // front-end style keys ($t / i18n-t) + while ((match = translationRegex.exec(line)) !== null) { + const key = match.groups.key1 || match.groups.key2; + if (key && !enTranslations[key]) { + const [ start, end ] = getStartEnd(line, key); + missingKeys.push({ + filePath, + lineNum: lineNum + 1, + key, + line: line, + start, + end, + }); + } } - } - }); + + // server-side TranslatableError usage + let m; + while ((m = translatableErrorRegex.exec(line)) !== null) { + const key3 = m.groups.key3; + if (key3 && !enTranslations[key3]) { + const [ start, end ] = getStartEnd(line, key3); + missingKeys.push({ + filePath, + lineNum: lineNum + 1, + key: key3, + line: line, + start, + end, + }); + } + } + }); + } } }