Add localization support

This commit is contained in:
Oliver Traber 2023-04-16 21:33:06 +02:00
parent 47b0436b14
commit 91f9b8c4a7
Signed by: Bluemedia
GPG Key ID: C0674B105057136C
15 changed files with 217 additions and 49 deletions

View File

@ -61,3 +61,9 @@ If you want to add new pages, there are some things to be aware of.
- Create a new subfolder for the page in the `views` folder. The name of the new folder must match the name of the FTL file in the [Keycloak base theme](https://github.com/keycloak/keycloak/tree/main/themes/src/main/resources/theme/base/login).
- Copy the three `index.*` files of an existing page into the new folder. The page name also needs to be adjusted in the `index.ftl` file within the attribute `pageId` as well as in the path for the script.
- Add the new page in `webpack.config.js` in the upper part to the list `customPages`.
### Localization
If the browser language is supported, it will be loaded automatically. Otherwise, English is loaded by default.
To add new languages, the corresponding file must be added under `static/login/resources/locales`. Then the language must be added to `SUPPORT_LOCALES` in `functions/i18n.ts`.

View File

@ -46,6 +46,7 @@
},
"dependencies": {
"rfc4648": "^1.5.2",
"vue": "^3.2.26"
"vue": "^3.2.26",
"vue-i18n": "9"
}
}

39
src/functions/i18n.ts Normal file
View File

@ -0,0 +1,39 @@
import { nextTick } from "vue"
import { createI18n } from "vue-i18n"
import { KcContextBase } from "~/types/context";
export const SUPPORT_LOCALES = ["en", "de"]
export function setupI18n(options = {}) {
let browserLocale = navigator.language.split("-")[0];
let defaultLocale = "en";
if (SUPPORT_LOCALES.includes(browserLocale)) {
defaultLocale = browserLocale;
}
const i18n = createI18n(options)
setI18nLanguage(i18n, defaultLocale)
return i18n
}
export function setI18nLanguage(i18n, locale) {
loadLocaleMessages(i18n, locale);
if (i18n.mode === 'legacy') {
i18n.global.locale = locale
} else {
i18n.global.locale.value = locale
}
}
export async function loadLocaleMessages(i18n, locale) {
// load locale messages with dynamic import
let context = (window as any).kcContext as KcContextBase.Common;
const messages = await import(
/* webpackIgnore: true */`${context.url.resourcesPath}/locales/${locale}.js`
)
// set locale and locale message
i18n.global.setLocaleMessage(locale, messages.content)
return nextTick()
}

View File

@ -0,0 +1,37 @@
export const content = {
"login": {
"title": "Mit Ihrem Konto anmelden",
"noAccount": "Sie haben noch kein Konto?",
"signupLink": "Jetzt registrieren.",
"username": "Benutzername",
"usernameOrEmail": "Benutzername oder E-Mail-Adresse",
"password": "Passwort",
"forgotPassword": "Passwort vergessen?",
"rememberMe": "Angemeldet bleiben",
"login": "Anmelden",
"rety": "Erneut versuchen",
"welcome": "Willkommen!"
},
"2fa": {
"selectFactor": "Bitte wählen Sie einen zweiten Faktor aus:",
"hwSecKey": "Hardware Sicherheits-Schlüssel",
"hwSecKeyDesc": "Verwenden Sie ein WebAuthn kompatibles Gerät",
"otp": "Authenticator App",
"otpDesc": "Verwenden Sie einen Einmalcode aus ihrem Authenticator",
"recoveryCode": "Wiederherstellungsschlüssel",
"recoveryCodeDesc": "Verwenden Sie einen ihrer Wiederherstellungsschlüssel",
"tryAnotherWay": "Eine andere Option nutzen."
},
"webauthn": {
"title": "Bitte verwenden Sie eines ihrer Geräte, um fortzufahren:",
"registered": "Registriert: {date}",
"authenticate": "Fortfahren",
"noDevice": "Kein Gerät zur Hand?",
"noSupport": "Es scheint, als würde ihr Browser WebAuthn nicht unterstützen. Bitte nutzen Sie einen anderen Browser für die Anmeldung.",
"noSupportOtherMethod": "Es scheint, als würde ihr Browser WebAuthn nicht unterstützen. Bitte nutzen Sie einen anderen Browser für die Anmeldung, oder wählen Sie eine andere Anmeldeoption.",
"error": "Bei der Anmeldung mit ihrem Gerät ist etwas schief gelaufen. Bitte versuchen Sie es erneut, oder wählen Sie eine andere Anmeldeoption."
},
"redirect": {
"message": "Sie werden weitergeleitet..."
}
}

