From 39aae25e81bb86807e07fb99867eafffea0ee5b4 Mon Sep 17 00:00:00 2001 From: Tero Halla-aho Date: Thu, 27 Nov 2025 23:37:49 +0200 Subject: [PATCH] Add Swedish locale and flag language selector --- PROGRESS.md | 1 + app/components/I18nProvider.tsx | 2 +- app/components/NavBar.tsx | 19 ++++++++------- app/listings/new/page.tsx | 4 +++- lib/i18n.ts | 41 +++++++++++++++++++++++++++++---- 5 files changed, 53 insertions(+), 14 deletions(-) diff --git a/PROGRESS.md b/PROGRESS.md index 7bb24f9..1e8f105 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -66,3 +66,4 @@ - Browse amenity filters now show the same icons as listing detail; image `registry.halla-aho.net/thalla/lomavuokraus-web:e95d9e0` built/pushed and rolled out to staging. - Home hero cleaned up (removed sample/browse CTAs), hero FI text updated, and health check link moved to About page runtime section. - Listing creation form now supports editing all locales at once with language tabs, per-locale readiness badges, and an AI JSON helper to translate and apply copy across languages; API accepts multiple translations in one request. +- Added Swedish locale support across the app, language selector is now a flag dropdown (FI/SV/EN), and the new listing form/AI helper handle all three languages. diff --git a/app/components/I18nProvider.tsx b/app/components/I18nProvider.tsx index 6a92065..a67d933 100644 --- a/app/components/I18nProvider.tsx +++ b/app/components/I18nProvider.tsx @@ -15,7 +15,7 @@ export function I18nProvider({ children }: { children: React.ReactNode }) { const [locale, setLocale] = useState(() => { if (typeof window === 'undefined') return 'en'; const stored = localStorage.getItem('locale'); - if (stored === 'fi' || stored === 'en') return stored; + if (stored === 'fi' || stored === 'en' || stored === 'sv') return stored as Locale; return resolveLocale({ cookieLocale: null, acceptLanguage: navigator.language ?? navigator.languages?.[0] ?? null }); }); diff --git a/app/components/NavBar.tsx b/app/components/NavBar.tsx index ed3d3a8..e3bf67e 100644 --- a/app/components/NavBar.tsx +++ b/app/components/NavBar.tsx @@ -191,17 +191,20 @@ export default function NavBar() { )} -
+
+ + ); diff --git a/app/listings/new/page.tsx b/app/listings/new/page.tsx index ed8e92a..fa659cd 100644 --- a/app/listings/new/page.tsx +++ b/app/listings/new/page.tsx @@ -14,7 +14,7 @@ type SelectedImage = { const MAX_IMAGES = 6; const MAX_IMAGE_BYTES = 5 * 1024 * 1024; // 5 MB per image -const SUPPORTED_LOCALES: Locale[] = ['en', 'fi']; +const SUPPORTED_LOCALES: Locale[] = ['en', 'fi', 'sv']; type LocaleFields = { title: string; description: string; teaser: string }; export default function NewListingPage() { @@ -24,6 +24,7 @@ export default function NewListingPage() { const [translations, setTranslations] = useState>({ en: { title: '', description: '', teaser: '' }, fi: { title: '', description: '', teaser: '' }, + sv: { title: '', description: '', teaser: '' }, }); const [country, setCountry] = useState('Finland'); const [region, setRegion] = useState(''); @@ -273,6 +274,7 @@ export default function NewListingPage() { setTranslations({ en: { title: '', description: '', teaser: '' }, fi: { title: '', description: '', teaser: '' }, + sv: { title: '', description: '', teaser: '' }, }); setRegion(''); setCity(''); diff --git a/lib/i18n.ts b/lib/i18n.ts index d878c06..1e9e07e 100644 --- a/lib/i18n.ts +++ b/lib/i18n.ts @@ -1,6 +1,6 @@ -export type Locale = 'en' | 'fi'; +export type Locale = 'en' | 'fi' | 'sv'; -const allMessages = { +const baseMessages = { en: { brand: 'lomavuokraus.fi', navProfile: 'Profile', @@ -525,14 +525,47 @@ const allMessages = { }, } as const; -export const messages = allMessages; -export type MessageKey = keyof typeof allMessages.en; +const svMessages: typeof baseMessages.en = { + ...baseMessages.en, + navProfile: 'Profil', + navMyListings: 'Mina annonser', + navNewListing: 'Ny annons', + navApprovals: 'GodkĂ€nnanden', + navUsers: 'AnvĂ€ndare', + navLogout: 'Logga ut', + navLogin: 'Logga in', + navSignup: 'Registrera dig', + navBrowse: 'BlĂ€ddra bland annonser', + navLanguage: 'SprĂ„k', + heroTitle: 'Hitta ditt nĂ€sta finska getaway', + heroBody: 'UpptĂ€ck stugor, lĂ€genheter och villor direkt frĂ„n Ă€garna. Annonser verifieras innan publicering och du kontaktar vĂ€rdarna direkt — enkelt och transparent.', + ctaBrowse: 'BlĂ€ddra bland annonser', + createListingTitle: 'Skapa annons', + languageTabsLabel: 'Annonsens sprĂ„k', + languageTabsHint: 'LĂ€gg till översĂ€ttningar för varje sprĂ„k', + localeReady: 'Klar', + localePartial: 'PĂ„gĂ„r', + localeMissing: 'Saknas', + aiHelperTitle: 'AI-översĂ€ttningshjĂ€lp', + aiHelperLead: 'Kopiera prompten till din AI-assistent, lĂ„t den översĂ€tta saknade sprĂ„k och klistra in JSON-svaret hĂ€r.', + aiPromptLabel: 'Prompt till AI', + aiResponseLabel: 'Klistra in AI-svar (JSON)', + aiApply: 'AnvĂ€nd AI-svar', + aiApplyError: 'Kunde inte lĂ€sa AI-svaret. Kontrollera att det Ă€r giltig JSON med locales-nyckeln.', + aiApplySuccess: 'ÖversĂ€ttningar uppdaterade frĂ„n AI-svaret.', + aiHelperNote: 'AI:n ska bara returnera JSON med samma nycklar.', + translationMissing: 'LĂ€gg till minst ett sprĂ„k med titel och beskrivning.', +}; + +export const messages = { ...baseMessages, sv: svMessages } as const; +export type MessageKey = keyof typeof baseMessages.en; function normalizeLocale(input?: string | null): Locale | null { if (!input) return null; const lower = input.toLowerCase(); if (lower.startsWith('fi')) return 'fi'; if (lower.startsWith('en')) return 'en'; + if (lower.startsWith('sv')) return 'sv'; return null; }