i18n init
This commit is contained in:
240
client/I18N_README.md
Normal file
240
client/I18N_README.md
Normal file
@@ -0,0 +1,240 @@
|
|||||||
|
# Internationalization (i18n) Setup
|
||||||
|
|
||||||
|
This project includes a comprehensive internationalization system built with Astro 5.x. The system supports multiple languages with URL-based routing and automatic language detection.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- ✅ URL-based language routing (Spanish: `/`, English: `/en/`)
|
||||||
|
- ✅ Spanish as default language (no URL prefix)
|
||||||
|
- ✅ Automatic language detection from URL
|
||||||
|
- ✅ Language switcher component
|
||||||
|
- ✅ Type-safe translation keys
|
||||||
|
- ✅ Fallback to default language
|
||||||
|
- ✅ Middleware for automatic redirects
|
||||||
|
- ✅ SEO-friendly URLs
|
||||||
|
|
||||||
|
## Supported Languages
|
||||||
|
|
||||||
|
Currently supported languages:
|
||||||
|
- **Spanish (es)** - Default language (no URL prefix)
|
||||||
|
- **English (en)** - Secondary language (with `/en/` prefix)
|
||||||
|
|
||||||
|
## URL Structure
|
||||||
|
|
||||||
|
- **Spanish (default)**: `/`, `/elements`, `/about`
|
||||||
|
- **English**: `/en/`, `/en/elements`, `/en/about`
|
||||||
|
|
||||||
|
## How to Use
|
||||||
|
|
||||||
|
### 1. Adding New Translations
|
||||||
|
|
||||||
|
Edit the translation dictionaries in `src/lib/i18n.ts`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const translations = {
|
||||||
|
es: {
|
||||||
|
'your.new.key': 'Texto en español',
|
||||||
|
// ... more translations
|
||||||
|
},
|
||||||
|
en: {
|
||||||
|
'your.new.key': 'English text',
|
||||||
|
// ... more translations
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Using Translations in Components
|
||||||
|
|
||||||
|
```astro
|
||||||
|
---
|
||||||
|
import { t, getLanguageFromPath } from '../lib/i18n';
|
||||||
|
|
||||||
|
const currentLang = getLanguageFromPath(Astro.url.pathname);
|
||||||
|
---
|
||||||
|
|
||||||
|
<h1>{t('your.new.key', currentLang)}</h1>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Creating Language-Specific Pages
|
||||||
|
|
||||||
|
For English pages, create them in the `src/pages/[lang]/` directory:
|
||||||
|
|
||||||
|
```astro
|
||||||
|
---
|
||||||
|
// src/pages/[lang]/about.astro (English only)
|
||||||
|
import { t, isSupportedLanguage, type SupportedLanguage, DEFAULT_LANGUAGE } from '../../lib/i18n';
|
||||||
|
|
||||||
|
const { lang } = Astro.params;
|
||||||
|
|
||||||
|
if (!lang || !isSupportedLanguage(lang)) {
|
||||||
|
return Astro.redirect('/about');
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentLang = lang as SupportedLanguage;
|
||||||
|
|
||||||
|
// Redirect Spanish to root
|
||||||
|
if (currentLang === DEFAULT_LANGUAGE) {
|
||||||
|
return Astro.redirect('/about');
|
||||||
|
}
|
||||||
|
---
|
||||||
|
|
||||||
|
<h1>{t('about.title', currentLang)}</h1>
|
||||||
|
```
|
||||||
|
|
||||||
|
For Spanish pages (default), create them directly in `src/pages/`:
|
||||||
|
|
||||||
|
```astro
|
||||||
|
---
|
||||||
|
// src/pages/about.astro (Spanish default)
|
||||||
|
import { t, DEFAULT_LANGUAGE } from '../lib/i18n';
|
||||||
|
|
||||||
|
const currentLang = DEFAULT_LANGUAGE;
|
||||||
|
---
|
||||||
|
|
||||||
|
<h1>{t('about.title', currentLang)}</h1>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Adding the Language Switcher
|
||||||
|
|
||||||
|
The language switcher is automatically included in the main layout. To add it to other components:
|
||||||
|
|
||||||
|
```astro
|
||||||
|
---
|
||||||
|
import LanguageSwitcher from '../components/LanguageSwitcher.astro';
|
||||||
|
---
|
||||||
|
|
||||||
|
<LanguageSwitcher />
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Creating Localized Links
|
||||||
|
|
||||||
|
Use the `getLocalizedPath` function for navigation:
|
||||||
|
|
||||||
|
```astro
|
||||||
|
---
|
||||||
|
import { getLocalizedPath, getLanguageFromPath } from '../lib/i18n';
|
||||||
|
|
||||||
|
const currentLang = getLanguageFromPath(Astro.url.pathname);
|
||||||
|
---
|
||||||
|
|
||||||
|
<a href={getLocalizedPath('/about', currentLang)}>About</a>
|
||||||
|
```
|
||||||
|
|
||||||
|
## File Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
├── lib/
|
||||||
|
│ └── i18n.ts # Main i18n utilities and translations
|
||||||
|
├── components/
|
||||||
|
│ ├── LanguageSwitcher.astro # Language selection component
|
||||||
|
│ └── ... # Other components
|
||||||
|
├── pages/
|
||||||
|
│ ├── [lang]/ # English-specific pages
|
||||||
|
│ │ ├── index.astro # Redirects Spanish to root
|
||||||
|
│ │ ├── elements.astro # Redirects Spanish to root
|
||||||
|
│ │ └── ...
|
||||||
|
│ ├── index.astro # Spanish default homepage
|
||||||
|
│ ├── elements.astro # Spanish default elements page
|
||||||
|
│ └── ... # Other Spanish default pages
|
||||||
|
└── middleware.ts # Language routing middleware
|
||||||
|
```
|
||||||
|
|
||||||
|
## API Reference
|
||||||
|
|
||||||
|
### Translation Functions
|
||||||
|
|
||||||
|
#### `t(key, lang?)`
|
||||||
|
Get a translation for a given key and language.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
t('nav.home', 'es') // Returns: "Inicio"
|
||||||
|
t('nav.home', 'en') // Returns: "Home"
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `getLanguageFromPath(pathname)`
|
||||||
|
Extract language from URL path.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
getLanguageFromPath('/en/elements') // Returns: "en"
|
||||||
|
getLanguageFromPath('/elements') // Returns: "es" (default)
|
||||||
|
getLanguageFromPath('/') // Returns: "es" (default)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `getLocalizedPath(path, lang)`
|
||||||
|
Generate a localized URL path.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
getLocalizedPath('/elements', 'es') // Returns: "/elements"
|
||||||
|
getLocalizedPath('/elements', 'en') // Returns: "/en/elements"
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `isSupportedLanguage(lang)`
|
||||||
|
Check if a language is supported.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
isSupportedLanguage('es') // Returns: true
|
||||||
|
isSupportedLanguage('fr') // Returns: false
|
||||||
|
```
|
||||||
|
|
||||||
|
### Constants
|
||||||
|
|
||||||
|
- `SUPPORTED_LANGUAGES` - Array of supported language codes (['es', 'en'])
|
||||||
|
- `DEFAULT_LANGUAGE` - Default language code ('es')
|
||||||
|
- `TranslationKey` - TypeScript type for translation keys
|
||||||
|
|
||||||
|
## Adding New Languages
|
||||||
|
|
||||||
|
1. Add the language code to `SUPPORTED_LANGUAGES` in `src/lib/i18n.ts`
|
||||||
|
2. Add translations for the new language in the `translations` object
|
||||||
|
3. Update the language switcher component if needed
|
||||||
|
4. Create language-specific pages in `src/pages/[lang]/`
|
||||||
|
|
||||||
|
Example for French:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export const SUPPORTED_LANGUAGES = ['es', 'en', 'fr'] as const;
|
||||||
|
|
||||||
|
const translations = {
|
||||||
|
es: { /* Spanish translations */ },
|
||||||
|
en: { /* English translations */ },
|
||||||
|
fr: { /* French translations */ }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
1. **Use descriptive keys**: Use namespaced keys like `nav.home`, `elements.title`
|
||||||
|
2. **Keep translations organized**: Group related translations together
|
||||||
|
3. **Test all languages**: Ensure all translations are complete
|
||||||
|
4. **Use TypeScript**: Leverage the `TranslationKey` type for type safety
|
||||||
|
5. **Handle missing translations**: The system falls back to the default language
|
||||||
|
6. **Spanish first**: Always put Spanish translations first in the dictionary
|
||||||
|
|
||||||
|
## SEO Considerations
|
||||||
|
|
||||||
|
- Spanish content is served at root URLs (better for SEO)
|
||||||
|
- English content has `/en/` prefix
|
||||||
|
- Language is properly set in the HTML `lang` attribute
|
||||||
|
- Search engines can index content in multiple languages
|
||||||
|
- Use proper hreflang tags if needed for advanced SEO
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Common Issues
|
||||||
|
|
||||||
|
1. **Translation not found**: Check if the key exists in all language dictionaries
|
||||||
|
2. **Language not detected**: Ensure the URL follows the correct pattern
|
||||||
|
3. **Redirect loops**: Check middleware configuration
|
||||||
|
4. **Type errors**: Use the `TranslationKey` type for translation keys
|
||||||
|
|
||||||
|
### Debug Mode
|
||||||
|
|
||||||
|
Enable debug logging by adding console logs in the i18n functions:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export function t(key: TranslationKey, lang: SupportedLanguage = DEFAULT_LANGUAGE): string {
|
||||||
|
console.log(`Translating: ${key} for language: ${lang}`);
|
||||||
|
// ... rest of function
|
||||||
|
}
|
||||||
|
```
|
||||||
49
client/src/components/LanguageSwitcher.astro
Normal file
49
client/src/components/LanguageSwitcher.astro
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
---
|
||||||
|
import { SUPPORTED_LANGUAGES, getLanguageFromPath, getLocalizedPath, type SupportedLanguage } from '../lib/i18n';
|
||||||
|
|
||||||
|
// Get current language from URL
|
||||||
|
const currentPath = Astro.url.pathname;
|
||||||
|
const currentLang = getLanguageFromPath(currentPath);
|
||||||
|
|
||||||
|
// Generate language options
|
||||||
|
const languageOptions = SUPPORTED_LANGUAGES.map(lang => ({
|
||||||
|
code: lang,
|
||||||
|
name: lang === 'es' ? 'Español' : 'English',
|
||||||
|
path: getLocalizedPath(currentPath, lang),
|
||||||
|
isCurrent: lang === currentLang
|
||||||
|
}));
|
||||||
|
---
|
||||||
|
|
||||||
|
<div class="language-switcher">
|
||||||
|
<select
|
||||||
|
id="language-select"
|
||||||
|
class="px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||||
|
onchange="window.location.href = this.value"
|
||||||
|
>
|
||||||
|
{languageOptions.map((option: any) => (
|
||||||
|
<option
|
||||||
|
value={option.path}
|
||||||
|
selected={option.isCurrent}
|
||||||
|
>
|
||||||
|
{option.name}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.language-switcher {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.language-switcher select {
|
||||||
|
background-color: white;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
line-height: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.language-switcher select:focus {
|
||||||
|
outline: none;
|
||||||
|
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,5 +1,9 @@
|
|||||||
---
|
---
|
||||||
// Navigation component for Pole Sport website
|
// Navigation component for Pole Sport website
|
||||||
|
import { t, getLanguageFromPath, getLocalizedPath } from "../lib/i18n";
|
||||||
|
|
||||||
|
// Get current language from URL
|
||||||
|
const currentLang = getLanguageFromPath(Astro.url.pathname);
|
||||||
---
|
---
|
||||||
|
|
||||||
<nav class="bg-white shadow-lg">
|
<nav class="bg-white shadow-lg">
|
||||||
@@ -7,18 +11,18 @@
|
|||||||
<div class="flex justify-between items-center py-4">
|
<div class="flex justify-between items-center py-4">
|
||||||
<!-- Logo/Brand -->
|
<!-- Logo/Brand -->
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<a href="/" class="text-xl font-bold text-gray-800 hover:text-gray-600 transition-colors">
|
<a href={getLocalizedPath("/", currentLang)} class="text-xl font-bold text-gray-800 hover:text-gray-600 transition-colors">
|
||||||
Pole Book
|
Pole Book
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Navigation Links -->
|
<!-- Navigation Links -->
|
||||||
<div class="hidden md:flex space-x-8">
|
<div class="hidden md:flex space-x-8">
|
||||||
<a href="/" class="text-gray-600 hover:text-gray-900 transition-colors font-medium">
|
<a href={getLocalizedPath("/", currentLang)} class="text-gray-600 hover:text-gray-900 transition-colors font-medium">
|
||||||
Home
|
{t('nav.home', currentLang)}
|
||||||
</a>
|
</a>
|
||||||
<a href="/elements" class="text-gray-600 hover:text-gray-900 transition-colors font-medium">
|
<a href={getLocalizedPath("/elements", currentLang)} class="text-gray-600 hover:text-gray-900 transition-colors font-medium">
|
||||||
Elements
|
{t('nav.elements', currentLang)}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -35,11 +39,11 @@
|
|||||||
<!-- Mobile menu -->
|
<!-- Mobile menu -->
|
||||||
<div id="mobile-menu" class="md:hidden hidden">
|
<div id="mobile-menu" class="md:hidden hidden">
|
||||||
<div class="px-2 pt-2 pb-3 space-y-1 sm:px-3">
|
<div class="px-2 pt-2 pb-3 space-y-1 sm:px-3">
|
||||||
<a href="/" class="block px-3 py-2 text-gray-600 hover:text-gray-900 hover:bg-gray-50 rounded-md font-medium">
|
<a href={getLocalizedPath("/", currentLang)} class="block px-3 py-2 text-gray-600 hover:text-gray-900 hover:bg-gray-50 rounded-md font-medium">
|
||||||
Home
|
{t('nav.home', currentLang)}
|
||||||
</a>
|
</a>
|
||||||
<a href="/elements" class="block px-3 py-2 text-gray-600 hover:text-gray-900 hover:bg-gray-50 rounded-md font-medium">
|
<a href={getLocalizedPath("/elements", currentLang)} class="block px-3 py-2 text-gray-600 hover:text-gray-900 hover:bg-gray-50 rounded-md font-medium">
|
||||||
Elements
|
{t('nav.elements', currentLang)}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import MarkdownComponent from "./MardownContent.astro";
|
import MarkdownComponent from "./MardownContent.astro";
|
||||||
import { getStrapiMedia } from "../lib/strapi";
|
import { getStrapiMedia } from "../lib/strapi";
|
||||||
import { getStrapiBaseUrl } from "../config/strapi";
|
import { getStrapiBaseUrl } from "../config/strapi";
|
||||||
|
import { t, getLanguageFromPath, type SupportedLanguage } from "../lib/i18n";
|
||||||
import type { HTMLAttributes } from "astro/types";
|
import type { HTMLAttributes } from "astro/types";
|
||||||
|
|
||||||
// Define the props interface
|
// Define the props interface
|
||||||
@@ -13,10 +14,18 @@ export interface Props extends HTMLAttributes<'div'> {
|
|||||||
// Destructure props and get BASE_URL from global config
|
// Destructure props and get BASE_URL from global config
|
||||||
const { elements, ...otherProps } = Astro.props;
|
const { elements, ...otherProps } = Astro.props;
|
||||||
const BASE_URL = getStrapiBaseUrl();
|
const BASE_URL = getStrapiBaseUrl();
|
||||||
|
|
||||||
|
// Get current language
|
||||||
|
const currentLang = getLanguageFromPath(Astro.url.pathname);
|
||||||
---
|
---
|
||||||
|
|
||||||
<div {...otherProps}>
|
<div {...otherProps}>
|
||||||
{
|
{
|
||||||
|
elements.length === 0 ? (
|
||||||
|
<div class="text-center py-8">
|
||||||
|
<p class="text-gray-500 text-lg">{t('elements.noElements', currentLang)}</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
elements.map((poleElement) => (
|
elements.map((poleElement) => (
|
||||||
<a href={`/elements/${poleElement.id}`} class="block">
|
<a href={`/elements/${poleElement.id}`} class="block">
|
||||||
<article class="flex items-center bg-white rounded-lg shadow-lg overflow-hidden hover:shadow-xl transition-shadow duration-200">
|
<article class="flex items-center bg-white rounded-lg shadow-lg overflow-hidden hover:shadow-xl transition-shadow duration-200">
|
||||||
@@ -36,5 +45,6 @@ const BASE_URL = getStrapiBaseUrl();
|
|||||||
</article>
|
</article>
|
||||||
</a>
|
</a>
|
||||||
))
|
))
|
||||||
|
)
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
@@ -2,16 +2,23 @@
|
|||||||
import "../styles/global.css";
|
import "../styles/global.css";
|
||||||
import Head from "./Head.astro";
|
import Head from "./Head.astro";
|
||||||
import Navigation from "../components/Navigation.astro";
|
import Navigation from "../components/Navigation.astro";
|
||||||
|
import { getLanguageFromPath } from "../lib/i18n";
|
||||||
|
|
||||||
const { title = "Just a title", description = "Adescription" } = Astro.props;
|
const { title = "Just a title", description = "Adescription" } = Astro.props;
|
||||||
|
|
||||||
|
// Get current language from URL
|
||||||
|
const currentLang = getLanguageFromPath(Astro.url.pathname);
|
||||||
---
|
---
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang={currentLang}>
|
||||||
<Head {title} {description} />
|
<Head {title} {description} />
|
||||||
<body>
|
<body>
|
||||||
|
<header class="bg-white shadow-sm">
|
||||||
<Navigation />
|
<Navigation />
|
||||||
<div class="container max-w-4xl mx-auto p-4">
|
</header>
|
||||||
|
<main class="container max-w-4xl mx-auto p-4">
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</main>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
||||||
|
|||||||
147
client/src/lib/i18n.ts
Normal file
147
client/src/lib/i18n.ts
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
// Supported languages
|
||||||
|
export const SUPPORTED_LANGUAGES = ['es', 'en'] as const;
|
||||||
|
export type SupportedLanguage = typeof SUPPORTED_LANGUAGES[number];
|
||||||
|
|
||||||
|
// Default language
|
||||||
|
export const DEFAULT_LANGUAGE: SupportedLanguage = 'es';
|
||||||
|
|
||||||
|
// Translation dictionaries
|
||||||
|
const translations = {
|
||||||
|
es: {
|
||||||
|
// Common
|
||||||
|
'common.loading': 'Cargando...',
|
||||||
|
'common.error': 'Error',
|
||||||
|
'common.success': 'Éxito',
|
||||||
|
'common.cancel': 'Cancelar',
|
||||||
|
'common.save': 'Guardar',
|
||||||
|
'common.edit': 'Editar',
|
||||||
|
'common.delete': 'Eliminar',
|
||||||
|
'common.back': 'Atrás',
|
||||||
|
'common.next': 'Siguiente',
|
||||||
|
'common.previous': 'Anterior',
|
||||||
|
|
||||||
|
// Navigation
|
||||||
|
'nav.home': 'Inicio',
|
||||||
|
'nav.about': 'Acerca de',
|
||||||
|
'nav.contact': 'Contacto',
|
||||||
|
'nav.elements': 'Elementos',
|
||||||
|
|
||||||
|
// Elements
|
||||||
|
'elements.title': 'Elementos',
|
||||||
|
'elements.view': 'Ver Elemento',
|
||||||
|
'elements.noElements': 'No se encontraron elementos',
|
||||||
|
'elements.loading': 'Cargando elementos...',
|
||||||
|
|
||||||
|
// Forms
|
||||||
|
'form.required': 'Este campo es obligatorio',
|
||||||
|
'form.invalid': 'Entrada inválida',
|
||||||
|
'form.submit': 'Enviar',
|
||||||
|
'form.reset': 'Restablecer',
|
||||||
|
|
||||||
|
// Messages
|
||||||
|
'message.elementNotFound': 'Elemento no encontrado',
|
||||||
|
'message.loadingError': 'Error al cargar contenido',
|
||||||
|
'message.saveSuccess': 'Guardado exitosamente',
|
||||||
|
'message.deleteSuccess': 'Eliminado exitosamente',
|
||||||
|
},
|
||||||
|
en: {
|
||||||
|
// Common
|
||||||
|
'common.loading': 'Loading...',
|
||||||
|
'common.error': 'Error',
|
||||||
|
'common.success': 'Success',
|
||||||
|
'common.cancel': 'Cancel',
|
||||||
|
'common.save': 'Save',
|
||||||
|
'common.edit': 'Edit',
|
||||||
|
'common.delete': 'Delete',
|
||||||
|
'common.back': 'Back',
|
||||||
|
'common.next': 'Next',
|
||||||
|
'common.previous': 'Previous',
|
||||||
|
|
||||||
|
// Navigation
|
||||||
|
'nav.home': 'Home',
|
||||||
|
'nav.about': 'About',
|
||||||
|
'nav.contact': 'Contact',
|
||||||
|
'nav.elements': 'Elements',
|
||||||
|
|
||||||
|
// Elements
|
||||||
|
'elements.title': 'Elements',
|
||||||
|
'elements.view': 'View Element',
|
||||||
|
'elements.noElements': 'No elements found',
|
||||||
|
'elements.loading': 'Loading elements...',
|
||||||
|
|
||||||
|
// Forms
|
||||||
|
'form.required': 'This field is required',
|
||||||
|
'form.invalid': 'Invalid input',
|
||||||
|
'form.submit': 'Submit',
|
||||||
|
'form.reset': 'Reset',
|
||||||
|
|
||||||
|
// Messages
|
||||||
|
'message.elementNotFound': 'Element not found',
|
||||||
|
'message.loadingError': 'Error loading content',
|
||||||
|
'message.saveSuccess': 'Successfully saved',
|
||||||
|
'message.deleteSuccess': 'Successfully deleted',
|
||||||
|
}
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
// Type for translation keys
|
||||||
|
export type TranslationKey = keyof typeof translations.es;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get translation for a given key and language
|
||||||
|
*/
|
||||||
|
export function t(key: TranslationKey, lang: SupportedLanguage = DEFAULT_LANGUAGE): string {
|
||||||
|
const langTranslations = translations[lang];
|
||||||
|
if (!langTranslations) {
|
||||||
|
console.warn(`Language ${lang} not supported, falling back to ${DEFAULT_LANGUAGE}`);
|
||||||
|
return translations[DEFAULT_LANGUAGE][key] || key;
|
||||||
|
}
|
||||||
|
|
||||||
|
return langTranslations[key] || translations[DEFAULT_LANGUAGE][key] || key;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current language from URL path
|
||||||
|
*/
|
||||||
|
export function getLanguageFromPath(pathname: string): SupportedLanguage {
|
||||||
|
const pathSegments = pathname.split('/').filter(Boolean);
|
||||||
|
const firstSegment = pathSegments[0];
|
||||||
|
|
||||||
|
if (SUPPORTED_LANGUAGES.includes(firstSegment as SupportedLanguage)) {
|
||||||
|
return firstSegment as SupportedLanguage;
|
||||||
|
}
|
||||||
|
|
||||||
|
return DEFAULT_LANGUAGE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get path with language prefix
|
||||||
|
*/
|
||||||
|
export function getLocalizedPath(path: string, lang: SupportedLanguage): string {
|
||||||
|
if (lang === DEFAULT_LANGUAGE) {
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove leading slash if present
|
||||||
|
const cleanPath = path.startsWith('/') ? path.slice(1) : path;
|
||||||
|
return `/${lang}/${cleanPath}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all translations for a given key across all languages
|
||||||
|
*/
|
||||||
|
export function getAllTranslations(key: TranslationKey): Record<SupportedLanguage, string> {
|
||||||
|
const result: Record<SupportedLanguage, string> = {} as Record<SupportedLanguage, string>;
|
||||||
|
|
||||||
|
SUPPORTED_LANGUAGES.forEach(lang => {
|
||||||
|
result[lang] = t(key, lang);
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a language is supported
|
||||||
|
*/
|
||||||
|
export function isSupportedLanguage(lang: string): lang is SupportedLanguage {
|
||||||
|
return SUPPORTED_LANGUAGES.includes(lang as SupportedLanguage);
|
||||||
|
}
|
||||||
26
client/src/middleware.ts
Normal file
26
client/src/middleware.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { defineMiddleware } from 'astro:middleware';
|
||||||
|
import { SUPPORTED_LANGUAGES, DEFAULT_LANGUAGE, isSupportedLanguage } from './lib/i18n';
|
||||||
|
|
||||||
|
export const onRequest = defineMiddleware(async (context, next) => {
|
||||||
|
const { pathname } = context.url;
|
||||||
|
|
||||||
|
// Skip middleware for static assets
|
||||||
|
if (pathname.startsWith('/_astro/') || pathname.startsWith('/assets/') || pathname.includes('.')) {
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
|
const pathSegments = pathname.split('/').filter(Boolean);
|
||||||
|
const firstSegment = pathSegments[0];
|
||||||
|
|
||||||
|
// If the first segment is a supported language, continue
|
||||||
|
if (isSupportedLanguage(firstSegment)) {
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no language prefix, it's Spanish (default) - continue
|
||||||
|
if (pathname === '/' || !isSupportedLanguage(firstSegment)) {
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
|
return next();
|
||||||
|
});
|
||||||
50
client/src/pages/[lang]/elements.astro
Normal file
50
client/src/pages/[lang]/elements.astro
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
---
|
||||||
|
// Import necessary components and utilities
|
||||||
|
import Layout from "../../layouts/Layout.astro";
|
||||||
|
import PoleElementsList from "../../components/PoleElementsList.astro";
|
||||||
|
import { t, isSupportedLanguage, type SupportedLanguage, DEFAULT_LANGUAGE } from "../../lib/i18n";
|
||||||
|
|
||||||
|
// Get language from URL parameter
|
||||||
|
const { lang } = Astro.params;
|
||||||
|
|
||||||
|
// Validate language parameter
|
||||||
|
if (!lang || !isSupportedLanguage(lang)) {
|
||||||
|
return Astro.redirect('/elements');
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentLang = lang as SupportedLanguage;
|
||||||
|
|
||||||
|
// If it's the default language (Spanish), redirect to root
|
||||||
|
if (currentLang === DEFAULT_LANGUAGE) {
|
||||||
|
return Astro.redirect('/elements');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mock data for demonstration - replace with actual data fetching
|
||||||
|
const elements = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
name: "Basic Spin",
|
||||||
|
mainImage: {
|
||||||
|
url: "/placeholder-image.jpg",
|
||||||
|
alternativeText: "Basic Spin"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
name: "Fireman Spin",
|
||||||
|
mainImage: {
|
||||||
|
url: "/placeholder-image.jpg",
|
||||||
|
alternativeText: "Fireman Spin"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
---
|
||||||
|
|
||||||
|
<Layout title={t('elements.title', currentLang)} description="Pole Elements">
|
||||||
|
<div class="container mx-auto p-4">
|
||||||
|
<div class="py-8">
|
||||||
|
<h1 class="text-3xl font-bold mb-8">{t('elements.title', currentLang)}</h1>
|
||||||
|
<PoleElementsList elements={elements} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Layout>
|
||||||
34
client/src/pages/[lang]/index.astro
Normal file
34
client/src/pages/[lang]/index.astro
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
---
|
||||||
|
// Import necessary components and utilities
|
||||||
|
import Layout from "../../layouts/Layout.astro";
|
||||||
|
import BigCard from "../../components/BigCard.astro";
|
||||||
|
import { t, isSupportedLanguage, type SupportedLanguage, DEFAULT_LANGUAGE } from "../../lib/i18n";
|
||||||
|
|
||||||
|
// Get language from URL parameter
|
||||||
|
const { lang } = Astro.params;
|
||||||
|
|
||||||
|
// Validate language parameter
|
||||||
|
if (!lang || !isSupportedLanguage(lang)) {
|
||||||
|
return Astro.redirect('/');
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentLang = lang as SupportedLanguage;
|
||||||
|
|
||||||
|
// If it's the default language (Spanish), redirect to root
|
||||||
|
if (currentLang === DEFAULT_LANGUAGE) {
|
||||||
|
return Astro.redirect('/');
|
||||||
|
}
|
||||||
|
---
|
||||||
|
|
||||||
|
<Layout title={t('nav.home', currentLang)} description="Pole Elements">
|
||||||
|
<div class="container mx-auto p-4">
|
||||||
|
<div class="py-12">
|
||||||
|
<BigCard
|
||||||
|
url={`/${currentLang}/elements`}
|
||||||
|
title={t('elements.title', currentLang)}
|
||||||
|
description="Explore a comprehensive collection of pole dance elements, techniques, and combinations. From basic spins to advanced tricks, discover everything you need to master pole sport."
|
||||||
|
ctaText={t('elements.view', currentLang)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Layout>
|
||||||
@@ -2,26 +2,32 @@
|
|||||||
// Import necessary components and utilities
|
// Import necessary components and utilities
|
||||||
import Layout from "../layouts/Layout.astro";
|
import Layout from "../layouts/Layout.astro";
|
||||||
import PoleElementsList from "../components/PoleElementsList.astro";
|
import PoleElementsList from "../components/PoleElementsList.astro";
|
||||||
|
import { t, DEFAULT_LANGUAGE } from "../lib/i18n";
|
||||||
|
|
||||||
import fetchApi from '../lib/strapi';
|
import fetchApi from "../lib/strapi";
|
||||||
import type PoleElement from "../interfaces/poleElement";
|
import type PoleElement from "../interfaces/poleElement";
|
||||||
|
|
||||||
|
// Use Spanish as default language
|
||||||
|
const currentLang = DEFAULT_LANGUAGE;
|
||||||
|
|
||||||
const strapiPoleElements = await fetchApi<PoleElement[]>({
|
const strapiPoleElements = await fetchApi<PoleElement[]>({
|
||||||
endpoint: 'elements?populate=*', // the content type to fetch
|
endpoint: "elements?populate=*", // the content type to fetch
|
||||||
wrappedByKey: 'data', // the key to unwrap the response
|
wrappedByKey: "data", // the key to unwrap the response
|
||||||
});
|
});
|
||||||
---
|
---
|
||||||
|
|
||||||
<Layout title="Pole Elements" description="Pole Elements">
|
<Layout title={t("elements.title", currentLang)} description="Pole Elements">
|
||||||
<!-- Main heading -->
|
<div class="container mx-auto p-4">
|
||||||
|
<div class="py-8">
|
||||||
<h1 class="text-3xl font-bold mb-8">
|
<h1 class="text-3xl font-bold mb-8">
|
||||||
Pole Elements
|
{t("elements.title", currentLang)}
|
||||||
</h1>
|
</h1>
|
||||||
<!-- Pole Elements List -->
|
|
||||||
<PoleElementsList
|
<PoleElementsList
|
||||||
elements={strapiPoleElements}
|
elements={strapiPoleElements}
|
||||||
class="max-w-4xl mx-auto space-y-4"
|
class="max-w-4xl mx-auto space-y-4"
|
||||||
id="pole-elements-list"
|
id="pole-elements-list"
|
||||||
data-testid="pole-elements"
|
data-testid="pole-elements"
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|||||||
@@ -2,16 +2,20 @@
|
|||||||
// Import necessary components and utilities
|
// Import necessary components and utilities
|
||||||
import Layout from "../layouts/Layout.astro";
|
import Layout from "../layouts/Layout.astro";
|
||||||
import BigCard from "../components/BigCard.astro";
|
import BigCard from "../components/BigCard.astro";
|
||||||
|
import { t, DEFAULT_LANGUAGE } from "../lib/i18n";
|
||||||
|
|
||||||
|
// Use Spanish as default language
|
||||||
|
const currentLang = DEFAULT_LANGUAGE;
|
||||||
---
|
---
|
||||||
|
|
||||||
<Layout title="Pole Elements" description="Pole Elements">
|
<Layout title={t('nav.home', currentLang)} description="Pole Elements">
|
||||||
<div class="container mx-auto p-4">
|
<div class="container mx-auto p-4">
|
||||||
<div class="py-12">
|
<div class="py-12">
|
||||||
<BigCard
|
<BigCard
|
||||||
url="/elements"
|
url="/elements"
|
||||||
title="Pole Elements"
|
title={t('elements.title', currentLang)}
|
||||||
description="Explore a comprehensive collection of pole dance elements, techniques, and combinations. From basic spins to advanced tricks, discover everything you need to master pole sport."
|
description="Explore a comprehensive collection of pole dance elements, techniques, and combinations. From basic spins to advanced tricks, discover everything you need to master pole sport."
|
||||||
ctaText="Explore Elements"
|
ctaText={t('elements.view', currentLang)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user