View File

@ -0,0 +1,37 @@
export const content = {
"login": {
"title": "Login to your account",
"noAccount": "Don't have an account yet?",
"signupLink": "Sign up.",
"username": "Username",
"usernameOrEmail": "Email or Username",
"password": "Password",
"forgotPassword": "Forgot Password?",
"rememberMe": "Remember me",
"login": "Login",
"rety": "Retry",
"welcome": "Welcome!"
},
"2fa": {
"selectFactor": "Please select a second factor you would like to use:",
"hwSecKey": "Hardware Security Key",
"hwSecKeyDesc": "Authenticate using a WebAuthn capable device",
"otp": "Authenticator App",
"otpDesc": "Authenticate using a one time code from your authenticator app",
"recoveryCode": "Recovery Code",
"recoveryCodeDesc": "Authenticate using one of your recovery codes",
"tryAnotherWay": "Try another method."
},
"webauthn": {
"title": "Please use one of your registered devices to continue:",
"registered": "Registered: {date}",
"authenticate": "Continue",
"noDevice": "Don't have your device at hand?",
"noSupport": "It seems that your browser doesn't support WebAuthn. Please try logging in with a different browser.",
"noSupportOtherMethod": "It seems that your browser doesn't support WebAuthn. Please try logging in with a different browser, or try a different login method using the button below.",
"error": "Something went wrong during authentication using your device. Please try again, or use a different login method."
},
"redirect": {
"message": "Redirecting..."
}
}

View File

@ -1,2 +1,2 @@
locales=en,de
locales=en
parent=keycloak

View File

@ -1,7 +1,9 @@
import { createApp } from "vue";
import index from "./index.vue";
import { setupI18n } from "~/functions/i18n";
if ((window as any).kcContext) {
const app = createApp(index);
app.use(setupI18n())
app.mount("#app");
}

View File

@ -1,16 +1,16 @@
<template>
<layout>
<h1 class="mb-8 text-3xl font-semibold text-center text-gray-700">
Login to your account
{{ $t("login.title") }}
</h1>
<p
v-if="context.realm.registrationAllowed"
class="mt-4 text-sm font-light text-center text-gray-700">
Don't have an account yet?
{{ $t("login.noAccount") }}
<a
:href="context.url.registrationUrl"
class="font-medium text-blue-500 hover:underline"
>Sign up</a
>{{ $t("login.signupLink") }}</a
>
</p>
<div v-if="context.message?.type == 'error'">
@ -22,7 +22,7 @@
method="post">
<div>
<label for="username" class="block text-sm text-gray-800">{{
context.realm.loginWithEmailAllowed ? "Email or Username" : "Username"
context.realm.loginWithEmailAllowed ? $t("login.usernameOrEmail") : $t("login.username")
}}</label>
<input
name="username"
@ -38,7 +38,7 @@
<div class="mt-4">
<div>
<label for="password" class="block text-sm text-gray-800"
>Password</label
>{{ $t("login.password") }}</label
>
<input
type="password"
@ -50,7 +50,7 @@
v-if="context.realm.resetPasswordAllowed"
:href="context.url.loginResetCredentialsUrl"
class="text-xs text-gray-600 hover:underline"
>Forgot Password?</a
>{{ $t("login.forgotPassword") }}</a
>
</div>
<div
@ -61,7 +61,7 @@
name="rememberMe"
class="w-4 h-4 text-blue-500 bg-gray-100 border-gray-400 rounded dark:ring-offset-gray-800 dark:bg-gray-700 dark:border-gray-600" />
<label for="rememberMe" class="block ml-3 text-sm text-gray-800"
>Remember me</label
>{{ $t("login.rememberMe") }}</label
>
</div>
<input
@ -72,7 +72,7 @@
" />
<button
class="w-full mt-6 px-4 py-2 tracking-wide text-white transition-colors duration-200 transform bg-blue-500 rounded-md hover:bg-blue-400 focus:outline-none focus:bg-blue-400">
Login
{{ $t("login.login") }}
</button>
</form>
</layout>

