Add Swedish locale and flag language selector
This commit is contained in:
parent
7dcc39f36e
commit
39aae25e81
5 changed files with 53 additions and 14 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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('');
|
||||
|
|
|
|||
41
lib/i18n.ts
41
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;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue