Add internationalization support with astro-i18next integration

- Implemented astro-i18next for multi-language support, including English, Dutch, and Italian.
- Configured default locale and language fallback settings.
- Defined routes for localized content in the configuration.
- Updated package.json and package-lock.json to include new dependencies for i18next and related plugins.
This commit is contained in:
becarta
2025-05-23 15:10:00 +02:00
parent 8a3507dce0
commit 3168826fa8
581 changed files with 88691 additions and 494 deletions

View File

@@ -1,97 +1,56 @@
---
import { t, getCurrentLocale } from '../utils/i18n';
import { t } from '../utils/i18n';
import { Trans } from 'astro-i18next/components';
import i18next from 'i18next';
const currentLocale = getCurrentLocale();
console.log('Hero component locale:', currentLocale);
// Log the locale for debugging
console.log('Hero component locale:', i18next.language);
---
<section class="relative min-h-screen flex items-center justify-center overflow-hidden bg-gradient-to-br from-background via-background to-muted">
<!-- Background decoration -->
<div class="absolute inset-0 overflow-hidden">
<div class="absolute -top-40 -right-32 w-80 h-80 rounded-full bg-primary/5 blur-3xl"></div>
<div class="absolute -bottom-40 -left-32 w-96 h-96 rounded-full bg-secondary/5 blur-3xl"></div>
<div class="absolute top-20 left-1/4 w-32 h-32 rounded-full bg-primary/10 blur-2xl animate-bounce-subtle"></div>
</div>
<!-- Grid overlay -->
<div class="absolute inset-0 bg-grid-pattern opacity-5"></div>
<section class="relative min-h-[calc(100vh-4rem)] flex items-center justify-center bg-gradient-to-br from-background via-background to-muted overflow-hidden">
<div class="container-custom relative z-10">
<div class="text-center max-w-4xl mx-auto">
<!-- Main headline -->
<h1 class="text-4xl sm:text-5xl lg:text-6xl xl:text-7xl font-display font-bold text-foreground mb-6 animate-on-scroll">
<span class="bg-gradient-to-r from-primary via-primary to-secondary bg-clip-text text-transparent">
{t('hero.title', currentLocale)}
</span>
<div class="flex flex-col items-center justify-center w-full animate-on-scroll">
<h1 class="text-4xl sm:text-5xl lg:text-6xl font-display font-bold text-foreground mb-6 text-center">
{t('hero.title', 'en')}
</h1>
<!-- Subtitle -->
<p class="text-lg sm:text-xl lg:text-2xl text-muted-foreground mb-8 max-w-3xl mx-auto leading-relaxed animate-on-scroll" style="animation-delay: 0.2s">
{t('hero.subtitle', currentLocale)}
<p class="text-lg sm:text-xl text-muted-foreground mb-8 max-w-2xl mx-auto text-center">
{t('hero.subtitle', 'en')}
</p>
<!-- CTA Buttons -->
<div class="flex flex-col sm:flex-row gap-4 justify-center items-center mb-12 animate-on-scroll" style="animation-delay: 0.4s">
<div class="flex flex-col sm:flex-row gap-4 justify-center mb-8">
<a
href="/contact"
class="btn-primary px-8 py-4 text-lg font-semibold rounded-xl shadow-lg hover:shadow-xl transition-all duration-300 hover:scale-105 group"
class="btn-primary px-8 py-4 text-lg font-semibold rounded-xl shadow-lg hover:shadow-xl transition-all duration-300 hover:scale-105"
>
{t('hero.cta.primary', currentLocale)}
<svg class="inline h-5 w-5 ml-2 group-hover:translate-x-1 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7l5 5m0 0l-5 5m5-5H6"/>
</svg>
{t('hero.cta.primary', 'en')}
</a>
<a
href="/services"
class="btn-outline px-8 py-4 text-lg font-semibold rounded-xl transition-all duration-300 hover:scale-105"
>
{t('hero.cta.secondary', currentLocale)}
{t('hero.cta.secondary', 'en')}
</a>
</div>
<!-- Trust indicators -->
<div class="flex flex-col items-center animate-on-scroll" style="animation-delay: 0.6s">
<p class="text-sm text-muted-foreground mb-4">{t('hero.trusted', currentLocale)}</p>
<!-- Service icons -->
<div class="flex items-center justify-center space-x-8 opacity-60 mb-12">
<!-- Microsoft 365 -->
<div class="text-2xl sm:text-3xl" title="Microsoft 365">
🏢
</div>
<!-- Networking -->
<div class="text-2xl sm:text-3xl" title="Networking">
🌐
</div>
<!-- Web Hosting -->
<div class="text-2xl sm:text-3xl" title="Web Hosting">
🚀
</div>
<!-- Automation -->
<div class="text-2xl sm:text-3xl" title="Automation">
⚙️
</div>
<!-- Custom Solutions -->
<div class="text-2xl sm:text-3xl" title="Custom Solutions">
🛠️
</div>
</div>
<!-- Scroll indicator -->
<div class="animate-bounce text-muted-foreground hover:text-foreground transition-colors">
<button
onclick="document.getElementById('services').scrollIntoView({behavior: 'smooth'})"
class="p-2"
aria-label="Scroll to services"
>
<svg class="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 14l-7 7m0 0l-7-7m7 7V3"/>
</svg>
</button>
</div>
<p class="mt-8 text-sm text-muted-foreground text-center">
{t('hero.trusted', 'en')}
</p>
<!-- Service Icons Row -->
<div class="flex justify-center gap-6 mt-6 mb-4 text-3xl opacity-80">
<span>🗂️</span>
<span>🌐</span>
<span>⚙️</span>
<span>🔒</span>
<span>🛠️</span>
</div>
<!-- Downward Arrow -->
<div class="flex justify-center mt-8">
<span class="animate-bounce text-3xl text-primary/60">↓</span>
</div>
</div>
</div>
<div class="absolute inset-0 -z-10 overflow-hidden">
<div class="absolute -top-1/2 -right-1/2 w-full h-full rotate-12 bg-gradient-radial from-primary/5 via-primary/2 to-transparent opacity-70"></div>
<div class="absolute -bottom-1/2 -left-1/2 w-full h-full -rotate-12 bg-gradient-radial from-primary/5 via-primary/2 to-transparent opacity-70"></div>
</div>
</section>
<style>

View File

@@ -0,0 +1,7 @@
import { LanguageSelector } from "astro-i18next/components";
<LanguageSelector
showFlag={true}
class="bg-white dark:bg-gray-800 text-gray-800 dark:text-white px-4 py-2 rounded-md border border-gray-300 dark:border-gray-600"
languageMapping={{ en: "English", nl: "Nederlands", it: "Italiano" }}
/>

View File

@@ -0,0 +1,21 @@
---
import { LANGUAGES } from '../site.config';
import { localizePath } from 'astro-i18next';
const { currentLocale } = Astro;
const { pathname } = Astro.url;
---
<select
class="language-selector bg-white dark:bg-gray-800 text-gray-800 dark:text-white px-4 py-2 rounded-md border border-gray-300 dark:border-gray-600"
onchange="window.location.href = this.value"
>
{Object.entries(LANGUAGES).map(([code, name]) => (
<option
value={localizePath(pathname, code)}
selected={code === currentLocale}
>
{name}
</option>
))}
</select>

View File

@@ -1,141 +1,21 @@
---
import { LANGUAGES } from '../site.config';
import { t, getCurrentLocale } from '../utils/i18n';
// Get the current locale on the server side
const currentLocale = getCurrentLocale();
console.log('Server-side current locale:', currentLocale);
import LanguageSelector from './LanguageSelector.astro';
---
<div class="relative inline-block text-left">
<button
id="language-toggle"
class="inline-flex items-center justify-center p-2 rounded-md text-muted-foreground hover:text-foreground hover:bg-accent focus:outline-none focus:ring-2 focus:ring-primary"
aria-expanded="false"
aria-haspopup="true"
aria-label={t('nav.language')}
title={t('nav.language')}
>
<!-- Globe icon -->
<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9"/>
</svg>
<!-- Current language indicator -->
<span id="current-lang" class="ml-1 text-sm font-medium" data-current-lang={currentLocale}>
{currentLocale.toUpperCase()}
</span>
<!-- Chevron down -->
<svg id="language-chevron" class="h-4 w-4 ml-1 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
</svg>
</button>
<!-- Dropdown menu -->
<div
id="language-dropdown"
class="hidden absolute right-0 mt-2 w-40 origin-top-right rounded-md bg-background border border-border shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none z-50"
role="menu"
aria-orientation="vertical"
aria-labelledby="language-toggle"
>
<div class="py-1" role="none">
{Object.entries(LANGUAGES).map(([code, name]) => (
<button
type="button"
class:list={[
"language-option w-full text-left block px-4 py-2 text-sm text-foreground hover:bg-accent hover:text-accent-foreground transition-colors",
{ "bg-accent text-accent-foreground": code === currentLocale }
]}
role="menuitem"
data-lang={code}
>
<div class="flex items-center">
<span class="text-base mr-2">
{code === 'en' ? '🇬🇧' : code === 'nl' ? '🇳🇱' : '🇮🇹'}
</span>
{name}
</div>
</button>
))}
</div>
</div>
<LanguageSelector />
</div>
<script>
import { setCurrentLocale, getCurrentLocale } from '../utils/i18n';
<style>
:global(.language-selector-wrapper) {
@apply relative;
}
document.addEventListener('DOMContentLoaded', () => {
const languageToggle = document.getElementById('language-toggle');
const languageDropdown = document.getElementById('language-dropdown');
const languageChevron = document.getElementById('language-chevron');
const currentLangSpan = document.getElementById('current-lang');
const languageOptions = document.querySelectorAll('.language-option');
:global(.language-selector) {
@apply inline-flex items-center justify-center p-2 rounded-md text-muted-foreground hover:text-foreground hover:bg-accent focus:outline-none focus:ring-2 focus:ring-primary;
}
if (!languageToggle || !languageDropdown || !languageChevron || !currentLangSpan) return;
// Update current language display
const currentLocale = getCurrentLocale();
console.log('Client-side current locale:', currentLocale);
currentLangSpan.textContent = currentLocale.toUpperCase();
currentLangSpan.setAttribute('data-current-lang', currentLocale);
// Update active state in dropdown
languageOptions.forEach((option) => {
const optionLang = option.getAttribute('data-lang');
if (optionLang === currentLocale) {
option.classList.add('bg-accent', 'text-accent-foreground');
} else {
option.classList.remove('bg-accent', 'text-accent-foreground');
}
});
// Toggle dropdown
languageToggle.addEventListener('click', (event) => {
event.stopPropagation();
const isExpanded = languageToggle.getAttribute('aria-expanded') === 'true';
languageToggle.setAttribute('aria-expanded', (!isExpanded).toString());
languageDropdown.classList.toggle('hidden');
languageChevron.classList.toggle('rotate-180');
});
// Handle language selection
languageOptions.forEach((option) => {
option.addEventListener('click', (event) => {
event.preventDefault();
event.stopPropagation();
const selectedLang = option.getAttribute('data-lang');
const currentLang = currentLangSpan.getAttribute('data-current-lang');
if (selectedLang && selectedLang !== currentLang) {
console.log('Switching language from', currentLang, 'to', selectedLang);
setCurrentLocale(selectedLang as 'en' | 'nl' | 'it');
}
// Close dropdown
languageToggle.setAttribute('aria-expanded', 'false');
languageDropdown.classList.add('hidden');
languageChevron.classList.remove('rotate-180');
});
});
// Close dropdown when clicking outside
document.addEventListener('click', (event) => {
if (!languageToggle.contains(event.target as Node) && !languageDropdown.contains(event.target as Node)) {
languageToggle.setAttribute('aria-expanded', 'false');
languageDropdown.classList.add('hidden');
languageChevron.classList.remove('rotate-180');
}
});
// Close dropdown when pressing Escape
document.addEventListener('keydown', (event) => {
if (event.key === 'Escape') {
languageToggle.setAttribute('aria-expanded', 'false');
languageDropdown.classList.add('hidden');
languageChevron.classList.remove('rotate-180');
}
});
});
</script>
:global(.language-selector-flag) {
@apply mr-2;
}
</style>