View File

@ -1,7 +1,9 @@
import { createApp } from "vue";
import index from "./index.vue";
import { setupI18n } from "~/functions/i18n";
if ((window as any).kcContext) {
const app = createApp(index);
app.use(setupI18n())
app.mount("#app");
}

View File

@ -12,7 +12,7 @@
stroke-dasharray="164 56" />
</svg>
</div>
<h1 class="mt-4 text-xl text-center text-gray-700">Redirecting...</h1>
<h1 class="mt-4 text-xl text-center text-gray-700">{{ $t("redirect.message") }}</h1>
</layout>
</template>
<script lang="ts">

View File

@ -1,7 +1,9 @@
import { createApp } from "vue";
import index from "./index.vue";
import { setupI18n } from "~/functions/i18n";
if ((window as any).kcContext) {
const app = createApp(index);
app.use(setupI18n())
app.mount("#app");
}

View File

@ -1,15 +1,10 @@
<template>
<layout>
<h1
v-if="context.auth.attemptedUsername && context.auth.showUsername"
class="text-3xl font-semibold text-center text-gray-700">
Welcome {{ context.auth?.attemptedUsername }}!
</h1>
<h1 v-else class="text-3xl font-semibold text-center text-gray-700">
Welcome!
<h1 class="text-3xl font-semibold text-center text-gray-700">
{{ $t("login.welcome") }}
</h1>
<p class="text-center mt-3">
Please select a second factor you would like to use:
{{ $t("2fa.selectFactor") }}
</p>
<div class="flex flex-col items-center mt-10 text-gray-700">
<div
@ -25,8 +20,8 @@
d="M6 .5a.5.5 0 0 1 .5-.5h4a.5.5 0 0 1 .5.5v4H6v-4ZM7 1v1h1V1H7Zm2 0v1h1V1H9ZM5.5 5a.5.5 0 0 0-.5.5V15a1 1 0 0 0 1 1h5a1 1 0 0 0 1-1V5.5a.5.5 0 0 0-.5-.5h-6Z" />
</svg>
<div class="flex flex-col ml-5 leading-none">
<h2 class="mb-1.5 font-semibold">Hardware Security Key</h2>
<p class="text-sm">Authenticate using a WebAuthn capable device</p>
<h2 class="mb-1.5 font-semibold">{{ $t("2fa.hwSecKey") }}</h2>
<p class="text-sm">{{ $t("2fa.hwSecKeyDesc") }}</p>
</div>
</div>
<div
@ -46,9 +41,9 @@
d="M7.5 3a.5.5 0 0 1 .5.5v5.21l3.248 1.856a.5.5 0 0 1-.496.868l-3.5-2A.5.5 0 0 1 7 9V3.5a.5.5 0 0 1 .5-.5z" />
</svg>
<div class="flex flex-col ml-5 leading-none">
<h2 class="mb-1.5 font-semibold">Authenticator App</h2>
<h2 class="mb-1.5 font-semibold">{{ $t("2fa.otp") }}</h2>
<p class="text-sm">
Authenticate using a one time code from your authenticator app
{{ $t("2fa.otpDesc") }}
</p>
</div>
</div>
@ -68,8 +63,8 @@
d="M8 4.466V.534a.25.25 0 0 0-.41-.192L5.23 2.308a.25.25 0 0 0 0 .384l2.36 1.966A.25.25 0 0 0 8 4.466z" />
</svg>
<div class="flex flex-col ml-5 leading-none">
<h2 class="mb-1.5 font-semibold">Recovery Code</h2>
<p class="text-sm">Authenticate using one of your recovery codes</p>
<h2 class="mb-1.5 font-semibold">{{ $t("2fa.recoveryCode") }}</h2>
<p class="text-sm">{{ $t("2fa.recoveryCodeDesc") }}</p>
</div>
</div>
</div>

