Skip to content

Commit 27afd01

Browse files
add language selector to desktop onboarding views (#6591)
Allows changing language during desktop onboarding <img width="3816" height="2045" alt="Selection_2231" src="https://github.com/user-attachments/assets/b8a0dda3-70e7-42a9-96f1-10d00e2fd85c" /> ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-6591-add-language-selector-to-desktop-onboarding-views-2a26d73d365081f58d00dca2b2759d82) by [Unito](https://www.unito.io)
1 parent 535f857 commit 27afd01

File tree

4 files changed

+220
-5
lines changed

4 files changed

+220
-5
lines changed
Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
<template>
2+
<Select
3+
:id="dropdownId"
4+
v-model="selectedLocale"
5+
:options="localeOptions"
6+
option-label="label"
7+
option-value="value"
8+
:disabled="isSwitching"
9+
:pt="dropdownPt"
10+
:size="props.size"
11+
class="language-selector"
12+
@change="onLocaleChange"
13+
>
14+
<template #value="{ value }">
15+
<span :class="valueClass">
16+
<i class="pi pi-language" :class="iconClass" />
17+
<span>{{ displayLabel(value as SupportedLocale) }}</span>
18+
</span>
19+
</template>
20+
<template #option="{ option }">
21+
<span :class="optionClass">
22+
<i class="pi pi-language" :class="iconClass" />
23+
<span class="leading-none">{{ option.label }}</span>
24+
</span>
25+
</template>
26+
</Select>
27+
</template>
28+
29+
<script setup lang="ts">
30+
import Select from 'primevue/select'
31+
import type { SelectChangeEvent } from 'primevue/select'
32+
import { computed, ref, watch } from 'vue'
33+
34+
import { i18n, loadLocale, st } from '@/i18n'
35+
36+
type VariantKey = 'dark' | 'light'
37+
type SizeKey = 'small' | 'large'
38+
39+
const props = withDefaults(
40+
defineProps<{
41+
variant?: VariantKey
42+
size?: SizeKey
43+
}>(),
44+
{
45+
variant: 'dark',
46+
size: 'small'
47+
}
48+
)
49+
50+
const dropdownId = `language-select-${Math.random().toString(36).slice(2)}`
51+
52+
const LOCALES = [
53+
['en', 'English'],
54+
['zh', '中文'],
55+
['zh-TW', '繁體中文'],
56+
['ru', 'Русский'],
57+
['ja', '日本語'],
58+
['ko', '한국어'],
59+
['fr', 'Français'],
60+
['es', 'Español'],
61+
['ar', 'عربي'],
62+
['tr', 'Türkçe']
63+
] as const satisfies ReadonlyArray<[string, string]>
64+
65+
type SupportedLocale = (typeof LOCALES)[number][0]
66+
67+
const SIZE_PRESETS = {
68+
large: {
69+
wrapper: 'px-3 py-1 min-w-[7rem]',
70+
gap: 'gap-2',
71+
valueText: 'text-xs',
72+
optionText: 'text-sm',
73+
icon: 'text-sm'
74+
},
75+
small: {
76+
wrapper: 'px-2 py-0.5 min-w-[5rem]',
77+
gap: 'gap-1',
78+
valueText: 'text-[0.65rem]',
79+
optionText: 'text-xs',
80+
icon: 'text-xs'
81+
}
82+
} as const satisfies Record<SizeKey, Record<string, string>>
83+
84+
const VARIANT_PRESETS = {
85+
light: {
86+
root: 'bg-white/80 border border-neutral-200 text-neutral-700 rounded-full shadow-sm backdrop-blur hover:border-neutral-400 transition-colors focus-visible:ring-offset-2 focus-visible:ring-offset-white',
87+
trigger: 'text-neutral-500 hover:text-neutral-700',
88+
item: 'text-neutral-700 bg-transparent hover:bg-neutral-100 focus-visible:outline-none',
89+
valueText: 'text-neutral-600',
90+
optionText: 'text-neutral-600',
91+
icon: 'text-neutral-500'
92+
},
93+
dark: {
94+
root: 'bg-neutral-900/70 border border-neutral-700 text-neutral-200 rounded-full shadow-sm backdrop-blur hover:border-neutral-500 transition-colors focus-visible:ring-offset-2 focus-visible:ring-offset-neutral-900',
95+
trigger: 'text-neutral-400 hover:text-neutral-200',
96+
item: 'text-neutral-200 bg-transparent hover:bg-neutral-800/80 focus-visible:outline-none',
97+
valueText: 'text-neutral-100',
98+
optionText: 'text-neutral-100',
99+
icon: 'text-neutral-300'
100+
}
101+
} as const satisfies Record<VariantKey, Record<string, string>>
102+
103+
const selectedLocale = ref<string>(i18n.global.locale.value)
104+
const isSwitching = ref(false)
105+
106+
const sizePreset = computed(() => SIZE_PRESETS[props.size as SizeKey])
107+
const variantPreset = computed(
108+
() => VARIANT_PRESETS[props.variant as VariantKey]
109+
)
110+
111+
const dropdownPt = computed(() => ({
112+
root: {
113+
class: `${variantPreset.value.root} ${sizePreset.value.wrapper}`
114+
},
115+
trigger: {
116+
class: variantPreset.value.trigger
117+
},
118+
item: {
119+
class: `${variantPreset.value.item} ${sizePreset.value.optionText}`
120+
}
121+
}))
122+
123+
const valueClass = computed(() =>
124+
[
125+
'flex items-center font-medium uppercase tracking-wide leading-tight',
126+
sizePreset.value.gap,
127+
sizePreset.value.valueText,
128+
variantPreset.value.valueText
129+
].join(' ')
130+
)
131+
132+
const optionClass = computed(() =>
133+
[
134+
'flex items-center leading-tight',
135+
sizePreset.value.gap,
136+
variantPreset.value.optionText,
137+
sizePreset.value.optionText
138+
].join(' ')
139+
)
140+
141+
const iconClass = computed(() =>
142+
[sizePreset.value.icon, variantPreset.value.icon].join(' ')
143+
)
144+
145+
const localeOptions = computed(() =>
146+
LOCALES.map(([value, fallback]) => ({
147+
value,
148+
label: st(`settings.Comfy_Locale.options.${value}`, fallback)
149+
}))
150+
)
151+
152+
const labelLookup = computed(() =>
153+
localeOptions.value.reduce<Record<string, string>>((acc, option) => {
154+
acc[option.value] = option.label
155+
return acc
156+
}, {})
157+
)
158+
159+
function displayLabel(locale?: SupportedLocale) {
160+
if (!locale) {
161+
return st('settings.Comfy_Locale.name', 'Language')
162+
}
163+
164+
return labelLookup.value[locale] ?? locale
165+
}
166+
167+
watch(
168+
() => i18n.global.locale.value,
169+
(newLocale) => {
170+
if (newLocale !== selectedLocale.value) {
171+
selectedLocale.value = newLocale
172+
}
173+
}
174+
)
175+
176+
async function onLocaleChange(event: SelectChangeEvent) {
177+
const nextLocale = event.value as SupportedLocale | undefined
178+
179+
if (!nextLocale || nextLocale === i18n.global.locale.value) {
180+
return
181+
}
182+
183+
isSwitching.value = true
184+
try {
185+
await loadLocale(nextLocale)
186+
i18n.global.locale.value = nextLocale
187+
} catch (error) {
188+
console.error(`Failed to change locale to "${nextLocale}"`, error)
189+
selectedLocale.value = i18n.global.locale.value
190+
} finally {
191+
isSwitching.value = false
192+
}
193+
}
194+
</script>
195+
196+
<style scoped>
197+
@reference '../../assets/css/style.css';
198+
199+
:deep(.p-dropdown-panel .p-dropdown-item) {
200+
@apply transition-colors;
201+
}
202+
203+
:deep(.p-dropdown) {
204+
@apply focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-yellow/60 focus-visible:ring-offset-2;
205+
}
206+
</style>

apps/desktop-ui/src/views/MetricsConsentView.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<template>
2-
<BaseViewTemplate dark>
2+
<BaseViewTemplate dark hide-language-selector>
33
<div class="h-full p-8 2xl:p-16 flex flex-col items-center justify-center">
44
<div
55
class="bg-neutral-800 rounded-lg shadow-lg p-6 w-full max-w-[600px] flex flex-col gap-6"

apps/desktop-ui/src/views/WelcomeView.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<template>
22
<BaseViewTemplate dark>
33
<div class="flex items-center justify-center min-h-screen">
4-
<div class="grid grid-rows-2 gap-8">
4+
<div class="grid gap-8">
55
<!-- Top container: Logo -->
66
<div class="flex items-end justify-center">
77
<img

apps/desktop-ui/src/views/templates/BaseViewTemplate.vue

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
<template>
22
<div
3-
class="font-sans w-screen h-screen flex flex-col"
3+
class="font-sans w-screen h-screen flex flex-col relative"
44
:class="[
55
dark
66
? 'text-neutral-300 bg-neutral-900 dark-theme'
77
: 'text-neutral-900 bg-neutral-300'
88
]"
99
>
10+
<div v-if="showLanguageSelector" class="absolute top-6 right-6 z-10">
11+
<LanguageSelector :variant="variant" />
12+
</div>
1013
<!-- Virtual top menu for native window (drag handle) -->
1114
<div
1215
v-show="isNativeWindow()"
@@ -20,14 +23,20 @@
2023
</template>
2124

2225
<script setup lang="ts">
23-
import { nextTick, onMounted, ref } from 'vue'
26+
import { computed, nextTick, onMounted, ref } from 'vue'
27+
28+
import LanguageSelector from '@/components/common/LanguageSelector.vue'
2429
2530
import { electronAPI, isElectron, isNativeWindow } from '../../utils/envUtil'
2631
27-
const { dark = false } = defineProps<{
32+
const { dark = false, hideLanguageSelector = false } = defineProps<{
2833
dark?: boolean
34+
hideLanguageSelector?: boolean
2935
}>()
3036
37+
const variant = computed(() => (dark ? 'dark' : 'light'))
38+
const showLanguageSelector = computed(() => !hideLanguageSelector)
39+
3140
const darkTheme = {
3241
color: 'rgba(0, 0, 0, 0)',
3342
symbolColor: '#d4d4d4'

0 commit comments

Comments
 (0)