1
0
mirror of https://github.com/louislam/uptime-kuma.git synced 2024-12-26 22:56:52 +02:00

Merge branch 'master' into clean-mobile-table

This commit is contained in:
LouisLam 2021-08-24 21:55:16 +08:00
commit d8d428907e
19 changed files with 8050 additions and 601 deletions

View File

@ -16,6 +16,7 @@ module.exports = {
requireConfigFile: false, requireConfigFile: false,
}, },
rules: { rules: {
"camelcase": "warn",
// override/add rules settings here, such as: // override/add rules settings here, such as:
// 'vue/no-unused-vars': 'error' // 'vue/no-unused-vars': 'error'
"no-unused-vars": "warn", "no-unused-vars": "warn",

View File

@ -6,7 +6,38 @@ The project was created with vite.js (vue3). Then I created a sub-directory call
The frontend code build into "dist" directory. The server uses "dist" as root. This is how production is working. The frontend code build into "dist" directory. The server uses "dist" as root. This is how production is working.
Your IDE should follow the config in ".editorconfig". The most special thing is I set it to 4 spaces indentation. I know 2 spaces indentation became a kind of standard nowadays for js, but my eyes is not so comfortable for this. In my opinion, there is no callback-hell nowadays, it is good to go back 4 spaces world again. # Can I create a pull request for Uptime Kuma?
Generally, if the pull request is working fine and it do not affect any existing logic, workflow and perfomance, I will merge to the master branch once it is tested.
If you are not sure, feel free to create an empty pull request draft first.
## Pull Request Examples
### ✅ High - Medium Priority
- Add a new notification
- Add a chart
- Fix a bug
### *️⃣ Requires one more reviewer
I do not have such knowledge to test it
- Add k8s supports
### *️⃣ Low Priority
It chnaged my current workflow and require further studies.
- Change my release approach
### ❌ Won't Merge
- Duplicated pull request
- Buggy
- Existing logic is completely modified or deleted
- A function that is completely out of scope
# Project Styles # Project Styles
@ -19,16 +50,27 @@ For example, recently, because I am not a python expert, I spent a 2 hours to re
- All settings in frontend. - All settings in frontend.
- Easy to use - Easy to use
# Coding Styles
- Follow .editorconfig
- Follow eslint
## Name convention
- Javascript/Typescript: camelCaseType
- SQLite: underscore_type
- CSS/SCSS: dash-type
# Tools # Tools
- Node.js >= 14 - Node.js >= 14
- Git - Git
- IDE that supports .editorconfig (I am using Intellji Idea) - IDE that supports .editorconfig and eslint (I am using Intellji Idea)
- A SQLite tool (I am using SQLite Expert Personal) - A SQLite tool (I am using SQLite Expert Personal)
# Prepare the dev # Install dependencies
```bash ```bash
npm install npm install --dev
``` ```
# Backend Dev # Backend Dev
@ -39,7 +81,6 @@ npm run start-server
# Or # Or
node server/server.js node server/server.js
``` ```
It binds to 0.0.0.0:3001 by default. It binds to 0.0.0.0:3001 by default.
@ -92,7 +133,8 @@ The data and socket logic in "src/mixins/socket.js"
# Database Migration # Database Migration
TODO 1. create `patch{num}.sql` in `./db/`
1. update `latestVersion` in `./server/database.js`
# Unit Test # Unit Test

8124
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -10,6 +10,9 @@
"node": "14.*" "node": "14.*"
}, },
"scripts": { "scripts": {
"lint:js": "eslint --ext \".js,.vue\" --ignore-path .gitignore .",
"lint:style": "stylelint \"**/*.{vue,css}\" --ignore-path .gitignore",
"lint": "npm run lint:js && npm run lint:style",
"dev": "vite --host", "dev": "vite --host",
"start": "npm run start-server", "start": "npm run start-server",
"start-server": "node server/server.js", "start-server": "node server/server.js",
@ -64,6 +67,7 @@
"vue": "^3.2.2", "vue": "^3.2.2",
"vue-chart-3": "^0.5.7", "vue-chart-3": "^0.5.7",
"vue-confirm-dialog": "^1.0.2", "vue-confirm-dialog": "^1.0.2",
"vue-i18n": "^9.1.7",
"vue-multiselect": "^3.0.0-alpha.2", "vue-multiselect": "^3.0.0-alpha.2",
"vue-router": "^4.0.11", "vue-router": "^4.0.11",
"vue-toastification": "^2.0.0-rc.1" "vue-toastification": "^2.0.0-rc.1"

View File

@ -1,8 +1,6 @@
const fs = require("fs"); const fs = require("fs");
const { sleep, debug, isDev } = require("../src/util");
const { R } = require("redbean-node"); const { R } = require("redbean-node");
const { setSetting, setting } = require("./util-server"); const { setSetting, setting } = require("./util-server");
const knex = require("knex");
class Database { class Database {

View File

@ -4,7 +4,7 @@
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<h5 id="exampleModalLabel" class="modal-title"> <h5 id="exampleModalLabel" class="modal-title">
Confirm {{ $t("Confirm") }}
</h5> </h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" /> <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" />
</div> </div>
@ -35,7 +35,7 @@ export default {
}, },
yesText: { yesText: {
type: String, type: String,
default: "Yes", default: "Yes", // TODO: No idea what to translate this
}, },
noText: { noText: {
type: String, type: String,

View File

@ -6,12 +6,12 @@
<div class="form-floating"> <div class="form-floating">
<input id="floatingInput" v-model="username" type="text" class="form-control" placeholder="Username"> <input id="floatingInput" v-model="username" type="text" class="form-control" placeholder="Username">
<label for="floatingInput">Username</label> <label for="floatingInput">{{ $t("Username") }}</label>
</div> </div>
<div class="form-floating mt-3"> <div class="form-floating mt-3">
<input id="floatingPassword" v-model="password" type="password" class="form-control" placeholder="Password"> <input id="floatingPassword" v-model="password" type="password" class="form-control" placeholder="Password">
<label for="floatingPassword">Password</label> <label for="floatingPassword">{{ $t("Password") }}</label>
</div> </div>
<div class="form-check mb-3 mt-3 d-flex justify-content-center pe-4"> <div class="form-check mb-3 mt-3 d-flex justify-content-center pe-4">
@ -19,12 +19,12 @@
<input id="remember" v-model="$root.remember" type="checkbox" value="remember-me" class="form-check-input"> <input id="remember" v-model="$root.remember" type="checkbox" value="remember-me" class="form-check-input">
<label class="form-check-label" for="remember"> <label class="form-check-label" for="remember">
Remember me {{ $t("Remember me") }}
</label> </label>
</div> </div>
</div> </div>
<button class="w-100 btn btn-primary" type="submit" :disabled="processing"> <button class="w-100 btn btn-primary" type="submit" :disabled="processing">
Login {{ $t("Login") }}
</button> </button>
<div v-if="res && !res.ok" class="alert alert-danger mt-3" role="alert"> <div v-if="res && !res.ok" class="alert alert-danger mt-3" role="alert">

View File

@ -1,7 +1,7 @@
<template> <template>
<div class="shadow-box list mb-4"> <div class="shadow-box list mb-4">
<div v-if="Object.keys($root.monitorList).length === 0" class="text-center mt-3"> <div v-if="Object.keys($root.monitorList).length === 0" class="text-center mt-3">
No Monitors, please <router-link to="/add">add one</router-link>. {{ $t("No Monitors, please") }} <router-link to="/add">{{ $t("add one") }}</router-link>
</div> </div>
<router-link v-for="(item, index) in sortedMonitorList" :key="index" :to="monitorURL(item.id)" class="item" :class="{ 'disabled': ! item.active }"> <router-link v-for="(item, index) in sortedMonitorList" :key="index" :to="monitorURL(item.id)" class="item" :class="{ 'disabled': ! item.active }">

View File

@ -5,17 +5,17 @@
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<h5 id="exampleModalLabel" class="modal-title"> <h5 id="exampleModalLabel" class="modal-title">
Setup Notification {{ $t("Setup Notification") }}
</h5> </h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" /> <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" />
</div> </div>
<div class="modal-body"> <div class="modal-body">
<div class="mb-3"> <div class="mb-3">
<label for="type" class="form-label">Notification Type</label> <label for="type" class="form-label">{{ $t("Notification Type") }}</label>
<select id="type" v-model="notification.type" class="form-select"> <select id="type" v-model="notification.type" class="form-select">
<option value="telegram">Telegram</option> <option value="telegram">Telegram</option>
<option value="webhook">Webhook</option> <option value="webhook">Webhook</option>
<option value="smtp">Email (SMTP)</option> <option value="smtp">{{ $t("Email") }} (SMTP)</option>
<option value="discord">Discord</option> <option value="discord">Discord</option>
<option value="signal">Signal</option> <option value="signal">Signal</option>
<option value="gotify">Gotify</option> <option value="gotify">Gotify</option>
@ -31,7 +31,7 @@
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label for="name" class="form-label">Friendly Name</label> <label for="name" class="form-label">{{ $t("Friendly Name") }}</label>
<input id="name" v-model="notification.name" type="text" class="form-control" required> <input id="name" v-model="notification.name" type="text" class="form-control" required>
</div> </div>
@ -407,13 +407,13 @@
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button v-if="id" type="button" class="btn btn-danger" :disabled="processing" @click="deleteConfirm"> <button v-if="id" type="button" class="btn btn-danger" :disabled="processing" @click="deleteConfirm">
Delete {{ $t("Delete") }}
</button> </button>
<button type="button" class="btn btn-warning" :disabled="processing" @click="test"> <button type="button" class="btn btn-warning" :disabled="processing" @click="test">
Test {{ $t("Test") }}
</button> </button>
<button type="submit" class="btn btn-primary" :disabled="processing"> <button type="submit" class="btn btn-primary" :disabled="processing">
Save {{ $t("Save") }}
</button> </button>
</div> </div>
</div> </div>

View File

@ -27,18 +27,18 @@ export default {
text() { text() {
if (this.status === 0) { if (this.status === 0) {
return "Down" return this.$t("Down");
} }
if (this.status === 1) { if (this.status === 1) {
return "Up" return this.$t("Up");
} }
if (this.status === 2) { if (this.status === 2) {
return "Pending" return this.$t("Pending");
} }
return "Unknown" return this.$t("Unknown");
}, },
}, },
} }

12
src/languages/en.js Normal file
View File

@ -0,0 +1,12 @@
export default {
languageName: "English",
checkEverySecond: "Check every {0} seconds.",
"Avg.": "Avg. ",
retriesDescription: "Maximum retries before the service is marked as down and a notification is sent",
ignoreTLSError: "Ignore TLS/SSL error for HTTPS websites",
upsideDownModeDescription: "Flip the status upside down. If the service is reachable, it is DOWN.",
maxRedirectDescription: "Maximum number of redirects to follow. Set to 0 to disable redirects.",
acceptedStatusCodesDescription: "Select status codes which are considered as a successful response.",
passwordNotMatchMsg: "The repeat password does not match.",
notificationDescription: "Please assign a notification to monitor(s) to get it to work.",
}

97
src/languages/zh-HK.js Normal file
View File

@ -0,0 +1,97 @@
export default {
languageName: "繁體中文 (香港)",
Settings: "設定",
Dashboard: "錶板",
"New Update": "有更新",
Language: "語言",
Appearance: "外觀",
Theme: "主題",
General: "一般",
Version: "版本",
"Check Update On GitHub": "到 Github 查看更新",
List: "列表",
Add: "新增",
"Add New Monitor": "新增監測器",
"Quick Stats": "綜合數據",
Up: "上線",
Down: "離線",
Pending: "待定",
Unknown: "不明",
Pause: "暫停",
Name: "名稱",
Status: "狀態",
DateTime: "日期時間",
Message: "內容",
"No important events": "沒有重要事件",
Resume: "恢復",
Edit: "編輯",
Delete: "刪除",
Current: "目前",
Uptime: "上線率",
"Cert Exp.": "証書期限",
days: "日",
day: "日",
"-day": "日",
hour: "小時",
"-hour": "小時",
checkEverySecond: "每 {0} 秒檢查一次",
"Avg.": "平均",
Response: "反應時間",
Ping: "反應時間",
"Monitor Type": "監測器類型",
Keyword: "關鍵字",
"Friendly Name": "名稱",
URL: "網址 URL",
Hostname: "Hostname",
Port: "Port",
"Heartbeat Interval": "檢查間距",
Retries: "重試數次確定為離線",
retriesDescription: "重試多少次後才判定為離線及傳送通知。如數值為 0 會即判定為離線及傳送通知。",
Advanced: "進階",
ignoreTLSError: "忽略 TLS/SSL 錯誤",
"Upside Down Mode": "反轉模式",
upsideDownModeDescription: "反轉狀態,如網址是可正常瀏覽,會被判定為 '離線/DOWN'",
"Max. Redirects": "跟隨重新導向 (Redirect) 的次數",
maxRedirectDescription: "設為 0 即不跟蹤",
"Accepted Status Codes": "接受為上線的 HTTP 狀態碼",
acceptedStatusCodesDescription: "可多選",
Save: "儲存",
Notifications: "通知",
"Not available, please setup.": "無法使用,需要設定",
"Setup Notification": "設定通知",
Light: "明亮",
Dark: "暗黑",
Auto: "自動",
"Theme - Heartbeat Bar": "監測器列表 狀態條外觀",
Normal: "一般",
Bottom: "下方",
None: "沒有",
Timezone: "時區",
"Search Engine Visibility": "是否允許搜尋器索引",
"Allow indexing": "允許索引",
"Discourage search engines from indexing site": "不建議搜尋器索引",
"Change Password": "變更密碼",
"Current Password": "目前密碼",
"New Password": "新密碼",
"Repeat New Password": "確認新密碼",
passwordNotMatchMsg: "密碼不一致",
"Update Password": "更新密碼",
"Disable Auth": "取消登入認証",
"Enable Auth": "開啟登入認証",
Logout: "登出",
notificationDescription: "新增後,你需要在監測器裡啟用。",
Leave: "離開",
"I understand, please disable": "我明白,請取消登入認証",
Confirm: "確認",
Yes: "是",
No: "否",
Username: "帳號",
Password: "密碼",
"Remember me": "記住我",
Login: "登入",
"No Monitors, please": "沒有監測器,請",
"add one": "新增",
"Notification Type": "通知類型",
"Email": "電郵",
"Test": "測試",
}

View File

@ -14,18 +14,18 @@
</router-link> </router-link>
<a v-if="hasNewVersion" target="_blank" href="https://github.com/louislam/uptime-kuma/releases" class="btn btn-info me-3"> <a v-if="hasNewVersion" target="_blank" href="https://github.com/louislam/uptime-kuma/releases" class="btn btn-info me-3">
<font-awesome-icon icon="arrow-alt-circle-up" /> New Update <font-awesome-icon icon="arrow-alt-circle-up" /> {{ $t("New Update") }}
</a> </a>
<ul class="nav nav-pills"> <ul class="nav nav-pills">
<li class="nav-item"> <li class="nav-item">
<router-link to="/dashboard" class="nav-link"> <router-link to="/dashboard" class="nav-link">
<font-awesome-icon icon="tachometer-alt" /> Dashboard <font-awesome-icon icon="tachometer-alt" /> {{ $t("Dashboard") }}
</router-link> </router-link>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<router-link to="/settings" class="nav-link"> <router-link to="/settings" class="nav-link">
<font-awesome-icon icon="cog" /> Settings <font-awesome-icon icon="cog" /> {{ $t("Settings") }}
</router-link> </router-link>
</li> </li>
</ul> </ul>
@ -48,8 +48,8 @@
<footer> <footer>
<div class="container-fluid"> <div class="container-fluid">
Uptime Kuma - Uptime Kuma -
Version: {{ $root.info.version }} - {{ $t("Version") }}: {{ $root.info.version }} -
<a href="https://github.com/louislam/uptime-kuma/releases" target="_blank" rel="noopener">Check Update On GitHub</a> <a href="https://github.com/louislam/uptime-kuma/releases" target="_blank" rel="noopener">{{ $t("Check Update On GitHub") }}</a>
</div> </div>
</footer> </footer>
@ -58,22 +58,22 @@
<nav v-if="$root.isMobile" class="bottom-nav"> <nav v-if="$root.isMobile" class="bottom-nav">
<router-link to="/dashboard" class="nav-link"> <router-link to="/dashboard" class="nav-link">
<div><font-awesome-icon icon="tachometer-alt" /></div> <div><font-awesome-icon icon="tachometer-alt" /></div>
Dashboard {{ $t("Dashboard") }}
</router-link> </router-link>
<router-link to="/list" class="nav-link"> <router-link to="/list" class="nav-link">
<div><font-awesome-icon icon="list" /></div> <div><font-awesome-icon icon="list" /></div>
List {{ $t("List") }}
</router-link> </router-link>
<router-link to="/add" class="nav-link"> <router-link to="/add" class="nav-link">
<div><font-awesome-icon icon="plus" /></div> <div><font-awesome-icon icon="plus" /></div>
Add {{ $t("Add") }}
</router-link> </router-link>
<router-link to="/settings" class="nav-link"> <router-link to="/settings" class="nav-link">
<div><font-awesome-icon icon="cog" /></div> <div><font-awesome-icon icon="cog" /></div>
Settings {{ $t("Settings") }}
</router-link> </router-link>
</nav> </nav>
</div> </div>

View File

@ -1,5 +1,6 @@
import "bootstrap"; import "bootstrap";
import { createApp, h } from "vue"; import { createApp, h } from "vue";
import { createI18n } from "vue-i18n"
import { createRouter, createWebHistory } from "vue-router"; import { createRouter, createWebHistory } from "vue-router";
import Toast from "vue-toastification"; import Toast from "vue-toastification";
import "vue-toastification/dist/index.css"; import "vue-toastification/dist/index.css";
@ -22,6 +23,9 @@ import List from "./pages/List.vue";
import { appName } from "./util.ts"; import { appName } from "./util.ts";
import en from "./languages/en";
import zhHK from "./languages/zh-HK";
const routes = [ const routes = [
{ {
path: "/", path: "/",
@ -83,6 +87,19 @@ const router = createRouter({
routes, routes,
}) })
const languageList = {
en,
"zh-HK": zhHK,
};
const i18n = createI18n({
locale: localStorage.locale || "en",
fallbackLocale: "en",
silentFallbackWarn: true,
silentTranslationWarn: true,
messages: languageList
});
const app = createApp({ const app = createApp({
mixins: [ mixins: [
socket, socket,
@ -98,7 +115,8 @@ const app = createApp({
render: () => h(App), render: () => h(App),
}) })
app.use(router) app.use(router);
app.use(i18n);
const options = { const options = {
position: "bottom-right", position: "bottom-right",

View File

@ -3,7 +3,7 @@
<div class="row"> <div class="row">
<div v-if="! $root.isMobile" class="col-12 col-md-5 col-xl-4"> <div v-if="! $root.isMobile" class="col-12 col-md-5 col-xl-4">
<div> <div>
<router-link to="/add" class="btn btn-primary mb-3"><font-awesome-icon icon="plus" /> Add New Monitor</router-link> <router-link to="/add" class="btn btn-primary mb-3"><font-awesome-icon icon="plus" /> {{ $t("Add New Monitor") }}</router-link>
</div> </div>
<MonitorList /> <MonitorList />
</div> </div>

View File

@ -2,50 +2,38 @@
<transition name="slide-fade" appear> <transition name="slide-fade" appear>
<div v-if="$route.name === 'DashboardHome'"> <div v-if="$route.name === 'DashboardHome'">
<h1 class="mb-3"> <h1 class="mb-3">
Quick Stats {{ $t("Quick Stats") }}
</h1> </h1>
<div class="shadow-box big-padding text-center"> <div class="shadow-box big-padding text-center">
<div class="row"> <div class="row">
<div class="col"> <div class="col">
<h3>Up</h3> <h3>{{ $t("Up") }}</h3>
<span class="num">{{ stats.up }}</span> <span class="num">{{ stats.up }}</span>
</div> </div>
<div class="col"> <div class="col">
<h3>Down</h3> <h3>{{ $t("Down") }}</h3>
<span class="num text-danger">{{ stats.down }}</span> <span class="num text-danger">{{ stats.down }}</span>
</div> </div>
<div class="col"> <div class="col">
<h3>Unknown</h3> <h3>{{ $t("Unknown") }}</h3>
<span class="num text-secondary">{{ stats.unknown }}</span> <span class="num text-secondary">{{ stats.unknown }}</span>
</div> </div>
<div class="col"> <div class="col">
<h3>Pause</h3> <h3>{{ $t("Pause") }}</h3>
<span class="num text-secondary">{{ stats.pause }}</span> <span class="num text-secondary">{{ stats.pause }}</span>
</div> </div>
</div> </div>
<div v-if="false" class="row">
<div class="col-3">
<h3>Uptime</h3>
<p>(24-hour)</p>
<span class="num" />
</div>
<div class="col-3">
<h3>Uptime</h3>
<p>(30-day)</p>
<span class="num" />
</div>
</div>
</div> </div>
<div class="shadow-box table-shadow-box" style="overflow-x: scroll"> <div class="shadow-box table-shadow-box" style="overflow-x: scroll">
<table class="table table-borderless table-hover"> <table class="table table-borderless table-hover">
<thead> <thead>
<tr> <tr>
<th>Name</th> <th>{{ $t("Name") }}</th>
<th>Status</th> <th>{{ $t("Status") }}</th>
<th>DateTime</th> <th>{{ $t("DateTime") }}</th>
<th>Message</th> <th>{{ $t("Message") }}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@ -58,7 +46,7 @@
<tr v-if="importantHeartBeatList.length === 0"> <tr v-if="importantHeartBeatList.length === 0">
<td colspan="4"> <td colspan="4">
No important events {{ $t("No important events") }}
</td> </td>
</tr> </tr>
</tbody> </tbody>

View File

@ -14,16 +14,16 @@
<div class="functions"> <div class="functions">
<button v-if="monitor.active" class="btn btn-light" @click="pauseDialog"> <button v-if="monitor.active" class="btn btn-light" @click="pauseDialog">
<font-awesome-icon icon="pause" /> Pause <font-awesome-icon icon="pause" /> {{ $t("Pause") }}
</button> </button>
<button v-if="! monitor.active" class="btn btn-primary" @click="resumeMonitor"> <button v-if="! monitor.active" class="btn btn-primary" @click="resumeMonitor">
<font-awesome-icon icon="play" /> Resume <font-awesome-icon icon="play" /> {{ $t("Resume") }}
</button> </button>
<router-link :to=" '/edit/' + monitor.id " class="btn btn-secondary"> <router-link :to=" '/edit/' + monitor.id " class="btn btn-secondary">
<font-awesome-icon icon="edit" /> Edit <font-awesome-icon icon="edit" /> {{ $t("Edit") }}
</router-link> </router-link>
<button class="btn btn-danger" @click="deleteDialog"> <button class="btn btn-danger" @click="deleteDialog">
<font-awesome-icon icon="trash" /> Delete <font-awesome-icon icon="trash" /> {{ $t("Delete") }}
</button> </button>
</div> </div>
@ -31,7 +31,7 @@
<div class="row"> <div class="row">
<div class="col-md-8"> <div class="col-md-8">
<HeartbeatBar :monitor-id="monitor.id" /> <HeartbeatBar :monitor-id="monitor.id" />
<span class="word">Check every {{ monitor.interval }} seconds.</span> <span class="word">{{ $t("checkEverySecond", [ monitor.interval ]) }}</span>
</div> </div>
<div class="col-md-4 text-center"> <div class="col-md-4 text-center">
<span class="badge rounded-pill" :class=" 'bg-' + status.color " style="font-size: 30px">{{ status.text }}</span> <span class="badge rounded-pill" :class=" 'bg-' + status.color " style="font-size: 30px">{{ status.text }}</span>
@ -43,7 +43,7 @@
<div class="row"> <div class="row">
<div class="col"> <div class="col">
<h4>{{ pingTitle }}</h4> <h4>{{ pingTitle }}</h4>
<p>(Current)</p> <p>({{ $t("Current") }})</p>
<span class="num"> <span class="num">
<a href="#" @click.prevent="showPingChartBox = !showPingChartBox"> <a href="#" @click.prevent="showPingChartBox = !showPingChartBox">
<CountUp :value="ping" /> <CountUp :value="ping" />
@ -51,26 +51,26 @@
</span> </span>
</div> </div>
<div class="col"> <div class="col">
<h4>Avg. {{ pingTitle }}</h4> <h4>{{ $t("Avg.") }}{{ pingTitle }}</h4>
<p>(24-hour)</p> <p>(24{{ $t("-hour") }})</p>
<span class="num"><CountUp :value="avgPing" /></span> <span class="num"><CountUp :value="avgPing" /></span>
</div> </div>
<div class="col"> <div class="col">
<h4>Uptime</h4> <h4>{{ $t("Uptime") }}</h4>
<p>(24-hour)</p> <p>(24{{ $t("-hour") }})</p>
<span class="num"><Uptime :monitor="monitor" type="24" /></span> <span class="num"><Uptime :monitor="monitor" type="24" /></span>
</div> </div>
<div class="col"> <div class="col">
<h4>Uptime</h4> <h4>{{ $t("Uptime") }}</h4>
<p>(30-day)</p> <p>(30{{ $t("-day") }})</p>
<span class="num"><Uptime :monitor="monitor" type="720" /></span> <span class="num"><Uptime :monitor="monitor" type="720" /></span>
</div> </div>
<div v-if="certInfo" class="col"> <div v-if="certInfo" class="col">
<h4>Cert Exp.</h4> <h4>{{ $t("Cert Exp.") }}</h4>
<p>(<Datetime :value="certInfo.validTo" date-only />)</p> <p>(<Datetime :value="certInfo.validTo" date-only />)</p>
<span class="num"> <span class="num">
<a href="#" @click.prevent="toggleCertInfoBox = !toggleCertInfoBox">{{ certInfo.daysRemaining }} days</a> <a href="#" @click.prevent="toggleCertInfoBox = !toggleCertInfoBox">{{ certInfo.daysRemaining }} {{ $t("days") }}</a>
</span> </span>
</div> </div>
</div> </div>
@ -132,9 +132,9 @@
<table class="table table-borderless table-hover"> <table class="table table-borderless table-hover">
<thead> <thead>
<tr> <tr>
<th>Status</th> <th>{{ $t("Status") }}</th>
<th>DateTime</th> <th>{{ $t("DateTime") }}</th>
<th>Message</th> <th>{{ $t("Message") }}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@ -146,7 +146,7 @@
<tr v-if="importantHeartBeatList.length === 0"> <tr v-if="importantHeartBeatList.length === 0">
<td colspan="3"> <td colspan="3">
No important events {{ $t("No important events") }}
</td> </td>
</tr> </tr>
</tbody> </tbody>
@ -209,10 +209,9 @@ export default {
pingTitle() { pingTitle() {
if (this.monitor.type === "http") { if (this.monitor.type === "http") {
return "Response" return this.$t("Response");
} }
return this.$t("Ping");
return "Ping"
}, },
monitor() { monitor() {

View File

@ -6,10 +6,10 @@
<div class="shadow-box"> <div class="shadow-box">
<div class="row"> <div class="row">
<div class="col-md-6"> <div class="col-md-6">
<h2 class="mb-2">General</h2> <h2 class="mb-2">{{ $t("General") }}</h2>
<div class="my-3"> <div class="my-3">
<label for="type" class="form-label">Monitor Type</label> <label for="type" class="form-label">{{ $t("Monitor Type") }}</label>
<select id="type" v-model="monitor.type" class="form-select" aria-label="Default select example"> <select id="type" v-model="monitor.type" class="form-select" aria-label="Default select example">
<option value="http"> <option value="http">
HTTP(s) HTTP(s)
@ -21,23 +21,23 @@
Ping Ping
</option> </option>
<option value="keyword"> <option value="keyword">
HTTP(s) - Keyword HTTP(s) - {{ $t("Keyword") }}
</option> </option>
</select> </select>
</div> </div>
<div class="my-3"> <div class="my-3">
<label for="name" class="form-label">Friendly Name</label> <label for="name" class="form-label">{{ $t("Friendly Name") }}</label>
<input id="name" v-model="monitor.name" type="text" class="form-control" required> <input id="name" v-model="monitor.name" type="text" class="form-control" required>
</div> </div>
<div v-if="monitor.type === 'http' || monitor.type === 'keyword' " class="my-3"> <div v-if="monitor.type === 'http' || monitor.type === 'keyword' " class="my-3">
<label for="url" class="form-label">URL</label> <label for="url" class="form-label">{{ $t("URL") }}</label>
<input id="url" v-model="monitor.url" type="url" class="form-control" pattern="https?://.+" required> <input id="url" v-model="monitor.url" type="url" class="form-control" pattern="https?://.+" required>
</div> </div>
<div v-if="monitor.type === 'keyword' " class="my-3"> <div v-if="monitor.type === 'keyword' " class="my-3">
<label for="keyword" class="form-label">Keyword</label> <label for="keyword" class="form-label">{{ $t("Keyword") }}</label>
<input id="keyword" v-model="monitor.keyword" type="text" class="form-control" required> <input id="keyword" v-model="monitor.keyword" type="text" class="form-control" required>
<div class="form-text"> <div class="form-text">
Search keyword in plain html or JSON response and it is case-sensitive Search keyword in plain html or JSON response and it is case-sensitive
@ -45,57 +45,57 @@
</div> </div>
<div v-if="monitor.type === 'port' || monitor.type === 'ping' " class="my-3"> <div v-if="monitor.type === 'port' || monitor.type === 'ping' " class="my-3">
<label for="hostname" class="form-label">Hostname</label> <label for="hostname" class="form-label">{{ $t("Hostname") }}</label>
<input id="hostname" v-model="monitor.hostname" type="text" class="form-control" required> <input id="hostname" v-model="monitor.hostname" type="text" class="form-control" required>
</div> </div>
<div v-if="monitor.type === 'port' " class="my-3"> <div v-if="monitor.type === 'port' " class="my-3">
<label for="port" class="form-label">Port</label> <label for="port" class="form-label">{{ $t("Port") }}</label>
<input id="port" v-model="monitor.port" type="number" class="form-control" required min="0" max="65535" step="1"> <input id="port" v-model="monitor.port" type="number" class="form-control" required min="0" max="65535" step="1">
</div> </div>
<div class="my-3"> <div class="my-3">
<label for="interval" class="form-label">Heartbeat Interval (Every {{ monitor.interval }} seconds)</label> <label for="interval" class="form-label">{{ $t("Heartbeat Interval") }} ({{ $t("checkEverySecond", [ monitor.interval ]) }})</label>
<input id="interval" v-model="monitor.interval" type="number" class="form-control" required min="20" step="1"> <input id="interval" v-model="monitor.interval" type="number" class="form-control" required min="20" step="1">
</div> </div>
<div class="my-3"> <div class="my-3">
<label for="maxRetries" class="form-label">Retries</label> <label for="maxRetries" class="form-label">{{ $t("Retries") }}</label>
<input id="maxRetries" v-model="monitor.maxretries" type="number" class="form-control" required min="0" step="1"> <input id="maxRetries" v-model="monitor.maxretries" type="number" class="form-control" required min="0" step="1">
<div class="form-text"> <div class="form-text">
Maximum retries before the service is marked as down and a notification is sent {{ $t("retriesDescription") }}
</div> </div>
</div> </div>
<h2 class="mt-5 mb-2">Advanced</h2> <h2 class="mt-5 mb-2">{{ $t("Advanced") }}</h2>
<div v-if="monitor.type === 'http' || monitor.type === 'keyword' " class="my-3 form-check"> <div v-if="monitor.type === 'http' || monitor.type === 'keyword' " class="my-3 form-check">
<input id="ignore-tls" v-model="monitor.ignoreTls" class="form-check-input" type="checkbox" value=""> <input id="ignore-tls" v-model="monitor.ignoreTls" class="form-check-input" type="checkbox" value="">
<label class="form-check-label" for="ignore-tls"> <label class="form-check-label" for="ignore-tls">
Ignore TLS/SSL error for HTTPS websites {{ $t("ignoreTLSError") }}
</label> </label>
</div> </div>
<div class="my-3 form-check"> <div class="my-3 form-check">
<input id="upside-down" v-model="monitor.upsideDown" class="form-check-input" type="checkbox"> <input id="upside-down" v-model="monitor.upsideDown" class="form-check-input" type="checkbox">
<label class="form-check-label" for="upside-down"> <label class="form-check-label" for="upside-down">
Upside Down Mode {{ $t("Upside Down Mode") }}
</label> </label>
<div class="form-text"> <div class="form-text">
Flip the status upside down. If the service is reachable, it is DOWN. {{ $t("upsideDownModeDescription") }}
</div> </div>
</div> </div>
<div v-if="monitor.type === 'http' || monitor.type === 'keyword' " class="my-3"> <div v-if="monitor.type === 'http' || monitor.type === 'keyword' " class="my-3">
<label for="maxRedirects" class="form-label">Max. Redirects</label> <label for="maxRedirects" class="form-label">{{ $t("Max. Redirects") }}</label>
<input id="maxRedirects" v-model="monitor.maxredirects" type="number" class="form-control" required min="0" step="1"> <input id="maxRedirects" v-model="monitor.maxredirects" type="number" class="form-control" required min="0" step="1">
<div class="form-text"> <div class="form-text">
Maximum number of redirects to follow. Set to 0 to disable redirects. {{ $t("maxRedirectDescription") }}
</div> </div>
</div> </div>
<div v-if="monitor.type === 'http' || monitor.type === 'keyword' " class="my-3"> <div v-if="monitor.type === 'http' || monitor.type === 'keyword' " class="my-3">
<label for="acceptedStatusCodes" class="form-label">Accepted Status Codes</label> <label for="acceptedStatusCodes" class="form-label">{{ $t("Accepted Status Codes") }}</label>
<VueMultiselect <VueMultiselect
id="acceptedStatusCodes" id="acceptedStatusCodes"
@ -112,21 +112,21 @@
></VueMultiselect> ></VueMultiselect>
<div class="form-text"> <div class="form-text">
Select status codes which are considered as a successful response. {{ $t("acceptedStatusCodesDescription") }}
</div> </div>
</div> </div>
<div class="mt-5 mb-1"> <div class="mt-5 mb-1">
<button class="btn btn-primary" type="submit" :disabled="processing">Save</button> <button class="btn btn-primary" type="submit" :disabled="processing">{{ $t("Save") }}</button>
</div> </div>
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<div v-if="$root.isMobile" class="mt-3" /> <div v-if="$root.isMobile" class="mt-3" />
<h2 class="mb-2">Notifications</h2> <h2 class="mb-2">{{ $t("Notifications") }}</h2>
<p v-if="$root.notificationList.length === 0"> <p v-if="$root.notificationList.length === 0">
Not available, please setup. {{ $t("Not available, please setup.") }}
</p> </p>
<div v-for="notification in $root.notificationList" :key="notification.id" class="form-check form-switch my-3"> <div v-for="notification in $root.notificationList" :key="notification.id" class="form-check form-switch my-3">
@ -134,12 +134,12 @@
<label class="form-check-label" :for=" 'notification' + notification.id"> <label class="form-check-label" :for=" 'notification' + notification.id">
{{ notification.name }} {{ notification.name }}
<a href="#" @click="$refs.notificationDialog.show(notification.id)">Edit</a> <a href="#" @click="$refs.notificationDialog.show(notification.id)">{{ $t("Edit") }}</a>
</label> </label>
</div> </div>
<button class="btn btn-primary me-2" type="button" @click="$refs.notificationDialog.show()"> <button class="btn btn-primary me-2" type="button" @click="$refs.notificationDialog.show()">
Setup Notification {{ $t("Setup Notification") }}
</button> </button>
</div> </div>
</div> </div>
@ -175,7 +175,7 @@ export default {
computed: { computed: {
pageName() { pageName() {
return (this.isAdd) ? "Add New Monitor" : "Edit" return this.$t((this.isAdd) ? "Add New Monitor" : "Edit");
}, },
isAdd() { isAdd() {
return this.$route.path === "/add"; return this.$route.path === "/add";

View File

@ -2,53 +2,63 @@
<transition name="slide-fade" appear> <transition name="slide-fade" appear>
<div> <div>
<h1 v-show="show" class="mb-3"> <h1 v-show="show" class="mb-3">
Settings {{ $t("Settings") }}
</h1> </h1>
<div class="shadow-box"> <div class="shadow-box">
<div class="row"> <div class="row">
<div class="col-md-6"> <div class="col-md-6">
<h2 class="mb-2">General</h2> <h2 class="mb-2">{{ $t("Appearance") }}</h2>
<form class="mb-3" @submit.prevent="saveGeneral">
<div class="mb-3"> <div class="mb-3">
<label for="timezone" class="form-label">Theme</label> <label for="language" class="form-label">{{ $t("Language") }}</label>
<select id="language" v-model="$i18n.locale" class="form-select">
<option v-for="(lang, i) in $i18n.availableLocales" :key="`Lang${i}`" :value="lang">
{{ $i18n.messages[lang].languageName }}
</option>
</select>
</div>
<div class="mb-3">
<label for="timezone" class="form-label">{{ $t("Theme") }}</label>
<div> <div>
<div class="btn-group" role="group" aria-label="Basic checkbox toggle button group"> <div class="btn-group" role="group" aria-label="Basic checkbox toggle button group">
<input id="btncheck1" v-model="$root.userTheme" type="radio" class="btn-check" name="theme" autocomplete="off" value="light"> <input id="btncheck1" v-model="$root.userTheme" type="radio" class="btn-check" name="theme" autocomplete="off" value="light">
<label class="btn btn-outline-primary" for="btncheck1">Light</label> <label class="btn btn-outline-primary" for="btncheck1">{{ $t("Light") }}</label>
<input id="btncheck2" v-model="$root.userTheme" type="radio" class="btn-check" name="theme" autocomplete="off" value="dark"> <input id="btncheck2" v-model="$root.userTheme" type="radio" class="btn-check" name="theme" autocomplete="off" value="dark">
<label class="btn btn-outline-primary" for="btncheck2">Dark</label> <label class="btn btn-outline-primary" for="btncheck2">{{ $t("Dark") }}</label>
<input id="btncheck3" v-model="$root.userTheme" type="radio" class="btn-check" name="theme" autocomplete="off" value="auto"> <input id="btncheck3" v-model="$root.userTheme" type="radio" class="btn-check" name="theme" autocomplete="off" value="auto">
<label class="btn btn-outline-primary" for="btncheck3">Auto</label> <label class="btn btn-outline-primary" for="btncheck3">{{ $t("Auto") }}</label>
</div> </div>
</div> </div>
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label class="form-label">Theme - Heartbeat Bar</label> <label class="form-label">{{ $t("Theme - Heartbeat Bar") }}</label>
<div> <div>
<div class="btn-group" role="group" aria-label="Basic checkbox toggle button group"> <div class="btn-group" role="group" aria-label="Basic checkbox toggle button group">
<input id="btncheck4" v-model="$root.userHeartbeatBar" type="radio" class="btn-check" name="heartbeatBarTheme" autocomplete="off" value="normal"> <input id="btncheck4" v-model="$root.userHeartbeatBar" type="radio" class="btn-check" name="heartbeatBarTheme" autocomplete="off" value="normal">
<label class="btn btn-outline-primary" for="btncheck4">Normal</label> <label class="btn btn-outline-primary" for="btncheck4">{{ $t("Normal") }}</label>
<input id="btncheck5" v-model="$root.userHeartbeatBar" type="radio" class="btn-check" name="heartbeatBarTheme" autocomplete="off" value="bottom"> <input id="btncheck5" v-model="$root.userHeartbeatBar" type="radio" class="btn-check" name="heartbeatBarTheme" autocomplete="off" value="bottom">
<label class="btn btn-outline-primary" for="btncheck5">Bottom</label> <label class="btn btn-outline-primary" for="btncheck5">{{ $t("Bottom") }}</label>
<input id="btncheck6" v-model="$root.userHeartbeatBar" type="radio" class="btn-check" name="heartbeatBarTheme" autocomplete="off" value="none"> <input id="btncheck6" v-model="$root.userHeartbeatBar" type="radio" class="btn-check" name="heartbeatBarTheme" autocomplete="off" value="none">
<label class="btn btn-outline-primary" for="btncheck6">None</label> <label class="btn btn-outline-primary" for="btncheck6">{{ $t("None") }}</label>
</div> </div>
</div> </div>
</div> </div>
<h2 class="mt-5 mb-2">{{ $t("General") }}</h2>
<form class="mb-3" @submit.prevent="saveGeneral">
<div class="mb-3"> <div class="mb-3">
<label for="timezone" class="form-label">Timezone</label> <label for="timezone" class="form-label">{{ $t("Timezone") }}</label>
<select id="timezone" v-model="$root.userTimezone" class="form-select"> <select id="timezone" v-model="$root.userTimezone" class="form-select">
<option value="auto"> <option value="auto">
Auto: {{ guessTimezone }} {{ $t("Auto") }}: {{ guessTimezone }}
</option> </option>
<option v-for="(timezone, index) in timezoneList" :key="index" :value="timezone.value"> <option v-for="(timezone, index) in timezoneList" :key="index" :value="timezone.value">
{{ timezone.name }} {{ timezone.name }}
@ -57,65 +67,65 @@
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label class="form-label">Search Engine Visibility</label> <label class="form-label">{{ $t("Search Engine Visibility") }}</label>
<div class="form-check"> <div class="form-check">
<input id="searchEngineIndexYes" v-model="settings.searchEngineIndex" class="form-check-input" type="radio" name="flexRadioDefault" :value="true" required> <input id="searchEngineIndexYes" v-model="settings.searchEngineIndex" class="form-check-input" type="radio" name="flexRadioDefault" :value="true" required>
<label class="form-check-label" for="searchEngineIndexYes"> <label class="form-check-label" for="searchEngineIndexYes">
Allow indexing {{ $t("Allow indexing") }}
</label> </label>
</div> </div>
<div class="form-check"> <div class="form-check">
<input id="searchEngineIndexNo" v-model="settings.searchEngineIndex" class="form-check-input" type="radio" name="flexRadioDefault" :value="false" required> <input id="searchEngineIndexNo" v-model="settings.searchEngineIndex" class="form-check-input" type="radio" name="flexRadioDefault" :value="false" required>
<label class="form-check-label" for="searchEngineIndexNo"> <label class="form-check-label" for="searchEngineIndexNo">
Discourage search engines from indexing site {{ $t("Discourage search engines from indexing site") }}
</label> </label>
</div> </div>
</div> </div>
<div> <div>
<button class="btn btn-primary" type="submit"> <button class="btn btn-primary" type="submit">
Save {{ $t("Save") }}
</button> </button>
</div> </div>
</form> </form>
<template v-if="loaded"> <template v-if="loaded">
<template v-if="! settings.disableAuth"> <template v-if="! settings.disableAuth">
<h2 class="mt-5 mb-2">Change Password</h2> <h2 class="mt-5 mb-2">{{ $t("Change Password") }}</h2>
<form class="mb-3" @submit.prevent="savePassword"> <form class="mb-3" @submit.prevent="savePassword">
<div class="mb-3"> <div class="mb-3">
<label for="current-password" class="form-label">Current Password</label> <label for="current-password" class="form-label">{{ $t("Current Password") }}</label>
<input id="current-password" v-model="password.currentPassword" type="password" class="form-control" required> <input id="current-password" v-model="password.currentPassword" type="password" class="form-control" required>
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label for="new-password" class="form-label">New Password</label> <label for="new-password" class="form-label">{{ $t("New Password") }}</label>
<input id="new-password" v-model="password.newPassword" type="password" class="form-control" required> <input id="new-password" v-model="password.newPassword" type="password" class="form-control" required>
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label for="repeat-new-password" class="form-label">Repeat New Password</label> <label for="repeat-new-password" class="form-label">{{ $t("Repeat New Password") }}</label>
<input id="repeat-new-password" v-model="password.repeatNewPassword" type="password" class="form-control" :class="{ 'is-invalid' : invalidPassword }" required> <input id="repeat-new-password" v-model="password.repeatNewPassword" type="password" class="form-control" :class="{ 'is-invalid' : invalidPassword }" required>
<div class="invalid-feedback"> <div class="invalid-feedback">
The repeat password does not match. {{ $t("passwordNotMatchMsg") }}
</div> </div>
</div> </div>
<div> <div>
<button class="btn btn-primary" type="submit"> <button class="btn btn-primary" type="submit">
Update Password {{ $t("Update Password") }}
</button> </button>
</div> </div>
</form> </form>
</template> </template>
<h2 class="mt-5 mb-2">Advanced</h2> <h2 class="mt-5 mb-2">{{ $t("Advanced") }}</h2>
<div class="mb-3"> <div class="mb-3">
<button v-if="settings.disableAuth" class="btn btn-outline-primary me-1" @click="enableAuth">Enable Auth</button> <button v-if="settings.disableAuth" class="btn btn-outline-primary me-1" @click="enableAuth">{{ $t("Enable Auth") }}</button>
<button v-if="! settings.disableAuth" class="btn btn-primary me-1" @click="confirmDisableAuth">Disable Auth</button> <button v-if="! settings.disableAuth" class="btn btn-primary me-1" @click="confirmDisableAuth">{{ $t("Disable Auth") }}</button>
<button v-if="! settings.disableAuth" class="btn btn-danger me-1" @click="$root.logout">Logout</button> <button v-if="! settings.disableAuth" class="btn btn-danger me-1" @click="$root.logout">{{ $t("Logout") }}</button>
</div> </div>
</template> </template>
</div> </div>
@ -123,23 +133,23 @@
<div class="notification-list col-md-6"> <div class="notification-list col-md-6">
<div v-if="$root.isMobile" class="mt-3" /> <div v-if="$root.isMobile" class="mt-3" />
<h2>Notifications</h2> <h2>{{ $t("Notifications") }}</h2>
<p v-if="$root.notificationList.length === 0"> <p v-if="$root.notificationList.length === 0">
Not available, please setup. {{ $t("Not available, please setup.") }}
</p> </p>
<p v-else> <p v-else>
Please assign a notification to monitor(s) to get it to work. {{ $t("notificationDescription") }}
</p> </p>
<ul class="list-group mb-3" style="border-radius: 1rem;"> <ul class="list-group mb-3" style="border-radius: 1rem;">
<li v-for="(notification, index) in $root.notificationList" :key="index" class="list-group-item"> <li v-for="(notification, index) in $root.notificationList" :key="index" class="list-group-item">
{{ notification.name }}<br> {{ notification.name }}<br>
<a href="#" @click="$refs.notificationDialog.show(notification.id)">Edit</a> <a href="#" @click="$refs.notificationDialog.show(notification.id)">{{ $t("Edit") }}</a>
</li> </li>
</ul> </ul>
<button class="btn btn-primary me-2" type="button" @click="$refs.notificationDialog.show()"> <button class="btn btn-primary me-2" type="button" @click="$refs.notificationDialog.show()">
Setup Notification {{ $t("Setup Notification") }}
</button> </button>
</div> </div>
</div> </div>
@ -147,10 +157,18 @@
<NotificationDialog ref="notificationDialog" /> <NotificationDialog ref="notificationDialog" />
<Confirm ref="confirmDisableAuth" btn-style="btn-danger" yes-text="I understand, please disable" no-text="Leave" @yes="disableAuth"> <Confirm ref="confirmDisableAuth" btn-style="btn-danger" :yes-text="$t('I understand, please disable')" :no-text="$t('Leave')" @yes="disableAuth">
<template v-if="$i18n.locale === 'en' ">
<p>Are you sure want to <strong>disable auth</strong>?</p> <p>Are you sure want to <strong>disable auth</strong>?</p>
<p>It is for <strong>someone who have 3rd-party auth</strong> in front of Uptime Kuma such as Cloudflare Access.</p> <p>It is for <strong>someone who have 3rd-party auth</strong> in front of Uptime Kuma such as Cloudflare Access.</p>
<p>Please use it carefully.</p> <p>Please use it carefully.</p>
</template>
<template v-if="$i18n.locale === 'zh-HK' ">
<p>你是否確認<strong>取消登入認証</strong></p>
<p>這個功能是設計給已有<strong>第三方認証</strong>的用家例如 Cloudflare Access</p>
<p>請小心使用</p>
</template>
</Confirm> </Confirm>
</div> </div>
</transition> </transition>
@ -195,6 +213,10 @@ export default {
"password.repeatNewPassword"() { "password.repeatNewPassword"() {
this.invalidPassword = false; this.invalidPassword = false;
}, },
"$i18n.locale"() {
localStorage.locale = this.$i18n.locale;
},
}, },
mounted() { mounted() {