Skip to content

Commit 74da13b

Browse files
committed
feat: add i18n support with 5 locales
1 parent da368c7 commit 74da13b

File tree

15 files changed

+1410
-25
lines changed

15 files changed

+1410
-25
lines changed

.github/workflows/ci.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,6 @@ jobs:
3131

3232
- name: Typecheck
3333
run: pnpm run typecheck
34+
35+
- name: Test
36+
run: pnpm test

app/pages/index.vue

Lines changed: 41 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -36,18 +36,50 @@ function removeCategory(categoryId: string) {
3636
/>
3737
</div>
3838

39+
<!-- Language Selector -->
40+
<DevOnly>
41+
<div right-16 top-16 fixed z-50>
42+
<SelectRoot v-model="$i18n.locale">
43+
<SelectTrigger
44+
outline="~ 1.5 neutral-300"
45+
flex="~ items-center gap-8" shadow-sm font-medium py-8 rounded-8 bg-white cursor-pointer text-f-sm f-px-md
46+
>
47+
<SelectValue placeholder="Language" />
48+
<Icon name="i-tabler:chevron-down" />
49+
</SelectTrigger>
50+
<SelectContent
51+
position="popper" outline="~ 1.5 neutral-200"
52+
rounded-8 bg-white max-h-256 shadow z-50 of-auto
53+
>
54+
<SelectViewport f-p-xs>
55+
<SelectItem
56+
v-for="locale in $i18n.locales.value"
57+
:key="locale.code"
58+
:value="locale.code"
59+
flex="~ items-center gap-8" text="f-sm neutral-800 data-[highlighted]:neutral-900"
60+
bg="data-[highlighted]:neutral-50" py-10 outline-none rounded-6 cursor-pointer
61+
transition-colors f-px-md
62+
>
63+
{{ locale.name }}
64+
</SelectItem>
65+
</SelectViewport>
66+
</SelectContent>
67+
</SelectRoot>
68+
</div>
69+
</DevOnly>
70+
3971
<div mx-auto max-w-640 relative z-1 f-px-md>
4072
<div f-mb-2xl>
4173
<h1 text="neutral-900 f-2xl" font-bold f-mb-xs>
42-
Spend your crypto in Lugano
74+
{{ $t('hero.title') }}
4375
</h1>
4476
<p text="neutral-600 f-md" f-mb-lg>
45-
Discover places that accept cryptocurrency payments
77+
{{ $t('hero.subtitle') }}
4678
</p>
4779

4880
<ComboboxRoot v-model="selectedCategories" multiple>
4981
<ComboboxAnchor w-full>
50-
<ComboboxInput v-model="searchQuery" placeholder="Search locations or add category filters..." nq-input-box :display-value="() => searchQuery" @focus="refreshCategories" />
82+
<ComboboxInput v-model="searchQuery" :placeholder="$t('search.placeholder')" nq-input-box :display-value="() => searchQuery" @focus="refreshCategories" />
5183
</ComboboxAnchor>
5284

5385
<ComboboxContent position="popper" bg="white" outline="~ 1.5 neutral-200" rounded-t-8 max-h-256 w-full shadow z-50 of-auto>
@@ -57,7 +89,7 @@ function removeCategory(categoryId: string) {
5789
{{ category.name }}
5890
</ComboboxItem>
5991
<ComboboxEmpty v-if="!categories?.length" f-p-md text="f-sm neutral-500 center">
60-
No categories found
92+
{{ $t('search.noCategoriesFound') }}
6193
</ComboboxEmpty>
6294
</ComboboxViewport>
6395
</ComboboxContent>
@@ -87,7 +119,7 @@ function removeCategory(categoryId: string) {
87119
text="14 neutral-800 hocus:neutral"
88120
font-medium py-4 rounded-full cursor-pointer transition-colors f-px-2xs
89121
>
90-
Open now
122+
{{ $t('filters.openNow') }}
91123
</ToggleGroupItem>
92124
<ToggleGroupItem
93125
value="walkable"
@@ -96,7 +128,7 @@ function removeCategory(categoryId: string) {
96128
text="14 neutral-800 hocus:neutral"
97129
font-medium py-4 rounded-full cursor-pointer transition-colors f-px-2xs
98130
>
99-
Walkable distance
131+
{{ $t('filters.walkableDistance') }}
100132
</ToggleGroupItem>
101133
</ToggleGroupRoot>
102134
</div>
@@ -161,7 +193,7 @@ function removeCategory(categoryId: string) {
161193
:to="location.website"
162194
target="_blank" external w-fit f-mt-sm nq-arrow nq-pill-blue
163195
>
164-
Visit Website
196+
{{ $t('location.visitWebsite') }}
165197
</NuxtLink>
166198
</div>
167199
</div>
@@ -175,10 +207,10 @@ function removeCategory(categoryId: string) {
175207
shadow-md text-center rounded-12 f-py-2xl f-mt-xl
176208
>
177209
<p text="neutral-500" font-medium f-text-lg>
178-
No locations found
210+
{{ $t('empty.title') }}
179211
</p>
180212
<p text="neutral-400 f-sm" f-mt-xs>
181-
Try adjusting your search or filters
213+
{{ $t('empty.subtitle') }}
182214
</p>
183215
</div>
184216
</div>

i18n/locales/de.json

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{
2+
"hero": {
3+
"title": "Gib deine Krypto in Lugano aus",
4+
"subtitle": "Entdecke Orte, die Kryptowährungszahlungen akzeptieren"
5+
},
6+
"search": {
7+
"placeholder": "Suche Standorte oder füge Kategoriefilter hinzu...",
8+
"noCategoriesFound": "Keine Kategorien gefunden"
9+
},
10+
"filters": {
11+
"openNow": "Jetzt geöffnet",
12+
"walkableDistance": "Fußläufige Entfernung"
13+
},
14+
"location": {
15+
"visitWebsite": "Website besuchen"
16+
},
17+
"empty": {
18+
"title": "Keine Standorte gefunden",
19+
"subtitle": "Versuche, deine Suche oder Filter anzupassen"
20+
}
21+
}

i18n/locales/en.json

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{
2+
"hero": {
3+
"title": "Spend your crypto in Lugano",
4+
"subtitle": "Discover places that accept cryptocurrency payments"
5+
},
6+
"search": {
7+
"placeholder": "Search locations or add category filters...",
8+
"noCategoriesFound": "No categories found"
9+
},
10+
"filters": {
11+
"openNow": "Open now",
12+
"walkableDistance": "Walkable distance"
13+
},
14+
"location": {
15+
"visitWebsite": "Visit Website"
16+
},
17+
"empty": {
18+
"title": "No locations found",
19+
"subtitle": "Try adjusting your search or filters"
20+
}
21+
}

i18n/locales/es.json

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{
2+
"hero": {
3+
"title": "Gasta tus criptomonedas en Lugano",
4+
"subtitle": "Descubre lugares que aceptan pagos con criptomonedas"
5+
},
6+
"search": {
7+
"placeholder": "Busca ubicaciones o agrega filtros de categoría...",
8+
"noCategoriesFound": "No se encontraron categorías"
9+
},
10+
"filters": {
11+
"openNow": "Abierto ahora",
12+
"walkableDistance": "Distancia caminable"
13+
},
14+
"location": {
15+
"visitWebsite": "Visitar sitio web"
16+
},
17+
"empty": {
18+
"title": "No se encontraron ubicaciones",
19+
"subtitle": "Intenta ajustar tu búsqueda o filtros"
20+
}
21+
}

i18n/locales/fr.json

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{
2+
"hero": {
3+
"title": "Dépensez vos cryptos à Lugano",
4+
"subtitle": "Découvrez des endroits qui acceptent les paiements en cryptomonnaies"
5+
},
6+
"search": {
7+
"placeholder": "Rechercher des emplacements ou ajouter des filtres de catégorie...",
8+
"noCategoriesFound": "Aucune catégorie trouvée"
9+
},
10+
"filters": {
11+
"openNow": "Ouvert maintenant",
12+
"walkableDistance": "Distance à pied"
13+
},
14+
"location": {
15+
"visitWebsite": "Visiter le site web"
16+
},
17+
"empty": {
18+
"title": "Aucun emplacement trouvé",
19+
"subtitle": "Essayez d'ajuster votre recherche ou vos filtres"
20+
}
21+
}

i18n/locales/pt.json

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{
2+
"hero": {
3+
"title": "Gaste suas criptomoedas em Lugano",
4+
"subtitle": "Descubra lugares que aceitam pagamentos com criptomoedas"
5+
},
6+
"search": {
7+
"placeholder": "Pesquisar locais ou adicionar filtros de categoria...",
8+
"noCategoriesFound": "Nenhuma categoria encontrada"
9+
},
10+
"filters": {
11+
"openNow": "Aberto agora",
12+
"walkableDistance": "Distância a pé"
13+
},
14+
"location": {
15+
"visitWebsite": "Visitar site"
16+
},
17+
"empty": {
18+
"title": "Nenhum local encontrado",
19+
"subtitle": "Tente ajustar sua pesquisa ou filtros"
20+
}
21+
}

nuxt.config.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export default defineNuxtConfig({
1414
'@nuxt/icon',
1515
'reka-ui/nuxt',
1616
'@nuxt/image',
17+
'@nuxtjs/i18n',
1718
],
1819
hub: {
1920
database: true,
@@ -49,6 +50,19 @@ export default defineNuxtConfig({
4950
icon: {
5051
collections: ['tabler'],
5152
customCollections: [nimiqIcons],
53+
clientBundle: {
54+
sizeLimitKb: 512, // 512KB
55+
},
56+
},
57+
i18n: {
58+
locales: [
59+
{ code: 'en', language: 'en-US', name: 'English', file: 'en.json' },
60+
{ code: 'es', language: 'es-ES', name: 'Español', file: 'es.json' },
61+
{ code: 'de', language: 'de-DE', name: 'Deutsch', file: 'de.json' },
62+
{ code: 'fr', language: 'fr-FR', name: 'Français', file: 'fr.json' },
63+
{ code: 'pt', language: 'pt-PT', name: 'Português', file: 'pt.json' },
64+
],
65+
defaultLocale: 'en',
5266
},
5367
compatibilityDate: '2025-10-01',
5468
})

package.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,15 @@
1616
"db:generate": "drizzle-kit generate",
1717
"lint": "eslint . --cache",
1818
"lint:fix": "eslint . --fix --cache",
19+
"test": "vitest run",
1920
"typecheck": "nuxt typecheck"
2021
},
2122
"dependencies": {
2223
"@nuxt/fonts": "catalog:",
2324
"@nuxt/icon": "catalog:",
2425
"@nuxt/image": "catalog:",
2526
"@nuxthub/core": "catalog:",
27+
"@nuxtjs/i18n": "catalog:",
2628
"@vueuse/nuxt": "catalog:",
2729
"consola": "catalog:",
2830
"drizzle-orm": "catalog:",
@@ -36,15 +38,20 @@
3638
"devDependencies": {
3739
"@antfu/eslint-config": "catalog:",
3840
"@nuxt/eslint": "catalog:",
41+
"@nuxt/test-utils": "catalog:",
3942
"@unocss/eslint-plugin": "catalog:",
4043
"@unocss/nuxt": "catalog:",
4144
"@unocss/preset-attributify": "catalog:",
45+
"@vue/test-utils": "catalog:",
4246
"drizzle-kit": "catalog:",
4347
"eslint": "catalog:",
4448
"eslint-plugin-format": "catalog:",
49+
"happy-dom": "catalog:",
4550
"nimiq-css": "catalog:",
51+
"playwright-core": "catalog:",
4652
"typescript": "catalog:",
4753
"unocss-preset-onmax": "catalog:",
54+
"vitest": "catalog:",
4855
"wrangler": "catalog:"
4956
}
5057
}

plugins/i18n-locale.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
export default defineNuxtPlugin(async () => {
2+
const { setLocale, locales } = useI18n()
3+
const route = useRoute()
4+
5+
// Get available locale codes
6+
const availableLocales = (locales.value as { code: string }[]).map(l => l.code)
7+
8+
// Check for locale in query param
9+
const queryLocale = route.query.locale as string | undefined
10+
11+
if (queryLocale && availableLocales.includes(queryLocale)) {
12+
await setLocale(queryLocale)
13+
}
14+
})

0 commit comments

Comments
 (0)