Browse Source

Merge 188ac58a22 into 2333d1c7a7

pull/597/merge
Bert Verhelst 4 years ago
committed by GitHub
parent
commit
c68f77878b
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 99
      extra/extract-translations.js
  2. 3
      extra/update-language-files/.gitignore
  3. 86
      extra/update-language-files/index.js
  4. 12
      extra/update-language-files/package.json
  5. 73
      package-lock.json
  6. 6
      package.json
  7. 18
      src/components/NotificationDialog.vue
  8. 126
      src/components/notifications/index.js
  9. 2
      src/languages/README.md
  10. 9
      src/pages/Details.vue
  11. 2
      src/pages/EditMonitor.vue

99
extra/extract-translations.js

@ -0,0 +1,99 @@
const findInFiles = require("find-in-files");
const _ = require("lodash");
const fs = require("fs/promises");
const JSON5 = require("json5");
// Extract translations from $t() functions in the source code and add the missing translations to all language files in src/languages/*.js
async function extractTranslations() {
// Load all ES6 module translation files into a commonJS process
const languageList = {};
const filesNames = await fs.readdir("src/languages");
for (let fileName of filesNames) {
if (fileName.endsWith("js") && fileName !== "index.js") {
const content = (await fs.readFile("src/languages/" + fileName)).toString("utf-8");
const json = content.replace("export default {", "{").replace("};", "}");
languageList[fileName.split(".")[0]] = JSON5.parse(json);
}
}
const en = languageList.en;
const englishExtracted = [];
// Search the source code for usages of $t(...)
const tFuncResults = await findInFiles.find({
term: "\\$t\\(([^)]+?)\\)",
flags: "g",
}, "./src", "\\.(vue|js)");
// Add the found strings to the englishExtracted list
const warnings = [];
Object.values(tFuncResults).map(result => {
result.matches.map(match => {
const functionParams = match.substring(3, match.length - 1).trim();
const firstChar = functionParams[0];
if (!["\"", "'"].includes(firstChar)) {
// Variable => cannot extract => output warning
warnings.push("Cannot extract non string values in " + match);
} else {
// Actual string
const content = _.trim(functionParams.split(firstChar)[1], "\"' ");
englishExtracted.push(content);
}
});
});
// Search the source code for usages of <i18n-t tag="..." keypath="...">
const i18nTTagResults = await findInFiles.find({
term: "<i18n-t[^>]+keypath=\"([^\"]+)\"[^>]*>",
flags: "g",
}, "./src", "\\.vue");
// Add the found strings to the englishExtracted list
Object.values(i18nTTagResults).map(result => {
result.matches.map(match => {
const content = _.trim(match.split("keypath")[1].split("\"")[1], "\"' ");
englishExtracted.push(content);
});
});
// Update all languages with the missing strings
for (let extractedTranslation of englishExtracted) {
for (let langDict of Object.values(languageList)) {
if (!Object.keys(langDict).includes(extractedTranslation)) {
langDict[extractedTranslation] = en[extractedTranslation] || extractedTranslation;
}
}
}
// Check for translations in other language files that are not in the English file and output warnings for them
const englishKeys = Object.keys(en);
for (let langName of Object.keys(languageList)) {
if (langName !== "en") {
const langKeys = Object.keys(languageList[langName]);
const unusedKeys = _.without(langKeys, ...englishKeys);
if (unusedKeys.length) {
warnings.push(`Language file ${langName} contains keys that are not used: ["${unusedKeys.join("\", \"")}"]`);
}
}
}
// Write the translation string json back to files
for (let langName of Object.keys(languageList)) {
const translationsString = JSON5.stringify(languageList[langName], {
quote: "\"",
space: 4,
})
.replace(/"$/m, "\","); // Add comma to the last line
await fs.writeFile(`./src/languages/${_.kebabCase(langName)}.js`, `export default ${translationsString};\n`);
}
// Output warnings if there are any
if (warnings.length) {
console.log("Extraction successful with warnings: \n\t" + warnings.join("\n\t"));
} else {
console.log("Extraction successful");
}
}
extractTranslations();

3
extra/update-language-files/.gitignore

@ -1,3 +0,0 @@
package-lock.json
test.js
languages/

86
extra/update-language-files/index.js

@ -1,86 +0,0 @@
// Need to use ES6 to read language files
import fs from "fs";
import path from "path";
import util from "util";
// https://stackoverflow.com/questions/13786160/copy-folder-recursively-in-node-js
/**
* Look ma, it's cp -R.
* @param {string} src The path to the thing to copy.
* @param {string} dest The path to the new copy.
*/
const copyRecursiveSync = function (src, dest) {
let exists = fs.existsSync(src);
let stats = exists && fs.statSync(src);
let isDirectory = exists && stats.isDirectory();
if (isDirectory) {
fs.mkdirSync(dest);
fs.readdirSync(src).forEach(function (childItemName) {
copyRecursiveSync(path.join(src, childItemName),
path.join(dest, childItemName));
});
} else {
fs.copyFileSync(src, dest);
}
};
console.log("Arguments:", process.argv);
const baseLangCode = process.argv[2] || "en";
console.log("Base Lang: " + baseLangCode);
if (fs.existsSync("./languages")) {
fs.rmdirSync("./languages", { recursive: true });
}
copyRecursiveSync("../../src/languages", "./languages");
const en = (await import("./languages/en.js")).default;
const baseLang = (await import(`./languages/${baseLangCode}.js`)).default;
const files = fs.readdirSync("./languages");
console.log("Files:", files);
for (const file of files) {
if (!file.endsWith(".js")) {
console.log("Skipping " + file);
continue;
}
console.log("Processing " + file);
const lang = await import("./languages/" + file);
let obj;
if (lang.default) {
obj = lang.default;
} else {
console.log("Empty file");
obj = {
languageName: "<Your Language name in your language (not in English)>"
};
}
// En first
for (const key in en) {
if (! obj[key]) {
obj[key] = en[key];
}
}
if (baseLang !== en) {
// Base second
for (const key in baseLang) {
if (! obj[key]) {
obj[key] = key;
}
}
}
const code = "export default " + util.inspect(obj, {
depth: null,
});
fs.writeFileSync(`../../src/languages/${file}`, code);
}
fs.rmdirSync("./languages", { recursive: true });
console.log("Done. Fixing formatting by ESLint...");

12
extra/update-language-files/package.json

@ -1,12 +0,0 @@
{
"name": "update-language-files",
"type": "module",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC"
}

73
package-lock.json

@ -1,12 +1,12 @@
{
"name": "uptime-kuma",
"version": "1.9.2",
"version": "1.10.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "uptime-kuma",
"version": "1.9.2",
"version": "1.10.0",
"license": "MIT",
"dependencies": {
"@fortawesome/fontawesome-svg-core": "~1.2.36",
@ -76,8 +76,11 @@
"dns2": "~2.0.1",
"eslint": "~7.32.0",
"eslint-plugin-vue": "~7.18.0",
"find-in-files": "^0.5.0",
"jest": "~27.2.4",
"jest-puppeteer": "~6.0.0",
"json5": "^2.2.0",
"lodash": "^4.17.21",
"puppeteer": "~10.4.0",
"sass": "~1.42.1",
"stylelint": "~13.13.1",
@ -6238,6 +6241,15 @@
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
},
"node_modules/find": {
"version": "0.1.7",
"resolved": "https://registry.npmjs.org/find/-/find-0.1.7.tgz",
"integrity": "sha1-yGyHrxqxjyIrvjjeyGy8dg0Wpvs=",
"dev": true,
"dependencies": {
"traverse-chain": "~0.1.0"
}
},
"node_modules/find-file-up": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/find-file-up/-/find-file-up-0.1.3.tgz",
@ -6251,6 +6263,16 @@
"node": ">=0.10.0"
}
},
"node_modules/find-in-files": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/find-in-files/-/find-in-files-0.5.0.tgz",
"integrity": "sha512-VraTc6HdtdSHmAp0yJpAy20yPttGKzyBWc7b7FPnnsX9TOgmKx0g9xajizpF/iuu4IvNK4TP0SpyBT9zAlwG+g==",
"dev": true,
"dependencies": {
"find": "^0.1.5",
"q": "^1.0.1"
}
},
"node_modules/find-pkg": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/find-pkg/-/find-pkg-0.1.2.tgz",
@ -11591,6 +11613,16 @@
}
}
},
"node_modules/q": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz",
"integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=",
"dev": true,
"engines": {
"node": ">=0.6.0",
"teleport": ">=0.2.0"
}
},
"node_modules/qrcode": {
"version": "1.4.4",
"resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.4.4.tgz",
@ -13745,6 +13777,12 @@
"node": ">=8"
}
},
"node_modules/traverse-chain": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/traverse-chain/-/traverse-chain-0.1.0.tgz",
"integrity": "sha1-YdvC1Ttp/2CRoSoWj9fUMxB+QPE=",
"dev": true
},
"node_modules/tree-kill": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz",
@ -19500,6 +19538,15 @@
}
}
},
"find": {
"version": "0.1.7",
"resolved": "https://registry.npmjs.org/find/-/find-0.1.7.tgz",
"integrity": "sha1-yGyHrxqxjyIrvjjeyGy8dg0Wpvs=",
"dev": true,
"requires": {
"traverse-chain": "~0.1.0"
}
},
"find-file-up": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/find-file-up/-/find-file-up-0.1.3.tgz",
@ -19510,6 +19557,16 @@
"resolve-dir": "^0.1.0"
}
},
"find-in-files": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/find-in-files/-/find-in-files-0.5.0.tgz",
"integrity": "sha512-VraTc6HdtdSHmAp0yJpAy20yPttGKzyBWc7b7FPnnsX9TOgmKx0g9xajizpF/iuu4IvNK4TP0SpyBT9zAlwG+g==",
"dev": true,
"requires": {
"find": "^0.1.5",
"q": "^1.0.1"
}
},
"find-pkg": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/find-pkg/-/find-pkg-0.1.2.tgz",
@ -23526,6 +23583,12 @@
}
}
},
"q": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz",
"integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=",
"dev": true
},
"qrcode": {
"version": "1.4.4",
"resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.4.4.tgz",
@ -25200,6 +25263,12 @@
"punycode": "^2.1.1"
}
},
"traverse-chain": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/traverse-chain/-/traverse-chain-0.1.0.tgz",
"integrity": "sha1-YdvC1Ttp/2CRoSoWj9fUMxB+QPE=",
"dev": true
},
"tree-kill": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz",