View File

@ -1,7 +1,9 @@
import { createApp } from "vue";
import index from "./index.vue";
import { setupI18n } from "~/functions/i18n";
if ((window as any).kcContext) {
const app = createApp(index);
app.use(setupI18n());
app.mount("#app");
}

View File

@ -1,16 +1,11 @@
<template>
<layout>
<h1
v-if="context.isUserIdentified"
class="text-3xl font-semibold text-center text-gray-700">
Welcome {{ context.auth?.attemptedUsername }}!
</h1>
<h1 v-else class="text-3xl font-semibold text-center text-gray-700">
Welcome!
<h1 class="text-3xl font-semibold text-center text-gray-700">
{{ $t("login.welcome") }}
</h1>
<div v-if="webauthnSupported && !error">
<p class="mt-5 mb-5 text-center">
Please use one of your registered devices to continue:
{{ $t("webauthn.title") }}
</p>
<div
v-for="authenticator in context.authenticators.authenticators"
@ -29,51 +24,48 @@
<div class="w-full ml-5 flex flex-col">
<p>{{ authenticator.label }}</p>
<p class="text-sm">
Registered: {{ new Date(authenticator.createdAt).toLocaleString() }}
{{ $t("webauthn.registered", {date: new Date(authenticator.createdAt).toLocaleString()}) }}
</p>
</div>
</div>
<button
:onclick="prepareAuthenticate"
class="w-full px-4 py-2 tracking-wide text-white transition-colors duration-200 transform bg-blue-500 rounded-md hover:bg-blue-400 focus:outline-none focus:bg-blue-400">
Authenticate
{{ $t("webauthn.authenticate") }}
</button>
<p
class="mt-5 text-sm text-center"
v-if="context.auth?.showTryAnotherWayLink">
Don't have your device at hand?
{{ $t("webauthn.noDevice") }}
<a
:onclick="tryAnotherWay"
class="text-blue-500 hover:underline cursor-pointer"
>Try another way.</a
>{{ $t("2fa.tryAnotherWay") }}</a
>
</p>
</div>
<div v-if="!webauthnSupported">
<p class="mt-5 text-center">
It seems that your browser doesn't support WebAuthn. Please try logging
in with a different browser<span
v-if="!context.auth?.showTryAnotherWayLink"
>.</span
><span v-else
>, or try a different login method using the button below.</span
>
<p v-if="!context.auth?.showTryAnotherWayLink" class="mt-5 text-center">
{{ $t("webauthn.noSupportOtherMethod") }}
</p>
<p v-else class="mt-5 text-center">
{{ $t("webauthn.noSupport") }}
</p>
<button
v-if="context.auth?.showTryAnotherWayLink"
:onclick="tryAnotherWay"
class="w-full mt-5 px-4 py-2 tracking-wide text-white transition-colors duration-200 transform bg-blue-500 rounded-md hover:bg-blue-400 focus:outline-none focus:bg-blue-400">
Try another method
{{ $t("2fa.tryAnotherWay") }}
</button>
</div>
<div v-if="error" class="mt-5 flex flex-col">
<ErrorBox
>Something went wrong during authentication using your device.</ErrorBox
>{{ $t("webauthn.error") }}</ErrorBox
>
<button
:onclick="retryAuth"
class="w-full mt-5 px-4 py-2 tracking-wide text-white transition-colors duration-200 transform bg-blue-500 rounded-md hover:bg-blue-400 focus:outline-none focus:bg-blue-400">
Retry
{{ $t("login.retry") }}
</button>
</div>
</layout>

View File

