1
0
mirror of https://github.com/amir20/dozzle.git synced 2025-12-24 06:28:42 +01:00

feat: adds a dropdown to switch language locale (#2708)

This commit is contained in:
Amir Raminfar
2024-01-17 17:22:06 -08:00
committed by GitHub
parent ef359d339c
commit 89fe9e6b91
8 changed files with 38 additions and 23 deletions

View File

@@ -69,6 +69,7 @@ declare global {
const isReadonly: typeof import('vue')['isReadonly'] const isReadonly: typeof import('vue')['isReadonly']
const isRef: typeof import('vue')['isRef'] const isRef: typeof import('vue')['isRef']
const lightTheme: typeof import('./stores/settings')['lightTheme'] const lightTheme: typeof import('./stores/settings')['lightTheme']
const locale: typeof import('./stores/settings')['locale']
const logSearchContext: typeof import('./composable/logSearchContext')['logSearchContext'] const logSearchContext: typeof import('./composable/logSearchContext')['logSearchContext']
const makeDestructurable: typeof import('@vueuse/core')['makeDestructurable'] const makeDestructurable: typeof import('@vueuse/core')['makeDestructurable']
const mapActions: typeof import('pinia')['mapActions'] const mapActions: typeof import('pinia')['mapActions']
@@ -423,6 +424,7 @@ declare module 'vue' {
readonly isReadonly: UnwrapRef<typeof import('vue')['isReadonly']> readonly isReadonly: UnwrapRef<typeof import('vue')['isReadonly']>
readonly isRef: UnwrapRef<typeof import('vue')['isRef']> readonly isRef: UnwrapRef<typeof import('vue')['isRef']>
readonly lightTheme: UnwrapRef<typeof import('./stores/settings')['lightTheme']> readonly lightTheme: UnwrapRef<typeof import('./stores/settings')['lightTheme']>
readonly locale: UnwrapRef<typeof import('./stores/settings')['locale']>
readonly makeDestructurable: UnwrapRef<typeof import('@vueuse/core')['makeDestructurable']> readonly makeDestructurable: UnwrapRef<typeof import('@vueuse/core')['makeDestructurable']>
readonly mapActions: UnwrapRef<typeof import('pinia')['mapActions']> readonly mapActions: UnwrapRef<typeof import('pinia')['mapActions']>
readonly mapGetters: UnwrapRef<typeof import('pinia')['mapGetters']> readonly mapGetters: UnwrapRef<typeof import('pinia')['mapGetters']>
@@ -769,6 +771,7 @@ declare module '@vue/runtime-core' {
readonly isReadonly: UnwrapRef<typeof import('vue')['isReadonly']> readonly isReadonly: UnwrapRef<typeof import('vue')['isReadonly']>
readonly isRef: UnwrapRef<typeof import('vue')['isRef']> readonly isRef: UnwrapRef<typeof import('vue')['isRef']>
readonly lightTheme: UnwrapRef<typeof import('./stores/settings')['lightTheme']> readonly lightTheme: UnwrapRef<typeof import('./stores/settings')['lightTheme']>
readonly locale: UnwrapRef<typeof import('./stores/settings')['locale']>
readonly makeDestructurable: UnwrapRef<typeof import('@vueuse/core')['makeDestructurable']> readonly makeDestructurable: UnwrapRef<typeof import('@vueuse/core')['makeDestructurable']>
readonly mapActions: UnwrapRef<typeof import('pinia')['mapActions']> readonly mapActions: UnwrapRef<typeof import('pinia')['mapActions']>
readonly mapGetters: UnwrapRef<typeof import('pinia')['mapGetters']> readonly mapGetters: UnwrapRef<typeof import('pinia')['mapGetters']>

View File

@@ -20,14 +20,11 @@
<transition-group tag="ul" name="list" class="containers menu p-0 [&_li.menu-title]:px-0" v-else> <transition-group tag="ul" name="list" class="containers menu p-0 [&_li.menu-title]:px-0" v-else>
<li <li
v-for="item in menuItems" v-for="item in menuItems"
:key="item.id" :key="isContainer(item) ? item.id : item.keyLabel"
:class="isLabel(item) ? 'menu-title' : item.state" :class="isContainer(item) ? item.state : 'menu-title'"
:data-testid="item.id" :data-testid="isContainer(item) ? null : item.keyLabel"
> >
<template v-if="isLabel(item)"> <popup v-if="isContainer(item)">
{{ item.label }}
</template>
<popup v-else>
<router-link <router-link
:to="{ name: 'container-id', params: { id: item.id } }" :to="{ name: 'container-id', params: { id: item.id } }"
active-class="active-primary" active-class="active-primary"
@@ -51,6 +48,9 @@
<container-popup :container="item"></container-popup> <container-popup :container="item"></container-popup>
</template> </template>
</popup> </popup>
<template v-else>
{{ $t(item.keyLabel) }}
</template>
</li> </li>
</transition-group> </transition-group>
</transition> </transition>
@@ -65,8 +65,6 @@
import { Container } from "@/models/Container"; import { Container } from "@/models/Container";
import { sessionHost } from "@/composable/storage"; import { sessionHost } from "@/composable/storage";
const { t } = useI18n();
const store = useContainerStore(); const store = useContainerStore();
const { activeContainers, visibleContainers, ready } = storeToRefs(store); const { activeContainers, visibleContainers, ready } = storeToRefs(store);
@@ -104,15 +102,13 @@ const groupedContainers = computed(() =>
), ),
); );
type MenuLabel = { label: string; id: string; state: string }; function isContainer(item: any): item is Container {
const pinnedLabel = { label: t("label.pinned"), id: "pinned", state: "label" } as MenuLabel; return item.hasOwnProperty("image");
const allLabel = { label: t("label.containers"), id: "containers", state: "label" } as MenuLabel;
function isLabel(item: Container | MenuLabel): item is MenuLabel {
return (item as MenuLabel).label !== undefined;
} }
const menuItems = computed(() => { const menuItems = computed(() => {
const pinnedLabel = { keyLabel: "label.pinned" };
const allLabel = { keyLabel: "label.containers" };
if (groupedContainers.value.pinned.length > 0) { if (groupedContainers.value.pinned.length > 0) {
return [pinnedLabel, ...groupedContainers.value.pinned, allLabel, ...groupedContainers.value.unpinned]; return [pinnedLabel, ...groupedContainers.value.pinned, allLabel, ...groupedContainers.value.unpinned];
} else { } else {

View File

@@ -1,19 +1,21 @@
import { type App } from "vue"; import { type App } from "vue";
import { createI18n } from "vue-i18n"; import { createI18n } from "vue-i18n";
import { locale } from "@/stores/settings";
import messages from "@intlify/unplugin-vue-i18n/messages"; import messages from "@intlify/unplugin-vue-i18n/messages";
const locale = messages?.hasOwnProperty(navigator.language) ? navigator.language : navigator.language.slice(0, 2); const defaultLocale = messages?.hasOwnProperty(navigator.language)
? navigator.language
: navigator.language.slice(0, 2);
const i18n = createI18n({ const i18n = createI18n({
legacy: false, legacy: false,
locale: locale, locale: locale.value ?? defaultLocale,
fallbackLocale: "en", fallbackLocale: "en",
messages, messages,
}); });
export const install = (app: App) => { syncRefs(locale, i18n.global.locale, { immediate: false });
app.use(i18n);
};
export const install = (app: App) => app.use(i18n);
export default i18n; export default i18n;

View File

@@ -29,6 +29,15 @@
<toggle v-model="softWrap">{{ $t("settings.soft-wrap") }}</toggle> <toggle v-model="softWrap">{{ $t("settings.soft-wrap") }}</toggle>
<labeled-input>
<template #label>
{{ $t("settings.locale") }}
</template>
<template #input>
<dropdown-menu v-model="locale" :options="availableLocales.map((l) => ({ label: l, value: l }))" />
</template>
</labeled-input>
<labeled-input> <labeled-input>
<template #label> <template #label>
{{ $t("settings.datetime-format") }} {{ $t("settings.datetime-format") }}
@@ -134,9 +143,10 @@ import {
size, size,
smallerScrollbars, smallerScrollbars,
softWrap, softWrap,
locale,
} from "@/stores/settings"; } from "@/stores/settings";
const { t } = useI18n(); const { t, availableLocales } = useI18n();
setTitle(t("title.settings")); setTitle(t("title.settings"));
const { latest, hasUpdate } = useReleases(); const { latest, hasUpdate } = useReleases();

View File

@@ -14,6 +14,7 @@ export type Settings = {
softWrap: boolean; softWrap: boolean;
collapseNav: boolean; collapseNav: boolean;
automaticRedirect: boolean; automaticRedirect: boolean;
locale: string | undefined;
}; };
export const DEFAULT_SETTINGS: Settings = { export const DEFAULT_SETTINGS: Settings = {
search: true, search: true,
@@ -29,6 +30,7 @@ export const DEFAULT_SETTINGS: Settings = {
softWrap: true, softWrap: true,
collapseNav: false, collapseNav: false,
automaticRedirect: true, automaticRedirect: true,
locale: undefined,
}; };
export const settings = useProfileStorage("settings", DEFAULT_SETTINGS); export const settings = useProfileStorage("settings", DEFAULT_SETTINGS);
@@ -46,5 +48,6 @@ export const {
menuWidth, menuWidth,
size, size,
search, search,
locale,
automaticRedirect, automaticRedirect,
} = toRefs(settings); } = toRefs(settings);

View File

@@ -34,6 +34,6 @@ test.describe("es locale", () => {
test.use({ locale: "es" }); test.use({ locale: "es" });
test("translated text", async ({ page }) => { test("translated text", async ({ page }) => {
await expect(page.getByTestId("containers")).toHaveText("Contenedores"); await expect(page.getByTestId("label.containers")).toHaveText("Contenedores");
}); });
}); });

View File

@@ -5,5 +5,5 @@ test("simple authentication", async ({ page }) => {
await page.locator('input[name="username"]').fill("admin"); await page.locator('input[name="username"]').fill("admin");
await page.locator('input[name="password"]').fill("password"); await page.locator('input[name="password"]').fill("password");
await page.locator('button[type="submit"]').click(); await page.locator('button[type="submit"]').click();
await expect(page.getByTestId("containers")).toHaveText("Containers"); await expect(page.getByTestId("label.containers")).toHaveText("Containers");
}); });

View File

@@ -65,6 +65,7 @@ placeholder:
search-containers: Search containers (⌘ + k, ⌃k) search-containers: Search containers (⌘ + k, ⌃k)
settings: settings:
display: Display display: Display
locale: Override language
small-scrollbars: Use smaller scrollbars small-scrollbars: Use smaller scrollbars
show-timesamps: Show timestamps show-timesamps: Show timestamps
soft-wrap: Soft wrap lines soft-wrap: Soft wrap lines