6
package.json

@ -48,8 +48,7 @@
"test-install-script-ubuntu1604": "npm run compile-install-script && docker build --progress plain -f test/test_install_script/ubuntu1604.dockerfile .",
"test-nodejs16": "docker build --progress plain -f test/ubuntu-nodejs16.dockerfile .",
"simple-dns-server": "node extra/simple-dns-server.js",
"update-language-files-with-base-lang": "cd extra/update-language-files && node index.js %npm_config_base_lang% && eslint ../../src/languages/**.js --fix",
"update-language-files": "cd extra/update-language-files && node index.js && eslint ../../src/languages/**.js --fix"
"extract-translations": "node extra/extract-translations.js"
},
"dependencies": {
"@fortawesome/fontawesome-svg-core": "~1.2.36",
@ -119,8 +118,11 @@
"dns2": "~2.0.1",
"eslint": "~7.32.0",
"eslint-plugin-vue": "~7.18.0",
"find-in-files": "^0.5.0",
"jest": "~27.2.4",
"jest-puppeteer": "~6.0.0",
"json5": "^2.2.0",
"lodash": "^4.17.21",
"puppeteer": "~10.4.0",
"sass": "~1.42.1",
"stylelint": "~13.13.1",

18
src/components/NotificationDialog.vue

@ -13,7 +13,7 @@
<div class="mb-3">
<label for="notification-type" class="form-label">{{ $t("Notification Type") }}</label>
<select id="notification-type" v-model="notification.type" class="form-select">
<option v-for="type in notificationTypes" :key="type" :value="type">{{ $t(type) }}</option>
<option v-for="type in notificationTypes" :key="type" :value="type">{{ translatedType }}</option>
</select>
</div>
@ -69,10 +69,9 @@
<script lang="ts">
import { Modal } from "bootstrap";
import { ucfirst } from "../util.ts";
import Confirm from "./Confirm.vue";
import NotificationFormList from "./notifications";
import getNotificationFormList from "./notifications";
export default {
components: {
@ -81,14 +80,16 @@ export default {
props: {},
emits: ["added"],
data() {
const notificationTypeObjects = getNotificationFormList(this.$t);
return {
model: null,
processing: false,
id: null,
notificationTypes: Object.keys(NotificationFormList),
notificationTypeObjects: notificationTypeObjects,
notificationTypes: Object.keys(notificationTypeObjects),
notification: {
name: "",
/** @type { null | keyof NotificationFormList } */
/** @type { null | keyof getNotificationFormList() } */
type: null,
isDefault: false,
// Do not set default value here, please scroll to show()
@ -101,7 +102,10 @@ export default {
if (!this.notification.type) {
return null;
}
return NotificationFormList[this.notification.type];
return this.notificationTypeObjects[this.notification.type].component;
},
translatedType() {
return this.notificationTypeObjects[this.type].label;
}
},
@ -192,7 +196,7 @@ export default {
});
},
/**
* @param {keyof NotificationFormList} notificationKey
* @param {keyof getNotificationFormList()} notificationKey
* @return {string}
*/
getUniqueDefaultName(notificationKey) {

126
src/components/notifications/index.js

@ -1,4 +1,4 @@
import STMP from "./SMTP.vue"
import STMP from "./SMTP.vue";
import Telegram from "./Telegram.vue";
import Discord from "./Discord.vue";
import Webhook from "./Webhook.vue";
@ -28,31 +28,103 @@ import Bark from "./Bark.vue";
*
* @type { Record<string, any> }
*/
const NotificationFormList = {
"telegram": Telegram,
"webhook": Webhook,
"smtp": STMP,
"discord": Discord,
"teams": Teams,
"signal": Signal,
"gotify": Gotify,
"slack": Slack,
"rocket.chat": RocketChat,
"pushover": Pushover,
"pushy": Pushy,
"octopush": Octopush,
"promosms": PromoSMS,
"clicksendsms": ClickSendSMS,
"lunasea": LunaSea,
"Feishu": Feishu,
"AliyunSMS": AliyunSMS,
"apprise": Apprise,
"pushbullet": Pushbullet,
"line": Line,
"mattermost": Mattermost,
"matrix": Matrix,
"DingDing": DingDing,
"Bark": Bark
const getNotificationFormList = ($t) => ({
"telegram": {
component: Telegram,
label: $t("telegram")
},
"webhook": {
component: Webhook,
label: $t("webhook"),
},
"smtp": {
component: STMP,
label: $t("smtp"),
},
"discord": {
component: Discord,
label: $t("discord"),
},
"teams": {
component: Teams,
label: $t("teams"),
},
"signal": {
component: Signal,
label: $t("signal"),
},
"gotify": {
component: Gotify,
label: $t("gotify"),
},
"slack": {
component: Slack,
label: $t("slack"),
},
"rocket.chat": {
component: RocketChat,
label: $t("rocket.chat"),
},
"pushover": {
component: Pushover,
label: $t("pushover"),
},
"pushy": {
component: Pushy,
label: $t("pushy"),
},
"octopush": {
component: Octopush,
label: $t("octopush"),
},
"promosms": {
component: PromoSMS,
label: $t("promosms"),
},
"clicksendsms": {
component: ClickSendSMS,
label: $t("clicksendsms"),
},
"lunasea": {
component: LunaSea,
label: $t("lunasea"),
},
"Feishu": {
component: Feishu,
label: $t("Feishu"),
},
"AliyunSMS": {
component: AliyunSMS,
label: $t("AliyunSMS"),
},
"apprise": {
component: Apprise,
label: $t("apprise"),
},
"pushbullet": {
component: Pushbullet,
label: $t("pushbullet"),
},
"line": {
component: Line,
label: $t("line"),
},
"mattermost": {
component: Mattermost,
label: $t("mattermost"),
},
"matrix": {
component: Matrix,
label: $t("matrix"),
},
"DingDing": {
component: DingDing,
label: $t("DingDing"),
},
"Bark": {
component: Bark,
label: $t("Bark"),
}
});
export default NotificationFormList
export default getNotificationFormList;

2
src/languages/README.md

@ -2,7 +2,7 @@
1. Fork this repo.
2. Create a language file (e.g. `zh-TW.js`). The filename must be ISO language code: http://www.lingoes.net/en/translator/langcode.htm
3. Run `npm run update-language-files`. You can also use this command to check if there are new strings to translate for your language.
3. Run `npm run extract-translations`. This will add all translatable strings to your language file. Any untranslated keys will default to English.
4. Your language file should be filled in. You can translate now.
5. Translate `src/pages/Settings.vue` (search for a `Confirm` component with `rel="confirmDisableAuth"`).
6. Import your language file in `src/i18n.js` and add it to `languageList` constant.

9
src/pages/Details.vue

@ -350,16 +350,11 @@ export default {
},
pingTitle(average = false) {
let translationPrefix = "";
if (average) {
translationPrefix = "Avg. ";
}
if (this.monitor.type === "http") {
return this.$t(translationPrefix + "Response");
return average ? this.$t("Avg. Response") : this.$t("Response");
}
return this.$t(translationPrefix + "Ping");
return average ? this.$t("Avg. Ping") : this.$t("Ping");
},
},
};

2
src/pages/EditMonitor.vue

@ -324,7 +324,7 @@ export default {
},
pageName() {
return this.$t((this.isAdd) ? "Add New Monitor" : "Edit");
return this.isAdd ? this.$t("Add New Monitor") : this.$t("Edit");
},
isAdd() {

Loading…
Cancel
Save