Add Swedish locale and flag language selector

This commit is contained in:
Tero Halla-aho 2025-11-27 23:37:49 +02:00
parent 7dcc39f36e
commit 39aae25e81
5 changed files with 53 additions and 14 deletions

View file

@ -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.

View file

@ -15,7 +15,7 @@ export function I18nProvider({ children }: { children: React.ReactNode }) {
const [locale, setLocale] = useState<Locale>(() => {
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 });
});

View file

@ -191,17 +191,20 @@ export default function NavBar() {
</Link>
</>
)}
<div style={{ display: 'flex', alignItems: 'center', gap: 4, marginLeft: 8 }}>
<label style={{ display: 'flex', alignItems: 'center', gap: 6, marginLeft: 8 }}>
<span style={{ fontSize: 12, color: '#555', display: 'flex', alignItems: 'center', gap: 4 }}>
<Icon name="globe" /> {t('navLanguage')}:
</span>
<button className="button secondary" onClick={() => setLocale('fi')} style={{ padding: '4px 8px', opacity: locale === 'fi' ? 1 : 0.7 }}>
FI
</button>
<button className="button secondary" onClick={() => setLocale('en')} style={{ padding: '4px 8px', opacity: locale === 'en' ? 1 : 0.7 }}>
EN
</button>
</div>
<select
value={locale}
onChange={(e) => setLocale(e.target.value as any)}
style={{ padding: '6px 10px', borderRadius: 8, border: '1px solid #ddd', background: '#fff' }}
>
<option value="fi">🇫🇮 Suomi</option>
<option value="sv">🇸🇪 Svenska</option>
<option value="en">🇬🇧 English</option>
</select>
</label>
</nav>
</header>
);

View file

@ -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<Record<Locale, LocaleFields>>({
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('');

View file

@ -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;
}