From fe23132113f68c864f7b8c83e4eed3a4fe56f4b6 Mon Sep 17 00:00:00 2001 From: Benjamin Danies Date: Sat, 28 Mar 2026 15:35:10 +0100 Subject: [PATCH] feat(i18n): add French, English and Spanish translations - Add i18n.js with t() helper and translations for FR, EN, ES - Language selector in header, auto-detected from browser - Locale persisted in localStorage - Replace all hardcoded strings across all components - Nominatim queries use selected locale for address results --- CLAUDE.md | 2 + README.md | 1 + index.html | 2 +- src/App.svelte | 51 +++++--- src/lib/AddressInput.svelte | 9 +- src/lib/FriendList.svelte | 3 +- src/lib/GroupManager.svelte | 25 ++-- src/lib/MapView.svelte | 5 +- src/lib/ModeToggle.svelte | 5 +- src/lib/VenueList.svelte | 11 +- src/lib/i18n.js | 235 ++++++++++++++++++++++++++++++++++++ src/main.js | 3 + 12 files changed, 305 insertions(+), 47 deletions(-) create mode 100644 src/lib/i18n.js diff --git a/CLAUDE.md b/CLAUDE.md index fe0f806..8093185 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -32,6 +32,7 @@ src/ MapView.svelte — Leaflet map with markers and walking route polylines utils.js — pure business logic (haversine, fairness, formatting) utils.test.js — unit tests for utils.js + i18n.js — translations (FR, EN, ES) and t() helper ``` ## Commands @@ -71,4 +72,5 @@ This app calls these APIs from the browser (no backend): - State management uses Svelte 5 runes (`$state`) in `stores.svelte.js`, exported as shared reactive objects. - Saved groups are persisted in `localStorage` (no database). - `config.js` is gitignored — copy `config.example.js` and adjust values as needed. +- i18n: all user-facing strings are in `src/lib/i18n.js`. When adding UI text, add keys for all 3 languages (fr, en, es). Locale auto-detected from browser, persisted in `localStorage`. - ORS API key is optional in config — users can enter their own key in the app UI (settings icon). Key is stored in `sessionStorage` only (cleared on browser close). diff --git a/README.md b/README.md index f6272b1..39667c4 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,7 @@ npm run dev - Walking route visualization on the map with a different color per friend - Per-friend distance breakdown when selecting a venue - Save and load groups of addresses (persisted in localStorage) +- Multilingual: French, English, Spanish (auto-detected from browser) - km / miles toggle - Configurable max distance between friends and max number of addresses diff --git a/index.html b/index.html index 0018e82..0567cdc 100644 --- a/index.html +++ b/index.html @@ -1,5 +1,5 @@ - + diff --git a/src/App.svelte b/src/App.svelte index 84e8347..64f76d3 100644 --- a/src/App.svelte +++ b/src/App.svelte @@ -6,6 +6,7 @@ import MapView from './lib/MapView.svelte'; import GroupManager from './lib/GroupManager.svelte'; import { friends, venues, mode, ranking, unit, apiKey, setApiKey, tooFarApart, MAX_DISTANCE, MAX_ADDRESSES, searchVenues, rerankVenues, formatMaxDistance } from './lib/stores.svelte.js'; + import { t, locale, locales, localeLabels, setLocale } from './lib/i18n.js'; let selectedVenue = $state(null); let sidebarOpen = $state(true); @@ -23,11 +24,21 @@
📍
-

Rendez-vous

- +

{t('app.title')}

+
+ +
+ {#each locales as lang} + + {/each} +
+
@@ -57,20 +68,20 @@ {#if showSettings}
- +
setApiKey(keyInput.trim())} - placeholder="Paste your OpenRouteService key..." + placeholder={t('settings.apiKeyPlaceholder')} class="flex-1 bg-white rounded-lg border border-gray-200 px-3 py-1.5 text-sm text-dark placeholder:text-light outline-none focus:border-coral/30 min-w-0" /> {#if apiKey.value} - Active + {t('settings.active')} {:else} - Get a free key + {t('settings.getKey')} {/if}
@@ -86,7 +97,7 @@

- 👥 Who's coming? + 👥 {t('friends.title')}

@@ -97,7 +108,7 @@

- 💾 Saved groups + 💾 {t('groups.title')}

@@ -107,7 +118,7 @@
⚠️

- Some friends are more than {formatMaxDistance()} apart. Add closer addresses to find a meeting spot. + {t('warning.tooFar', { distance: formatMaxDistance() })}

{/if} @@ -116,7 +127,7 @@ {#if friends.list.length >= 2 && !tooFarApart.value}

- 🎯 What are you looking for? + 🎯 {t('mode.title')}

@@ -124,7 +135,7 @@

- ⚖️ Rank by + ⚖️ {t('ranking.title')}