Skip to content

Commit 54f86b2

Browse files
committed
feat: add i18n support with 5 locales
1 parent d6607cd commit 54f86b2

File tree

15 files changed

+1405
-17
lines changed

15 files changed

+1405
-17
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
@@ -72,18 +72,50 @@ function removeCategory(categoryId: string) {
7272
/>
7373
</div>
7474

75+
<!-- Language Selector -->
76+
<DevOnly>
77+
<div right-16 top-16 fixed z-50>
78+
<SelectRoot v-model="$i18n.locale">
79+
<SelectTrigger
80+
outline="~ 1.5 neutral-300"
81+
flex="~ items-center gap-8" shadow-sm font-medium py-8 rounded-8 bg-white cursor-pointer text-f-sm f-px-md
82+
>
83+
<SelectValue placeholder="Language" />
84+
<Icon name="i-tabler:chevron-down" />
85+
</SelectTrigger>
86+
<SelectContent
87+
position="popper" outline="~ 1.5 neutral-200"
88+
rounded-8 bg-white max-h-256 shadow z-50 of-auto
89+
>
90+
<SelectViewport f-p-xs>
91+
<SelectItem
92+
v-for="locale in $i18n.locales.value"
93+
:key="locale.code"
94+
:value="locale.code"
95+
flex="~ items-center gap-8" text="f-sm neutral-800 data-[highlighted]:neutral-900"
96+
bg="data-[highlighted]:neutral-50" py-10 outline-none rounded-6 cursor-pointer
97+
transition-colors f-px-md
98+
>
99+
{{ locale.name }}
100+
</SelectItem>
101+
</SelectViewport>
102+
</SelectContent>
103+
</SelectRoot>
104+
</div>
105+
</DevOnly>
106+
75107
<div mx-auto max-w-640 relative z-1 f-px-md>
76108
<div f-mb-2xl>
77109
<h1 text="neutral-900 f-2xl" font-bold f-mb-xs>
78-
Spend your crypto in Lugano
110+
{{ $t('hero.title') }}
79111
</h1>
80112
<p text="neutral-600 f-md" f-mb-lg>
81-
Discover places that accept cryptocurrency payments
113+
{{ $t('hero.subtitle') }}
82114
</p>
83115

84116
<ComboboxRoot v-model="selectedCategories" multiple>
85117
<ComboboxAnchor w-full>
86-
<ComboboxInput v-model="searchQuery" placeholder="Search locations or add category filters..." nq-input-box :display-value="() => searchQuery" @focus="refreshCategories" />
118+
<ComboboxInput v-model="searchQuery" :placeholder="$t('search.placeholder')" nq-input-box :display-value="() => searchQuery" @focus="refreshCategories" />
87119
</ComboboxAnchor>
88120

89121
<ComboboxContent position="popper" bg="white" outline="~ 1.5 neutral-200" rounded-t-8 max-h-256 w-full shadow z-50 of-auto>
@@ -93,7 +125,7 @@ function removeCategory(categoryId: string) {
93125
{{ category.name }}
94126
</ComboboxItem>
95127
<ComboboxEmpty v-if="!categories?.length" f-p-md text="f-sm neutral-500 center">
96-
No categories found
128+
{{ $t('search.noCategoriesFound') }}
97129
</ComboboxEmpty>
98130
</ComboboxViewport>
99131
</ComboboxContent>
@@ -123,7 +155,7 @@ function removeCategory(categoryId: string) {
123155
text="14 neutral-800 hocus:neutral"
124156
font-medium py-4 rounded-full cursor-pointer transition-colors f-px-2xs
125157
>
126-
Open now
158+
{{ $t('filters.openNow') }}
127159
</ToggleGroupItem>
128160
<ToggleGroupItem
129161
value="walkable"
@@ -132,7 +164,7 @@ function removeCategory(categoryId: string) {
132164
text="14 neutral-800 hocus:neutral"
133165
font-medium py-4 rounded-full cursor-pointer transition-colors f-px-2xs
134166
>
135-
Walkable distance
167+
{{ $t('filters.walkableDistance') }}
136168
</ToggleGroupItem>
137169
</ToggleGroupRoot>
138170
</div>
@@ -216,7 +248,7 @@ function removeCategory(categoryId: string) {
216248
:to="location.website"
217249
target="_blank" external w-fit f-mt-sm nq-arrow nq-pill-blue
218250
>
219-
Visit Website
251+
{{ $t('location.visitWebsite') }}
220252
</NuxtLink>
221253
</div>
222254
</div>
@@ -230,10 +262,10 @@ function removeCategory(categoryId: string) {
230262
shadow-md text-center rounded-12 f-py-2xl f-mt-xl
231263
>
232264
<p text="neutral-500" font-medium f-text-lg>
233-
No locations found
265+
{{ $t('empty.title') }}
234266
</p>
235267
<p text="neutral-400 f-sm" f-mt-xs>
236-
Try adjusting your search or filters
268+
{{ $t('empty.subtitle') }}
237269
</p>
238270
</div>
239271
</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:framework",
2324
"@nuxt/icon": "catalog:framework",
2425
"@nuxt/image": "catalog:framework",
2526
"@nuxthub/core": "catalog:framework",
27+
"@nuxtjs/i18n": "catalog:framework",
2628
"@vueuse/nuxt": "catalog:framework",
2729
"consola": "catalog:utils",
2830
"date-fns-tz": "catalog:utils",
@@ -38,15 +40,20 @@
3840
"devDependencies": {
3941
"@antfu/eslint-config": "catalog:dev",
4042
"@nuxt/eslint": "catalog:dev",
43+
"@nuxt/test-utils": "catalog:test",
4144
"@unocss/eslint-plugin": "catalog:dev",
4245
"@unocss/nuxt": "catalog:ui",
4346
"@unocss/preset-attributify": "catalog:ui",
47+
"@vue/test-utils": "catalog:test",
4448
"drizzle-kit": "catalog:database",
4549
"eslint": "catalog:dev",
4650
"eslint-plugin-format": "catalog:dev",
51+
"happy-dom": "catalog:test",
4752
"nimiq-css": "catalog:ui",
53+
"playwright-core": "catalog:test",
4854
"typescript": "catalog:dev",
4955
"unocss-preset-onmax": "catalog:ui",
56+
"vitest": "catalog:test",
5057
"wrangler": "catalog:dev"
5158
}
5259
}

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)