Skip to content
7 changes: 5 additions & 2 deletions server/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down Expand Up @@ -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) {
Expand All @@ -697,6 +698,7 @@ let needSetup = false;
callback({
ok: false,
msg: e.message,
msgi18n: !!e.msgi18n,
});
}
});
Expand Down Expand Up @@ -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);
Expand All @@ -1429,6 +1431,7 @@ let needSetup = false;
callback({
ok: false,
msg: e.message,
msgi18n: !!e.msgi18n,
});
}
});
Expand Down
17 changes: 17 additions & 0 deletions server/translatable-error.js
Original file line number Diff line number Diff line change
@@ -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;
1 change: 1 addition & 0 deletions src/lang/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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)",
Expand Down
77 changes: 58 additions & 19 deletions test/backend-test/check-translations.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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"));
Expand All @@ -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\(['"](?<key1>.*?)['"]\s*[,)]|i18n-t[^>]*\s+keypath="(?<key2>[^"]+)"/gd;

// detect server-side TranslatableError usage: new TranslatableError("key")
const translatableErrorRegex = /new\s+TranslatableError\(\s*['"](?<key3>[^'"]+)['"]\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,
});
}
}
}
});
});
}
}
}

Expand Down
Loading