Files
codex-proxy-rs/frontend/src/components/AppLanguageMenu.vue
T
ZyyandCodex 3cbebb5f9b feat(frontend): adopt UI v0.8.0 and refine form feedback
Unify theme namespaces and plugin theme delivery, improve validation and empty states, and pin UI v0.8.0 plus plugin workbench v0.9.0.

Co-authored-by: Codex <noreply@openai.com>
2026-11-01 01:40:09 +08:00

86 lines
3.2 KiB
Vue

<script setup lang="ts">
import type { IconButtonProps } from '@codex-proxy/ui'
import type { Locale } from '@/i18n/locale'
import { ZIconButton, ZMenuItem, ZPopover } from '@codex-proxy/ui'
import { Check, Languages, LoaderCircle } from '@lucide/vue'
import chineseFlag from 'circle-flags/flags/cn.svg?url&no-inline'
import englishFlag from 'circle-flags/flags/us.svg?url&no-inline'
import { computed, shallowRef, useTemplateRef } from 'vue'
import { useI18n } from 'vue-i18n'
import { useLocale } from '@/composables/useLocale'
import { notification } from '@/utils/notification'
withDefaults(defineProps<{ size?: IconButtonProps['size'], variant?: IconButtonProps['variant'] }>(), {
size: 'small',
variant: 'text',
})
const { t } = useI18n({ useScope: 'global' })
const { locale, localeOptions, setLocale } = useLocale()
const pendingLocale = shallowRef<Locale>()
const localeFlags: Record<Locale, string> = {
'zh-Hans': chineseFlag,
'zh-Hant': chineseFlag,
'en': englishFlag,
}
const choices = useTemplateRef<HTMLElement>('choices')
const label = computed(() => t('navigation.switchLanguage', {
value: localeOptions.find(option => option.value === locale.value)?.label ?? 'English',
}))
function focusSelected() {
choices.value?.querySelector<HTMLButtonElement>('button[aria-pressed="true"]')?.focus()
}
async function selectLocale(value: Locale, close: () => void) {
pendingLocale.value = value
try {
if (await setLocale(value))
close()
}
catch {
notification.error({
message: t('navigation.languageLoadFailed'),
})
}
finally {
if (pendingLocale.value === value)
pendingLocale.value = undefined
}
}
</script>
<template>
<ZPopover placement="top-end" @opened="focusSelected">
<template #reference="{ id, open }">
<ZIconButton :size="size" :variant="variant" :aria-label="label" :aria-expanded="open" :aria-controls="id">
<Languages :size="size === 'small' ? 18 : 19" />
</ZIconButton>
</template>
<template #default="{ close }">
<div ref="choices" role="group" :aria-label="t('common.language')" class="grid min-w-40 gap-1 p-1.5">
<ZMenuItem
v-for="option in localeOptions"
:key="option.value"
:aria-pressed="option.value === locale"
:disabled="pendingLocale === option.value"
:aria-busy="pendingLocale === option.value || undefined"
class="aria-pressed:bg-z-control-item-bg-active aria-pressed:text-z-primary-text aria-pressed:hover:bg-z-control-item-bg-active"
@click="selectLocale(option.value, close)"
>
<template #icon>
<img :src="localeFlags[option.value]" alt="" aria-hidden="true" class="size-4.5 shrink-0">
</template>
<span class="flex items-center justify-between gap-3">
<span :lang="option.value">{{ option.label }}</span>
<span class="inline-grid size-4 shrink-0 place-items-center" aria-hidden="true">
<LoaderCircle v-if="pendingLocale === option.value" class="size-4 animate-spin motion-reduce:animate-none" />
<Check v-else-if="option.value === locale" class="size-4" />
</span>
</span>
</ZMenuItem>
</div>
</template>
</ZPopover>
</template>