@ -987,6 +987,44 @@
resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz#b520529ec21d8e5945a1851dfd1c32e94e39ff45"
integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==
"@intlify/core-base@9.2.2":
version "9.2.2"
resolved "https://registry.yarnpkg.com/@intlify/core-base/-/core-base-9.2.2.tgz#5353369b05cc9fe35cab95fe20afeb8a4481f939"
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"
"@intlify/devtools-if@9.2.2":
version "9.2.2"
resolved "https://registry.yarnpkg.com/@intlify/devtools-if/-/devtools-if-9.2.2.tgz#b13d9ac4b4e2fe6d2e7daa556517a8061fe8bd39"
integrity sha512-4ttr/FNO29w+kBbU7HZ/U0Lzuh2cRDhP8UlWOtV9ERcjHzuyXVZmjyleESK6eVP60tGC9QtQW9yZE+JeRhDHkg==
dependencies:
"@intlify/shared" "9.2.2"
"@intlify/message-compiler@9.2.2":
version "9.2.2"
resolved "https://registry.yarnpkg.com/@intlify/message-compiler/-/message-compiler-9.2.2.tgz#e42ab6939b8ae5b3d21faf6a44045667a18bba1c"
integrity sha512-IUrQW7byAKN2fMBe8z6sK6riG1pue95e5jfokn8hA5Q3Bqy4MBJ5lJAofUsawQJYHeoPJ7svMDyBaVJ4d0GTtA==
dependencies:
"@intlify/shared" "9.2.2"
source-map "0.6.1"
"@intlify/shared@9.2.2":
version "9.2.2"
resolved "https://registry.yarnpkg.com/@intlify/shared/-/shared-9.2.2.tgz#5011be9ca2b4ab86f8660739286e2707f9abb4a5"
integrity sha512-wRwTpsslgZS5HNyM7uDQYZtxnbI12aGiBZURX3BTR9RFIKKRWpllTsgzHWvj3HKm3Y2Sh5LPC1r0PDCKEhVn9Q==
"@intlify/vue-devtools@9.2.2":
version "9.2.2"
resolved "https://registry.yarnpkg.com/@intlify/vue-devtools/-/vue-devtools-9.2.2.tgz#b95701556daf7ebb3a2d45aa3ae9e6415aed8317"
integrity sha512-+dUyqyCHWHb/UcvY1MlIpO87munedm3Gn6E9WWYdWrMuYLcoIoOEVDWSS8xSwtlPU+kA+MEQTP6Q1iI/ocusJg==
dependencies:
"@intlify/core-base" "9.2.2"
"@intlify/shared" "9.2.2"
"@jridgewell/gen-mapping@^0.1.0":
version "0.1.1"
resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz#e5d2e450306a9491e3bd77e323e38d7aff315996"
@ -1251,6 +1289,11 @@
"@vue/compiler-dom" "3.2.47"
"@vue/shared" "3.2.47"
"@vue/devtools-api@^6.2.1":
version "6.5.0"
resolved "https://registry.yarnpkg.com/@vue/devtools-api/-/devtools-api-6.5.0.tgz#98b99425edee70b4c992692628fa1ea2c1e57d07"
integrity sha512-o9KfBeaBmCKl10usN4crU53fYtC1r7jJwdGKjPT24t348rHxgfpZ0xL3Xm/gLUYnc0oTp8LAmrxOeLyu6tbk2Q==
"@vue/reactivity-transform@3.2.47":
version "3.2.47"
resolved "https://registry.yarnpkg.com/@vue/reactivity-transform/-/reactivity-transform-3.2.47.tgz#e45df4d06370f8abf29081a16afd25cffba6d84e"
@ -3924,7 +3967,7 @@ source-map-support@~0.5.20:
buffer-from "^1.0.0"
source-map "^0.6.0"
source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.0:
source-map@0.6.1, source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.0:
version "0.6.1"
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
@ -4226,6 +4269,16 @@ vue-eslint-parser@^8.0.1:
lodash "^4.17.21"
semver "^7.3.5"
vue-i18n@9:
version "9.2.2"
resolved "https://registry.yarnpkg.com/vue-i18n/-/vue-i18n-9.2.2.tgz#aeb49d9424923c77e0d6441e3f21dafcecd0e666"
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"
vue-loader@^17.0.0:
version "17.0.1"
resolved "https://registry.yarnpkg.com/vue-loader/-/vue-loader-17.0.1.tgz#c0ee8875e0610a0c2d13ba9b4d50a9c8442e7a3a"