chore: create a testcase to detect basic missing translations (#6591)

This commit is contained in:
Frank Elsinga 2026-01-06 00:40:11 +01:00 committed by GitHub
commit 85c278ece4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 129 additions and 28 deletions

View File

@ -14,25 +14,25 @@
<option value="numeric">{{ $t("Telephone number") }}</option>
</select>
<div class="form-text">
<p><b>{{ $t("Alphanumeric (recommended)") }}:</b><br /> {{ $t("Alphanumeric string (max 11 alphanumeric characters). Recipients can not reply to the message.") }}</p>
<p><b>{{ $t("Telephone number") }}:</b><br /> {{ $t("Numeric value (max 15 digits) with telephone number on international format without leading 00 (example UK number 07920 110 000 should be set as 447920110000). Recipients can reply to the message.") }}</p>
<p><b>{{ $t("Alphanumeric (recommended)") }}:</b><br /> {{ $t("cellsyntOriginatortypeAlphanumeric") }}</p>
<p><b>{{ $t("Telephone number") }}:</b><br /> {{ $t("cellsyntOriginatortypeNumeric") }}</p>
</div>
</div>
<div class="mb-3">
<label for="cellsynt-originator" class="form-label">{{ $t("Originator") }} <small>({{ $parent.notification.cellsyntOriginatortype === 'alpha' ? $t("max 11 alphanumeric characters") : $t("max 15 digits") }})</small></label>
<input v-if="$parent.notification.cellsyntOriginatortype === 'alpha'" id="cellsynt-originator" v-model="$parent.notification.cellsyntOriginator" type="text" class="form-control" pattern="[a-zA-Z0-9\s]+" maxlength="11" required>
<input v-else id="cellsynt-originator" v-model="$parent.notification.cellsyntOriginator" type="number" class="form-control" pattern="[0-9]+" maxlength="15" required>
<div class="form-text"><p>{{ $t("Visible on recipient's mobile phone as originator of the message. Allowed values and function depends on parameter originatortype.") }}</p></div>
<div class="form-text"><p>{{ $t("cellsyntOriginator") }}</p></div>
</div>
<div class="mb-3">
<label for="cellsynt-destination" class="form-label">{{ $t("Destination") }}</label>
<input id="cellsynt-destination" v-model="$parent.notification.cellsyntDestination" type="text" class="form-control" required>
<div class="form-text"><p>{{ $t("Recipient's telephone number using international format with leading 00 followed by country code, e.g. 00447920110000 for the UK number 07920 110 000 (max 17 digits in total). Max 25000 comma separated recipients per HTTP request.") }}</p></div>
<div class="form-text"><p>{{ $t("cellsyntDestination") }}</p></div>
</div>
<div class="form-check form-switch">
<input id="cellsynt-allow-long" v-model="$parent.notification.cellsyntAllowLongSMS" type="checkbox" class="form-check-input">
<label for="cellsynt-allow-long" class="form-label">{{ $t("Allow Long SMS") }}</label>
<div class="form-text">{{ $t("Split long messages into up to 6 parts. 153 x 6 = 918 characters.") }}</div>
<div class="form-text">{{ $t("cellsyntSplitLongMessages") }}</div>
</div>
<i18n-t tag="p" keypath="More info on:" style="margin-top: 8px;">
<a href="https://www.cellsynt.com/en/" target="_blank">https://www.cellsynt.com/en/</a>

View File

@ -10,7 +10,7 @@
</div>
<div class="mb-3">
<div class="form-text">
{{ $t("checkPrice", [$t("clicksendsms")]) }}
{{ $t("checkPrice", ["clicksendsms"]) }}
<a href="https://www.clicksend.com/us/pricing" target="_blank">https://clicksend.com/us/pricing</a>
</div>
</div>

View File

@ -2,7 +2,7 @@
<div class="mb-3">
<label for="octopush-version" class="form-label">{{ $t("Octopush API Version") }}</label>
<select id="octopush-version" v-model="$parent.notification.octopushVersion" class="form-select">
<option value="2">{{ $t("octopush") }} ({{ $t("endpoint") }}: api.octopush.com)</option>
<option value="2">{{ "octopush" }} ({{ $t("endpoint") }}: api.octopush.com)</option>
<option value="1">{{ $t("Legacy Octopush-DM") }} ({{ $t("endpoint") }}: www.octopush-dm.com)</option>
</select>
<div class="form-text">

View File

@ -14,7 +14,7 @@
<a href="https://docs.rocket.chat/guides/administration/administration/integrations" target="_blank">https://docs.rocket.chat/guides/administration/administration/integrations</a>
</i18n-t>
<p style="margin-top: 8px;">
{{ $t("aboutChannelName", [$t("rocket.chat")]) }}
{{ $t("aboutChannelName", ["rocket.chat"]) }}
</p>
<p style="margin-top: 8px;">
{{ $t("aboutKumaURL") }}

View File

@ -24,7 +24,7 @@
<a href="https://api.slack.com/messaging/webhooks" target="_blank">https://api.slack.com/messaging/webhooks</a>
</i18n-t>
<p style="margin-top: 8px;">
{{ $t("aboutChannelName", [$t("slack")]) }}
{{ $t("aboutChannelName", ["slack"]) }}
</p>
<p style="margin-top: 8px;">
{{ $t("aboutKumaURL") }}

View File

@ -100,7 +100,7 @@
</i18n-t>
<i18n-t tag="p" keypath="disableauth.message2">
<template #intendThirdPartyAuth>
<strong>{{ $t('intend to implement third-party authentication') }}</strong>
<strong>{{ $t('where you intend to implement third-party authentication') }}</strong>
</template>
</i18n-t>
<p>{{ $t("Please use this option carefully!") }}</p>

View File

@ -1174,6 +1174,7 @@
"resendFromName": "From Name",
"resendFromEmail": "From Email",
"resendLeaveBlankForDefaultName": "leave blank for default name",
"resendLeaveBlankForDefaultSubject": "Leave blank for default subject",
"resendToEmail": "To Email",
"resendSubject": "Subject",
"pingCountLabel": "Max Packets",
@ -1245,7 +1246,43 @@
"minimumIntervalWarning": "Intervals below 20 seconds may result in poor performance.",
"lowIntervalWarning": "Are you sure want to set the interval value below 20 seconds? Performance may be degraded, particularly if there are a large number of monitors.",
"imageResetConfirmation": "Image reset to default",
"mtls-auth-server-cert-label": "Cert",
"mtls-auth-server-cert-placeholder": "Cert body",
"mtls-auth-server-key-label": "Key",
"mtls-auth-server-key-placeholder": "Key body",
"mtls-auth-server-ca-label": "CA",
"mtls-auth-server-ca-placeholder": "Server CA",
"avgPing": "Avg Ping",
"Uptime Kuma": "Uptime Kuma",
"maxPing": "Max Ping",
"minPing": "Min Ping"
"minPing": "Min Ping",
"Clear current filters": "Clear current filters",
"Sort options": "Sort options",
"Sort by status": "Sort by status",
"Sort by name": "Sort by name",
"Sort by uptime": "Sort by uptime",
"Sort by certificate expiry": "Sort by certificate expiry",
"Splunk Rest URL": "Splunk Rest URL",
"Severity": "Severity",
"SMSManager": "SMSManager",
"Message Format": "Message Format",
"smscTranslit": "smscTranslit",
"promosms": "promosms",
"Region": "Region",
"PushDeer Server URL": "PushDeer Server URL",
"To Number": "To Number",
"GrafanaOncallURL": "Grafana Oncall URL",
"Never": "Never",
"Json Query": "Json Query",
"System Service": "System Service",
"SSL/TLS": "SSL/TLS",
"playground": "playground",
"Check Type": "Check Type",
"Service Name": "Service Name",
"GRPC Options": "GRPC Options",
"Metadata": "Metadata",
"End": "End",
"Show this Maintenance Message on which Status Pages": "Show this Maintenance Message on which Status Pages",
"Endpoint": "Endpoint",
"Details": "Details"
}

