Skip to content

Commit 94b5cda

Browse files
committed
feat: enhance language switcher component and update locale configurations with partial translations
1 parent 1af1654 commit 94b5cda

3 files changed

Lines changed: 115 additions & 22 deletions

File tree

README.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,38 @@ Then sign in with:
144144

145145
---
146146

147+
### Updating to a new release
148+
149+
When a new version of Reqcore is released, follow these steps **in order** to update your instance. Your data is safe — updates never delete the database or your uploaded files.
150+
151+
**Step 1 — Pull the latest code**
152+
153+
```bash
154+
git pull origin main
155+
```
156+
157+
**Step 2 — Rebuild and restart the app**
158+
159+
```bash
160+
docker compose up --build -d
161+
```
162+
163+
This rebuilds the app image with the new code, applies any new database migrations automatically on startup, and restarts in the background. The whole process typically takes under a minute.
164+
165+
**Step 3 — Verify it's running**
166+
167+
```bash
168+
docker compose logs app --tail 20
169+
```
170+
171+
Look for `Listening on http://[::]:3000`. Then open [http://localhost:3000](http://localhost:3000) — you're on the latest version.
172+
173+
> **Something wrong after an update?** Roll back by running `git checkout <previous-commit>` and then `docker compose up --build -d`.
174+
175+
> **To find the latest release notes**, check the [CHANGELOG](CHANGELOG.md) or [GitHub Releases](https://github.com/reqcore-inc/reqcore/releases).
176+
177+
---
178+
147179
### Managing your instance
148180

149181
```bash

app/components/LanguageSwitcher.vue

Lines changed: 79 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,25 @@
11
<script setup lang="ts">
2+
import { ChevronDown } from 'lucide-vue-next'
3+
24
const route = useRoute()
35
const requestURL = useRequestURL()
46
const { locale, locales, t } = useI18n()
7+
8+
const isOpen = ref(false)
9+
const dropdownRef = ref<HTMLElement | null>(null)
10+
11+
function closeDropdown() {
12+
isOpen.value = false
13+
}
14+
15+
function handleClickOutside(event: MouseEvent) {
16+
if (dropdownRef.value && !dropdownRef.value.contains(event.target as Node)) {
17+
closeDropdown()
18+
}
19+
}
20+
21+
onMounted(() => document.addEventListener('mousedown', handleClickOutside))
22+
onUnmounted(() => document.removeEventListener('mousedown', handleClickOutside))
523
const localePath = useLocalePath()
624
const switchLocalePath = useSwitchLocalePath()
725
type SwitchLocale = Parameters<typeof switchLocalePath>[0]
@@ -49,13 +67,20 @@ function getLocaleFromRouteName(name: RouteName): string | null {
4967
return parts[1] ?? null
5068
}
5169
70+
type LocaleWithPartial = { code?: string | null, partial?: boolean }
71+
5272
const localeOptions = computed(() => {
5373
return locales.value
54-
.map(entry => getLocaleCode(entry as LocaleEntry))
55-
.filter((code): code is string => !!code)
56-
.map(code => ({
74+
.map((entry) => {
75+
const code = getLocaleCode(entry as LocaleEntry)
76+
const partial = (entry as LocaleWithPartial).partial === true
77+
return code ? { code, partial } : null
78+
})
79+
.filter((item): item is { code: string, partial: boolean } => !!item)
80+
.map(({ code, partial }) => ({
5781
code,
58-
label: `${localeFlags[code] ?? '🌐'} ${code.toLowerCase()}`,
82+
partial,
83+
flag: localeFlags[code] ?? '🌐',
5984
}))
6085
})
6186
@@ -124,9 +149,12 @@ const showI18nProbe = computed(() => {
124149
const i18nProbeText = computed(() => t('common.language'))
125150
126151
async function handleLocaleChange(nextLocale: string) {
127-
if (!nextLocale || nextLocale === selectedLocaleCode.value) return
152+
if (!nextLocale || nextLocale === selectedLocaleCode.value) {
153+
closeDropdown()
154+
return
155+
}
128156
if (!isSwitchLocale(nextLocale)) return
129-
157+
closeDropdown()
130158
const switchPath = switchLocalePath(nextLocale)
131159
await navigateTo(switchPath || localePath('/'))
132160
}
@@ -142,18 +170,51 @@ async function handleLocaleChange(nextLocale: string) {
142170
{{ i18nProbeText }}
143171
</span>
144172

145-
<select
146-
v-model="selectedLocaleCode"
147-
:aria-label="t('common.selectLanguage')"
148-
class="h-8 min-w-14 rounded-md border border-surface-300/45 dark:border-surface-700/55 bg-transparent px-2 text-xs font-medium lowercase text-surface-500 dark:text-surface-400 outline-none transition-colors hover:border-surface-400/60 hover:text-surface-700 dark:hover:border-surface-600 dark:hover:text-surface-200 focus:border-brand-500/70 focus:text-surface-800 dark:focus:text-surface-100"
149-
>
150-
<option
151-
v-for="option in localeOptions"
152-
:key="option.code"
153-
:value="option.code"
173+
<div ref="dropdownRef" class="relative">
174+
<!-- Trigger button -->
175+
<button
176+
type="button"
177+
:aria-label="t('common.selectLanguage')"
178+
:aria-expanded="isOpen"
179+
aria-haspopup="listbox"
180+
class="flex h-8 items-center gap-1 rounded-md border border-surface-300/45 dark:border-surface-700/55 bg-transparent px-2 text-xs font-medium lowercase text-surface-500 dark:text-surface-400 outline-none transition-colors hover:border-surface-400/60 hover:text-surface-700 dark:hover:border-surface-600 dark:hover:text-surface-200 focus:border-brand-500/70 focus:text-surface-800 dark:focus:text-surface-100"
181+
@click="isOpen = !isOpen"
182+
>
183+
<span>{{ localeOptions.find(o => o.code === selectedLocaleCode)?.flag ?? '🌐' }} {{ selectedLocaleCode }}</span>
184+
<ChevronDown class="size-3 opacity-60 transition-transform duration-150" :class="{ 'rotate-180': isOpen }" />
185+
</button>
186+
187+
<!-- Dropdown list -->
188+
<ul
189+
v-if="isOpen"
190+
role="listbox"
191+
:aria-label="t('common.selectLanguage')"
192+
class="absolute right-0 z-50 mt-1 min-w-40 rounded-md border border-surface-200 dark:border-surface-700 bg-white dark:bg-surface-900 shadow-lg py-1 text-xs"
154193
>
155-
{{ option.label }}
156-
</option>
157-
</select>
194+
<li
195+
v-for="option in localeOptions"
196+
:key="option.code"
197+
role="option"
198+
:aria-selected="option.code === selectedLocaleCode"
199+
class="flex cursor-pointer items-center justify-between gap-2 px-3 py-1.5 transition-colors"
200+
:class="option.code === selectedLocaleCode
201+
? 'bg-surface-100 dark:bg-surface-800 text-surface-900 dark:text-surface-100'
202+
: 'text-surface-600 dark:text-surface-300 hover:bg-surface-50 dark:hover:bg-surface-800'"
203+
@click="handleLocaleChange(option.code)"
204+
>
205+
<span class="flex items-center gap-1.5">
206+
<span>{{ option.flag }}</span>
207+
<span class="font-medium">{{ option.code }}</span>
208+
</span>
209+
<span
210+
v-if="option.partial"
211+
class="rounded bg-amber-100 dark:bg-amber-900/40 px-1 py-0.5 text-[10px] font-semibold uppercase tracking-wide text-amber-700 dark:text-amber-400"
212+
title="Translation incomplete"
213+
>
214+
partial
215+
</span>
216+
</li>
217+
</ul>
218+
</div>
158219
</div>
159220
</template>

nuxt.config.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,11 @@ const siteUrl = process.env.NUXT_PUBLIC_SITE_URL || 'https://reqcore.com'
77
const i18nDefaultLocale = 'en'
88
const i18nLocales = [
99
{ code: 'en', language: 'en-US', name: 'English', file: 'en.json' },
10-
{ code: 'es', language: 'es-ES', name: 'Español', file: 'es.json' },
11-
{ code: 'fr', language: 'fr-FR', name: 'Français', file: 'fr.json' },
12-
{ code: 'de', language: 'de-DE', name: 'Deutsch', file: 'de.json' },
10+
{ code: 'es', language: 'es-ES', name: 'Español', file: 'es.json', partial: true },
11+
{ code: 'fr', language: 'fr-FR', name: 'Français', file: 'fr.json', partial: true },
12+
{ code: 'de', language: 'de-DE', name: 'Deutsch', file: 'de.json', partial: true },
1313
{ code: 'nb', language: 'nb-NO', name: 'Norsk Bokmål', file: 'nb.json' },
14-
{ code: 'vi', language: 'vi-VN', name: 'Tiếng Việt', file: 'vi.json' },
14+
{ code: 'vi', language: 'vi-VN', name: 'Tiếng Việt', file: 'vi.json', partial: true },
1515
]
1616

1717
const localizedPublicRouteRules = Object.fromEntries(

0 commit comments

Comments
 (0)