Add vue-i18n
This commit is contained in:
parent
dfec9fb1de
commit
0da2281df0
10 changed files with 227 additions and 52 deletions
83
package-lock.json
generated
83
package-lock.json
generated
|
@ -14,6 +14,7 @@
|
|||
"base32-encode": "^2.0.0",
|
||||
"bulma": "^0.9.4",
|
||||
"vue": "^3.3.4",
|
||||
"vue-i18n": "^9.2.2",
|
||||
"vue-qrcode-reader": "^5.1.0",
|
||||
"vue-router": "^4.2.4"
|
||||
},
|
||||
|
@ -386,6 +387,63 @@
|
|||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@intlify/core-base": {
|
||||
"version": "9.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-9.2.2.tgz",
|
||||
"integrity": "sha512-JjUpQtNfn+joMbrXvpR4hTF8iJQ2sEFzzK3KIESOx+f+uwIjgw20igOyaIdhfsVVBCds8ZM64MoeNSx+PHQMkA==",
|
||||
"dependencies": {
|
||||
"@intlify/devtools-if": "9.2.2",
|
||||
"@intlify/message-compiler": "9.2.2",
|
||||
"@intlify/shared": "9.2.2",
|
||||
"@intlify/vue-devtools": "9.2.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14"
|
||||
}
|
||||
},
|
||||
"node_modules/@intlify/devtools-if": {
|
||||
"version": "9.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@intlify/devtools-if/-/devtools-if-9.2.2.tgz",
|
||||
"integrity": "sha512-4ttr/FNO29w+kBbU7HZ/U0Lzuh2cRDhP8UlWOtV9ERcjHzuyXVZmjyleESK6eVP60tGC9QtQW9yZE+JeRhDHkg==",
|
||||
"dependencies": {
|
||||
"@intlify/shared": "9.2.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14"
|
||||
}
|
||||
},
|
||||
"node_modules/@intlify/message-compiler": {
|
||||
"version": "9.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-9.2.2.tgz",
|
||||
"integrity": "sha512-IUrQW7byAKN2fMBe8z6sK6riG1pue95e5jfokn8hA5Q3Bqy4MBJ5lJAofUsawQJYHeoPJ7svMDyBaVJ4d0GTtA==",
|
||||
"dependencies": {
|
||||
"@intlify/shared": "9.2.2",
|
||||
"source-map": "0.6.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14"
|
||||
}
|
||||
},
|
||||
"node_modules/@intlify/shared": {
|
||||
"version": "9.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-9.2.2.tgz",
|
||||
"integrity": "sha512-wRwTpsslgZS5HNyM7uDQYZtxnbI12aGiBZURX3BTR9RFIKKRWpllTsgzHWvj3HKm3Y2Sh5LPC1r0PDCKEhVn9Q==",
|
||||
"engines": {
|
||||
"node": ">= 14"
|
||||
}
|
||||
},
|
||||
"node_modules/@intlify/vue-devtools": {
|
||||
"version": "9.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@intlify/vue-devtools/-/vue-devtools-9.2.2.tgz",
|
||||
"integrity": "sha512-+dUyqyCHWHb/UcvY1MlIpO87munedm3Gn6E9WWYdWrMuYLcoIoOEVDWSS8xSwtlPU+kA+MEQTP6Q1iI/ocusJg==",
|
||||
"dependencies": {
|
||||
"@intlify/core-base": "9.2.2",
|
||||
"@intlify/shared": "9.2.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/sourcemap-codec": {
|
||||
"version": "1.4.15",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz",
|
||||
|
@ -1003,6 +1061,14 @@
|
|||
"resolved": "https://registry.npmjs.org/sdp/-/sdp-3.2.0.tgz",
|
||||
"integrity": "sha512-d7wDPgDV3DDiqulJjKiV2865wKsJ34YI+NDREbm+FySq6WuKOikwyNQcm+doLAZ1O6ltdO0SeKle2xMpN3Brgw=="
|
||||
},
|
||||
"node_modules/source-map": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/source-map-js": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz",
|
||||
|
@ -1106,6 +1172,23 @@
|
|||
"@vue/shared": "3.3.4"
|
||||
}
|
||||
},
|
||||
"node_modules/vue-i18n": {
|
||||
"version": "9.2.2",
|
||||
"resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-9.2.2.tgz",
|
||||
"integrity": "sha512-yswpwtj89rTBhegUAv9Mu37LNznyu3NpyLQmozF3i1hYOhwpG8RjcjIFIIfnu+2MDZJGSZPXaKWvnQA71Yv9TQ==",
|
||||
"dependencies": {
|
||||
"@intlify/core-base": "9.2.2",
|
||||
"@intlify/shared": "9.2.2",
|
||||
"@intlify/vue-devtools": "9.2.2",
|
||||
"@vue/devtools-api": "^6.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vue": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/vue-qrcode-reader": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/vue-qrcode-reader/-/vue-qrcode-reader-5.1.0.tgz",
|
||||
|
|
|
@ -19,6 +19,7 @@
|
|||
"base32-encode": "^2.0.0",
|
||||
"bulma": "^0.9.4",
|
||||
"vue": "^3.3.4",
|
||||
"vue-i18n": "^9.2.2",
|
||||
"vue-qrcode-reader": "^5.1.0",
|
||||
"vue-router": "^4.2.4"
|
||||
},
|
||||
|
|
|
@ -22,9 +22,9 @@ const toggleBurger = () => {
|
|||
</div>
|
||||
<div class="navbar-menu" :class="{ 'is-active': menuActive }">
|
||||
<div class="navbar-end">
|
||||
<RouterLink to="/add-account" class="navbar-item">New account</RouterLink>
|
||||
<RouterLink to="/manage-accounts" class="navbar-item">Accounts</RouterLink>
|
||||
<RouterLink to="/about" class="navbar-item">About</RouterLink>
|
||||
<RouterLink to="/add-account" class="navbar-item">{{ $t("navbar.addAccount") }}</RouterLink>
|
||||
<RouterLink to="/manage-accounts" class="navbar-item">{{ $t("navbar.manageAccounts") }}</RouterLink>
|
||||
<RouterLink to="/about" class="navbar-item">{{ $t("navbar.about") }}</RouterLink>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
|
64
src/locales/en.json
Normal file
64
src/locales/en.json
Normal file
|
@ -0,0 +1,64 @@
|
|||
{
|
||||
"invariants": {
|
||||
"controls": {
|
||||
"cancel": "Cancel",
|
||||
"close": "Close",
|
||||
"copy": "Copy",
|
||||
"delete": "Delete"
|
||||
}
|
||||
},
|
||||
"navbar": {
|
||||
"addAccount": "New account",
|
||||
"manageAccounts": "Accounts",
|
||||
"about": "About"
|
||||
},
|
||||
"about": {
|
||||
"title": "About",
|
||||
"name": "Sub-Address KEy (SAKE) app",
|
||||
"version": "Version: {version}",
|
||||
"license": "License: {mit} or {apache}",
|
||||
"repository": "Repository: {url}",
|
||||
"close": "@:invariants.controls.close"
|
||||
},
|
||||
"addAccount": {
|
||||
"title": "New account",
|
||||
"localPart": "Local part",
|
||||
"separator": "Separator",
|
||||
"domainName": "Domain name",
|
||||
"privateKey": "Private key",
|
||||
"scan": "Scan",
|
||||
"addAccount": "Add account",
|
||||
"cancel": "@:invariants.controls.cancel",
|
||||
"error": {
|
||||
"invalidBase64": "The key must be a valid base64 string.",
|
||||
"invalidSeparator": "The separator must be a single character.",
|
||||
"cameraNotAllowed": "Camera access permission was not granted.",
|
||||
"cameraNotFound": "No camera detected.",
|
||||
"cameraInsecureContext": "Unable to access the camera through an insecure channel.",
|
||||
"cameraNotReadable": "Camera not accessible (potentially already in use).",
|
||||
"cameraOverconstrained": "Installed cameras are not suitable.",
|
||||
"cameraStreamApiNotSupported": "Stream API is not supported in this browser.",
|
||||
"unknown": "Unknown error."
|
||||
}
|
||||
},
|
||||
"deleteAccount": {
|
||||
"title": "Delete account",
|
||||
"account": "You are about to delete the following account:",
|
||||
"confirm": "Are you sure?",
|
||||
"delete": "@:invariants.controls.delete",
|
||||
"cancel": "@:invariants.controls.cancel"
|
||||
},
|
||||
"main": {
|
||||
"title": "New address",
|
||||
"account": "Account",
|
||||
"name": "Name",
|
||||
"input": "Dedicated name",
|
||||
"address": "Address",
|
||||
"copy": "@:invariants.controls.copy"
|
||||
},
|
||||
"manageAccounts": {
|
||||
"title": "Accounts",
|
||||
"delete": "@:invariants.controls.delete",
|
||||
"close": "@:invariants.controls.close"
|
||||
}
|
||||
}
|
13
src/main.js
13
src/main.js
|
@ -1,9 +1,22 @@
|
|||
import './assets/main.sass';
|
||||
|
||||
import { createApp } from 'vue';
|
||||
import { createI18n } from 'vue-i18n'
|
||||
import App from './App.vue';
|
||||
import router from './router';
|
||||
|
||||
import msg_en from './locales/en.json';
|
||||
|
||||
const messages = {
|
||||
en: msg_en,
|
||||
};
|
||||
const i18n = createI18n({
|
||||
locale: 'en',
|
||||
fallbackLocale: 'en',
|
||||
messages,
|
||||
});
|
||||
|
||||
const app = createApp(App);
|
||||
app.use(router);
|
||||
app.use(i18n);
|
||||
app.mount('#app');
|
||||
|
|
|
@ -16,20 +16,29 @@ const toMainView = () => {
|
|||
|
||||
<template>
|
||||
<LayoutComponent>
|
||||
<h1 class="title is-1">About</h1>
|
||||
<h4 class="subtitle is-4">Sub-Address KEy (SAKE) app</h4>
|
||||
<h1 class="title is-1">{{ $t("about.title") }}</h1>
|
||||
<h4 class="subtitle is-4">{{ $t("about.name") }}</h4>
|
||||
<div class="block">
|
||||
<p>Version {{ version }}</p>
|
||||
<p>
|
||||
License: <ExternalLinkComponent :url="mitLicenseUrl" name="MIT" /> or <ExternalLinkComponent :url="apacheLicenseUrl" name="Apache 2.0" />
|
||||
</p>
|
||||
<p>
|
||||
Repository: <ExternalLinkComponent :url="repoUrl" :name="repoUrl" />
|
||||
</p>
|
||||
<i18n-t keypath="about.version" tag="p">
|
||||
<template v-slot:version>{{ version }}</template>
|
||||
</i18n-t>
|
||||
<i18n-t keypath="about.license" tag="p">
|
||||
<template v-slot:mit>
|
||||
<ExternalLinkComponent :url="mitLicenseUrl" name="MIT" />
|
||||
</template>
|
||||
<template v-slot:apache>
|
||||
<ExternalLinkComponent :url="apacheLicenseUrl" name="Apache 2.0" />
|
||||
</template>
|
||||
</i18n-t>
|
||||
<i18n-t keypath="about.repository" tag="p">
|
||||
<template v-slot:url>
|
||||
<ExternalLinkComponent :url="repoUrl" :name="repoUrl" />
|
||||
</template>
|
||||
</i18n-t>
|
||||
</div>
|
||||
<div class="block">
|
||||
<div class="buttons is-centered">
|
||||
<button class="button is-light" @click="toMainView">Close</button>
|
||||
<button class="button is-light" @click="toMainView">{{ $t("about.close") }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</LayoutComponent>
|
||||
|
|
|
@ -13,7 +13,7 @@ const localPart = ref('');
|
|||
const separator = ref('+');
|
||||
const domainName = ref('');
|
||||
const privateKey = ref('');
|
||||
const errorMessage = ref('');
|
||||
const errorMessageId = ref('');
|
||||
|
||||
const base64Decode = (str_b64) => {
|
||||
try {
|
||||
|
@ -25,7 +25,7 @@ const base64Decode = (str_b64) => {
|
|||
}
|
||||
return b;
|
||||
} catch (e) {
|
||||
throw new Error('The key must be a valid base64 string.');
|
||||
throw new Error('addAccount.error.invalidBase64');
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -43,7 +43,7 @@ const addAccount = () => {
|
|||
if (!addDisabled.value) {
|
||||
try {
|
||||
if (separator.value.length != 1) {
|
||||
throw new Error('The separator must be a single character.');
|
||||
throw new Error('addAccount.error.invalidSeparator');
|
||||
}
|
||||
const key = base64Decode(privateKey.value);
|
||||
const hash = sha256(`${localPart.value}@${domainName.value}`);
|
||||
|
@ -57,7 +57,7 @@ const addAccount = () => {
|
|||
accounts.value.push(newAccount);
|
||||
return toMainView();
|
||||
} catch (e) {
|
||||
errorMessage.value = e.message;
|
||||
errorMessageId.value = e.message;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -74,24 +74,22 @@ const onQrCodeDetected = (result_list) => {
|
|||
}
|
||||
};
|
||||
const onQrCodeError = (err) => {
|
||||
var message = '';
|
||||
if (err.name === 'NotAllowedError') {
|
||||
message = 'Camera access permission was not granted.'
|
||||
setErrorMessage('addAccount.error.cameraNotAllowed');
|
||||
} else if (err.name === 'NotFoundError') {
|
||||
message = 'No camera detected'
|
||||
setErrorMessage('addAccount.error.cameraNotFound');
|
||||
} else if (err.name === 'NotSupportedError' || err.name === 'InsecureContextError') {
|
||||
message = 'Unable to access the camera through an insecure channel.'
|
||||
setErrorMessage('addAccount.error.cameraInsecureContext');
|
||||
} else if (err.name === 'NotReadableError') {
|
||||
message = 'Camera not accessible (potentially already in use).'
|
||||
setErrorMessage('addAccount.error.cameraNotReadable');
|
||||
} else if (err.name === 'OverconstrainedError') {
|
||||
message = 'Installed cameras are not suitable.'
|
||||
setErrorMessage('addAccount.error.cameraOverconstrained');
|
||||
} else if (err.name === 'StreamApiNotSupportedError') {
|
||||
message = 'Stream API is not supported in this browser.'
|
||||
setErrorMessage('addAccount.error.cameraStreamApiNotSupported');
|
||||
} else {
|
||||
message = `${err.name}: ${err.value}`;
|
||||
setErrorMessage('addAccount.error.unknown');
|
||||
console.log(`${err.name}: ${err.value}`);
|
||||
}
|
||||
console.log(message);
|
||||
errorMessage.value = message;
|
||||
};
|
||||
|
||||
// Cancel button
|
||||
|
@ -103,50 +101,57 @@ const toMainView = () => {
|
|||
};
|
||||
|
||||
// Error message
|
||||
const setErrorMessage = (messageId) => {
|
||||
if (messageId.startsWith('addAccount.error.')) {
|
||||
errorMessageId.value = messageId;
|
||||
} else {
|
||||
errorMessageId.value = 'addAccount.error.unknown';
|
||||
}
|
||||
};
|
||||
const resetErrorMessage = () => {
|
||||
errorMessage.value = '';
|
||||
errorMessageId.value = '';
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<LayoutComponent>
|
||||
<h1 class="title is-1">New account</h1>
|
||||
<div class="notification is-danger is-light" v-if="errorMessage">
|
||||
<h1 class="title is-1">{{ $t("addAccount.title") }}</h1>
|
||||
<div class="notification is-danger is-light" v-if="errorMessageId">
|
||||
<button class="delete" @click="resetErrorMessage"></button>
|
||||
{{ errorMessage }}
|
||||
{{ $t(errorMessageId) }}
|
||||
</div>
|
||||
<div class="container" id="new-account-error-msg-container"></div>
|
||||
<div class="field">
|
||||
<label class="label" for="new-addr-local-part">Local part</label>
|
||||
<label class="label" for="new-addr-local-part">{{ $t("addAccount.localPart") }}</label>
|
||||
<div class="control">
|
||||
<input class="input" type="text" id="new-addr-local-part" v-model="localPart">
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label" for="new-addr-separator">Separator</label>
|
||||
<label class="label" for="new-addr-separator">{{ $t("addAccount.separator") }}</label>
|
||||
<div class="control">
|
||||
<input class="input" type="text" id="new-addr-separator" v-model="separator">
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label" for="new-addr-domain">Domain name</label>
|
||||
<label class="label" for="new-addr-domain">{{ $t("addAccount.domainName") }}</label>
|
||||
<div class="control">
|
||||
<input class="input" type="text" id="new-addr-domain" placeholder="example.org" v-model="domainName">
|
||||
</div>
|
||||
</div>
|
||||
<label class="label" for="new-addr-key">Private key</label>
|
||||
<label class="label" for="new-addr-key">{{ $t("addAccount.privateKey") }}</label>
|
||||
<div class="field has-addons">
|
||||
<div class="control is-expanded">
|
||||
<input class="input" type="text" id="new-addr-key" v-model="privateKey">
|
||||
</div>
|
||||
<p class="control">
|
||||
<a class="button is-primary" @click="showQrCodeScanner">Scan</a>
|
||||
<a class="button is-primary" @click="showQrCodeScanner">{{ $t("addAccount.scan") }}</a>
|
||||
</p>
|
||||
</div>
|
||||
<qrcode-stream v-if="scanQrCode" @detect="onQrCodeDetected" @error="onQrCodeError"></qrcode-stream>
|
||||
<div class="buttons is-centered">
|
||||
<button class="button is-primary" :disabled="addDisabled" @click="addAccount">Add account</button>
|
||||
<button class="button is-light" v-if="!cancellDisabled" @click="toMainView">Cancel</button>
|
||||
<button class="button is-primary" :disabled="addDisabled" @click="addAccount">{{ $t("addAccount.addAccount") }}</button>
|
||||
<button class="button is-light" v-if="!cancellDisabled" @click="toMainView">{{ $t("addAccount.cancel") }}</button>
|
||||
</div>
|
||||
</LayoutComponent>
|
||||
</template>
|
||||
|
|
|
@ -19,13 +19,13 @@ const toMainView = () => {
|
|||
|
||||
<template>
|
||||
<LayoutComponent>
|
||||
<h1 class="title is-1">Delete account</h1>
|
||||
<p>You are about to delete the following account:</p>
|
||||
<h1 class="title is-1">{{ $t("deleteAccount.title") }}</h1>
|
||||
<p>{{ $t("deleteAccount.account") }}</p>
|
||||
<p class="has-text-weight-semibold is-size-5">{{ account.localPart }}@{{ account.domain }}</p>
|
||||
<p>Are you sure?</p>
|
||||
<p>{{ $t("deleteAccount.confirm") }}</p>
|
||||
<div class="buttons is-centered">
|
||||
<button class="button is-danger" @click="deleteAccount">Delete</button>
|
||||
<button class="button is-light" @click="toMainView">Cancel</button>
|
||||
<button class="button is-danger" @click="deleteAccount">{{ $t("deleteAccount.delete") }}</button>
|
||||
<button class="button is-light" @click="toMainView">{{ $t("deleteAccount.cancel") }}</button>
|
||||
</div>
|
||||
</LayoutComponent>
|
||||
</template>
|
||||
|
|
|
@ -49,9 +49,9 @@ const copyAddr = () => {
|
|||
<template>
|
||||
<NavBarComponent />
|
||||
<LayoutComponent>
|
||||
<h1 class="title is-1">New address</h1>
|
||||
<h1 class="title is-1">{{ $t("main.title") }}</h1>
|
||||
<div class="field">
|
||||
<label class="label" for="account-name">Account</label>
|
||||
<label class="label" for="account-name">{{ $t("main.account") }}</label>
|
||||
<div class="control">
|
||||
<div class="select is-fullwidth">
|
||||
<select id="account-name" v-model="selectedAccountId">
|
||||
|
@ -61,19 +61,19 @@ const copyAddr = () => {
|
|||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label" for="sub-addr-name">Name</label>
|
||||
<label class="label" for="sub-addr-name">{{ $t("main.name") }}</label>
|
||||
<div class="control">
|
||||
<input class="input" type="text" id="sub-addr-name" placeholder="Text input" v-model="subAddrName">
|
||||
<input class="input" type="text" id="sub-addr-name" :placeholder="$t('main.input')" v-model="subAddrName">
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label" for="generated-addr">Address</label>
|
||||
<label class="label" for="generated-addr">{{ $t("main.address") }}</label>
|
||||
<div class="control">
|
||||
<input class="input" type="text" id="generated-addr" v-model="generatedAddr" disabled>
|
||||
</div>
|
||||
</div>
|
||||
<div class="buttons is-centered">
|
||||
<button class="button is-primary" @click="copyAddr">Copy</button>
|
||||
<button class="button is-primary" @click="copyAddr">{{ $t("main.copy") }}</button>
|
||||
</div>
|
||||
</LayoutComponent>
|
||||
</template>
|
||||
|
|
|
@ -17,7 +17,7 @@ const toMainView = () => {
|
|||
|
||||
<template>
|
||||
<LayoutComponent>
|
||||
<h1 class="title is-1">Accounts</h1>
|
||||
<h1 class="title is-1">{{ $t("manageAccounts.title") }}</h1>
|
||||
<div class="block">
|
||||
<table class="table is-fullwidth">
|
||||
<tbody>
|
||||
|
@ -26,7 +26,7 @@ const toMainView = () => {
|
|||
{{ account.localPart }}@{{ account.domain }}
|
||||
</th>
|
||||
<th>
|
||||
<button class="button is-danger" @click="deleteAccount(account.id)">Delete</button>
|
||||
<button class="button is-danger" @click="deleteAccount(account.id)">{{ $t("manageAccounts.delete") }}</button>
|
||||
</th>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
@ -34,7 +34,7 @@ const toMainView = () => {
|
|||
</div>
|
||||
<div class="block">
|
||||
<div class="buttons is-centered">
|
||||
<button class="button is-light" @click="toMainView">Close</button>
|
||||
<button class="button is-light" @click="toMainView">{{ $t("manageAccounts.close") }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</LayoutComponent>
|
||||
|
|
Loading…
Reference in a new issue