View File

@ -1166,16 +1166,16 @@
<template v-if="monitor.authMethod && monitor.authMethod !== null ">
<template v-if="monitor.authMethod === 'mtls' ">
<div class="my-3">
<label for="tls-cert" class="form-label">{{ $t("Cert") }}</label>
<textarea id="tls-cert" v-model="monitor.tlsCert" class="form-control" :placeholder="$t('Cert body')" required></textarea>
<label for="tls-cert" class="form-label">{{ $t("mtls-auth-server-cert-label") }}</label>
<textarea id="tls-cert" v-model="monitor.tlsCert" class="form-control" :placeholder="$t('mtls-auth-server-cert-placeholder')" required></textarea>
</div>
<div class="my-3">
<label for="tls-key" class="form-label">{{ $t("Key") }}</label>
<textarea id="tls-key" v-model="monitor.tlsKey" class="form-control" :placeholder="$t('Key body')" required></textarea>
<label for="tls-key" class="form-label">{{ $t("mtls-auth-server-key-label") }}</label>
<textarea id="tls-key" v-model="monitor.tlsKey" class="form-control" :placeholder="$t('mtls-auth-server-key-placeholder')" required></textarea>
</div>
<div class="my-3">
<label for="tls-ca" class="form-label">{{ $t("CA") }}</label>
<textarea id="tls-ca" v-model="monitor.tlsCa" class="form-control" :placeholder="$t('Server CA')"></textarea>
<label for="tls-ca" class="form-label">{{ $t("mtls-auth-server-ca-label") }}</label>
<textarea id="tls-ca" v-model="monitor.tlsCa" class="form-control" :placeholder="$t('mtls-auth-server-ca-placeholder')"></textarea>
</div>
</template>
<template v-else-if="monitor.authMethod === 'oauth2-cc' ">
@ -1447,17 +1447,6 @@ export default {
return this.monitor.type === "ping" ? 60 : undefined;
},
timeoutLabel() {
return this.monitor.type === "ping" ? this.$t("pingTimeoutLabel") : this.$t("Request Timeout");
},
timeoutDescription() {
if (this.monitor.type === "ping") {
return this.$t("pingTimeoutDescription");
}
return "";
},
defaultFriendlyName() {
if (this.monitor.hostname) {
return this.monitor.hostname;

View File

@ -0,0 +1,75 @@
const { describe, it } = require("node:test");
const assert = require("node:assert");
const fs = require("fs");
const path = require("path");
/**
* Recursively walks a directory and yields file paths.
* @param {string} dir The directory to walk.
* @yields {string} The path to a file.
* @returns {Generator<string>} A generator that yields file paths.
*/
function* walk(dir) {
const files = fs.readdirSync(dir, { withFileTypes: true });
for (const file of files) {
if (file.isDirectory()) {
yield* walk(path.join(dir, file.name));
} else {
yield path.join(dir, file.name);
}
}
}
describe("Check Translations", () => {
it("should not have missing translation keys", () => {
const enTranslations = JSON.parse(fs.readFileSync("src/lang/en.json", "utf-8"));
// this is a resonably crude check, you can get around this trivially
/// 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;
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,
});
}
}
});
}
}
if (missingKeys.length > 0) {
let report = "Missing translation keys found:\n";
missingKeys.forEach(({ filePath, lineNum, key, line, start, end }) => {
report += `\nerror: Missing translation key: '${key}'`;
report += `\n --> ${filePath}:${lineNum}:${start}`;
report += "\n |";
report += `\n${String(lineNum).padEnd(5)}| ${line}`;
const arrow = " ".repeat(start) + "^".repeat(end - start);
report += `\n | ${arrow} unrecognized translation key`;
report += "\n |";
report += `\n = note: please register the translation key '${key}' in en.json so that our awesome team of translators can translate them`;
report += "\n = tip: if you want to contribute translations, please visit https://weblate.kuma.pet\n";
});
report += "\n===============================";
const fileCount = new Set(missingKeys.map(item => item.filePath)).size;
report += `\nFound a total of ${missingKeys.length} missing keys in ${fileCount} files.`;
assert.fail(report);
}
});
});