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.
|
- 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.
|
- 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.
|
- 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>(() => {
|
const [locale, setLocale] = useState<Locale>(() => {
|
||||||
if (typeof window === 'undefined') return 'en';
|
if (typeof window === 'undefined') return 'en';
|
||||||
const stored = localStorage.getItem('locale');
|
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 });
|
return resolveLocale({ cookieLocale: null, acceptLanguage: navigator.language ?? navigator.languages?.[0] ?? null });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -191,17 +191,20 @@ export default function NavBar() {
|
||||||
</Link>
|
</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 }}>
|
<span style={{ fontSize: 12, color: '#555', display: 'flex', alignItems: 'center', gap: 4 }}>
|
||||||
<Icon name="globe" /> {t('navLanguage')}:
|
<Icon name="globe" /> {t('navLanguage')}:
|
||||||
</span>
|
</span>
|
||||||
<button className="button secondary" onClick={() => setLocale('fi')} style={{ padding: '4px 8px', opacity: locale === 'fi' ? 1 : 0.7 }}>
|
<select
|
||||||
FI
|
value={locale}
|
||||||
</button>
|
onChange={(e) => setLocale(e.target.value as any)}
|
||||||
<button className="button secondary" onClick={() => setLocale('en')} style={{ padding: '4px 8px', opacity: locale === 'en' ? 1 : 0.7 }}>
|
style={{ padding: '6px 10px', borderRadius: 8, border: '1px solid #ddd', background: '#fff' }}
|
||||||
EN
|
>
|
||||||
</button>
|
<option value="fi">🇫🇮 Suomi</option>
|
||||||
</div>
|
<option value="sv">🇸🇪 Svenska</option>
|
||||||
|
<option value="en">🇬🇧 English</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ type SelectedImage = {
|
||||||
|
|
||||||
const MAX_IMAGES = 6;
|
const MAX_IMAGES = 6;
|
||||||
const MAX_IMAGE_BYTES = 5 * 1024 * 1024; // 5 MB per image
|
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 };
|
type LocaleFields = { title: string; description: string; teaser: string };
|
||||||
|
|
||||||
export default function NewListingPage() {
|
export default function NewListingPage() {
|
||||||
|
|
@ -24,6 +24,7 @@ export default function NewListingPage() {
|
||||||
const [translations, setTranslations] = useState<Record<Locale, LocaleFields>>({
|
const [translations, setTranslations] = useState<Record<Locale, LocaleFields>>({
|
||||||
en: { title: '', description: '', teaser: '' },
|
en: { title: '', description: '', teaser: '' },
|
||||||
fi: { title: '', description: '', teaser: '' },
|
fi: { title: '', description: '', teaser: '' },
|
||||||
|
sv: { title: '', description: '', teaser: '' },
|
||||||
});
|
});
|
||||||
const [country, setCountry] = useState('Finland');
|
const [country, setCountry] = useState('Finland');
|
||||||
const [region, setRegion] = useState('');
|
const [region, setRegion] = useState('');
|
||||||
|
|
@ -273,6 +274,7 @@ export default function NewListingPage() {
|
||||||
setTranslations({
|
setTranslations({
|
||||||
en: { title: '', description: '', teaser: '' },
|
en: { title: '', description: '', teaser: '' },
|
||||||
fi: { title: '', description: '', teaser: '' },
|
fi: { title: '', description: '', teaser: '' },
|
||||||
|
sv: { title: '', description: '', teaser: '' },
|
||||||
});
|
});
|
||||||
setRegion('');
|
setRegion('');
|
||||||
setCity('');
|
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: {
|
en: {
|
||||||
brand: 'lomavuokraus.fi',
|
brand: 'lomavuokraus.fi',
|
||||||
navProfile: 'Profile',
|
navProfile: 'Profile',
|
||||||
|
|
@ -525,14 +525,47 @@ const allMessages = {
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const messages = allMessages;
|
const svMessages: typeof baseMessages.en = {
|
||||||
export type MessageKey = keyof typeof allMessages.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 {
|
function normalizeLocale(input?: string | null): Locale | null {
|
||||||
if (!input) return null;
|
if (!input) return null;
|
||||||
const lower = input.toLowerCase();
|
const lower = input.toLowerCase();
|
||||||
if (lower.startsWith('fi')) return 'fi';
|
if (lower.startsWith('fi')) return 'fi';
|
||||||
if (lower.startsWith('en')) return 'en';
|
if (lower.startsWith('en')) return 'en';
|
||||||
|
if (lower.startsWith('sv')) return 'sv';
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue