Add comprehensive internationalization (i18n) with Spanish language support

This major feature introduces complete internationalization architecture with Spanish (Mexico) as the first additional language:

## New Features:
- **I18n Architecture**: Complete client-side and server-side internationalization system
- **Spanish (Mexico) Support**: Full es-MX translations for all user-facing text
- **Language Selector**: Dynamic language switching UI component in header
- **API Endpoints**: RESTful endpoints for serving translations (/api/i18n)
- **Progressive Enhancement**: Language detection via Accept-Language header and cookies

## Technical Implementation:
- **Frontend**: Client-side i18n.js with automatic DOM translation and language selector
- **Backend**: Server-side i18n service with locale detection middleware
- **Build Process**: Automated copying of translation files to dist/ directory
- **Responsive Design**: Language selector integrated into header controls layout

## Files Added:
- public/i18n.js - Client-side internationalization library
- src/i18n/index.ts - Server-side i18n service
- src/i18n/locales/en.json - English translations
- src/i18n/locales/es-MX.json - Spanish (Mexico) translations
- src/routes/i18n.ts - API endpoints for translations

## Files Modified:
- package.json - Updated build process to include i18n files
- public/index.html - Added i18n attributes and language selector
- public/app.js - Integrated dynamic translation updates
- src/server.ts - Added locale detection middleware
- src/scss/pages/_index.scss - Language selector styling

This implementation supports easy addition of future languages and maintains backward compatibility while providing a seamless multilingual experience.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Claude Code 2025-07-07 12:25:44 -04:00
parent 9b7325a9dd
commit 8b1787ec47
10 changed files with 1006 additions and 17 deletions

View file

@ -1,4 +1,9 @@
document.addEventListener('DOMContentLoaded', () => {
// Initialize language selector
if (window.i18n) {
window.i18n.createLanguageSelector('language-selector-container');
}
const map = L.map('map').setView([42.96008, -85.67403], 10);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
@ -82,7 +87,10 @@ document.addEventListener('DOMContentLoaded', () => {
.then(locations => {
showMarkers(locations);
const countElement = document.getElementById('location-count');
countElement.textContent = `${locations.length} active report${locations.length !== 1 ? 's' : ''}`;
const reportsText = locations.length === 1 ?
(window.t ? window.t('map.activeReports') : 'active report') :
(window.t ? window.t('map.activeReportsPlural') : 'active reports');
countElement.textContent = `${locations.length} ${reportsText}`;
// Add visual indicator of last update
const now = new Date();
@ -92,11 +100,13 @@ document.addEventListener('DOMContentLoaded', () => {
minute: '2-digit',
second: '2-digit'
});
countElement.title = `Last updated: ${timeStr}`;
const lastUpdatedText = window.t ? window.t('time.lastUpdated') : 'Last updated:';
countElement.title = `${lastUpdatedText} ${timeStr}`;
})
.catch(err => {
console.error('Error fetching locations:', err);
document.getElementById('location-count').textContent = 'Error loading locations';
const errorText = window.t ? window.t('table.errorLoading') : 'Error loading locations';
document.getElementById('location-count').textContent = errorText;
});
};
@ -242,13 +252,15 @@ document.addEventListener('DOMContentLoaded', () => {
.then(location => {
// Immediately refresh all locations to show the new one
refreshLocations();
messageDiv.textContent = 'Location reported successfully!';
const successText = window.t ? window.t('form.reportSubmittedMsg') : 'Location reported successfully!';
messageDiv.textContent = successText;
messageDiv.className = 'message success';
locationForm.reset();
})
.catch(err => {
console.error('Error reporting location:', err);
messageDiv.textContent = 'Error reporting location.';
const errorText = window.t ? window.t('form.submitError') : 'Error reporting location.';
messageDiv.textContent = errorText;
messageDiv.className = 'message error';
})
.finally(() => {