feat: add i18n system with English locale
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,46 @@
|
|||||||
|
type Translations = Record<string, string>;
|
||||||
|
|
||||||
|
export function createI18n(translations: Translations) {
|
||||||
|
function t(key: string, vars?: Record<string, string>): string {
|
||||||
|
let value = translations[key];
|
||||||
|
if (value === undefined) return key;
|
||||||
|
|
||||||
|
if (vars) {
|
||||||
|
for (const [varName, varValue] of Object.entries(vars)) {
|
||||||
|
value = value.replaceAll(`{${varName}}`, varValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { t };
|
||||||
|
}
|
||||||
|
|
||||||
|
function flattenObject(obj: Record<string, unknown>, prefix = ''): Translations {
|
||||||
|
const result: Translations = {};
|
||||||
|
for (const [key, value] of Object.entries(obj)) {
|
||||||
|
const fullKey = prefix ? `${prefix}.${key}` : key;
|
||||||
|
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
||||||
|
Object.assign(result, flattenObject(value as Record<string, unknown>, fullKey));
|
||||||
|
} else {
|
||||||
|
result[fullKey] = String(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
let cachedI18n: ReturnType<typeof createI18n> | null = null;
|
||||||
|
|
||||||
|
export async function loadI18n(locale = 'en'): Promise<ReturnType<typeof createI18n>> {
|
||||||
|
if (cachedI18n) return cachedI18n;
|
||||||
|
|
||||||
|
const localeData = await import(`./locales/${locale}.json`);
|
||||||
|
const flat = flattenObject(localeData.default || localeData);
|
||||||
|
cachedI18n = createI18n(flat);
|
||||||
|
return cachedI18n;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resetI18nCache(): void {
|
||||||
|
cachedI18n = null;
|
||||||
|
}
|
||||||
@@ -0,0 +1,163 @@
|
|||||||
|
{
|
||||||
|
"common": {
|
||||||
|
"save": "Save",
|
||||||
|
"cancel": "Cancel",
|
||||||
|
"delete": "Delete",
|
||||||
|
"edit": "Edit",
|
||||||
|
"create": "Create",
|
||||||
|
"back": "Back",
|
||||||
|
"search": "Search",
|
||||||
|
"filter": "Filter",
|
||||||
|
"loading": "Loading...",
|
||||||
|
"noResults": "No results found",
|
||||||
|
"confirm": "Are you sure?",
|
||||||
|
"yes": "Yes",
|
||||||
|
"no": "No",
|
||||||
|
"submit": "Submit",
|
||||||
|
"required": "Required",
|
||||||
|
"optional": "Optional",
|
||||||
|
"actions": "Actions",
|
||||||
|
"status": "Status",
|
||||||
|
"date": "Date",
|
||||||
|
"name": "Name",
|
||||||
|
"email": "Email",
|
||||||
|
"phone": "Phone",
|
||||||
|
"notes": "Notes",
|
||||||
|
"description": "Description"
|
||||||
|
},
|
||||||
|
"auth": {
|
||||||
|
"login": "Log In",
|
||||||
|
"logout": "Log Out",
|
||||||
|
"register": "Register",
|
||||||
|
"email": "Email",
|
||||||
|
"password": "Password",
|
||||||
|
"currentPassword": "Current Password",
|
||||||
|
"newPassword": "New Password",
|
||||||
|
"confirmPassword": "Confirm Password",
|
||||||
|
"changePassword": "Change Password",
|
||||||
|
"invalidCredentials": "Invalid email or password",
|
||||||
|
"accountInactive": "Account is not active",
|
||||||
|
"passwordMinLength": "Password must be at least 8 characters",
|
||||||
|
"passwordsDoNotMatch": "Passwords do not match"
|
||||||
|
},
|
||||||
|
"nav": {
|
||||||
|
"home": "Home",
|
||||||
|
"pets": "Our Pets",
|
||||||
|
"about": "About",
|
||||||
|
"events": "Events",
|
||||||
|
"donate": "Donate",
|
||||||
|
"contact": "Contact",
|
||||||
|
"apply": "Apply",
|
||||||
|
"adoptionProcess": "Adoption Process",
|
||||||
|
"becomeAFoster": "Become a Foster",
|
||||||
|
"howToHelp": "How to Help",
|
||||||
|
"volunteer": "Volunteer",
|
||||||
|
"whyAdopt": "Why Adopt",
|
||||||
|
"happyTails": "Happy Tails",
|
||||||
|
"sponsors": "Sponsors",
|
||||||
|
"fosterResources": "Foster Resources",
|
||||||
|
"nameGenerator": "Name Generator"
|
||||||
|
},
|
||||||
|
"pets": {
|
||||||
|
"title": "Our Pets",
|
||||||
|
"available": "Available for Adoption",
|
||||||
|
"featured": "Featured Pets",
|
||||||
|
"noFeatured": "No featured pets at the moment",
|
||||||
|
"noPets": "No pets available at the moment",
|
||||||
|
"adoptionFee": "Adoption Fee",
|
||||||
|
"learnMore": "Learn More",
|
||||||
|
"applyToAdopt": "Apply to Adopt",
|
||||||
|
"applyToFoster": "Apply to Foster",
|
||||||
|
"filterSpecies": "Species",
|
||||||
|
"filterAge": "Age",
|
||||||
|
"filterSex": "Sex",
|
||||||
|
"all": "All"
|
||||||
|
},
|
||||||
|
"applications": {
|
||||||
|
"adoption": "Adoption Application",
|
||||||
|
"foster": "Foster Application",
|
||||||
|
"volunteer": "Volunteer Application",
|
||||||
|
"surrender": "Surrender Application",
|
||||||
|
"thankYou": "Thank you for your application!",
|
||||||
|
"weWillReview": "We will review your application and get back to you soon."
|
||||||
|
},
|
||||||
|
"foster": {
|
||||||
|
"portal": "Foster Portal",
|
||||||
|
"myPets": "My Foster Pets",
|
||||||
|
"submitUpdate": "Submit Update",
|
||||||
|
"requestSupplies": "Request Supplies",
|
||||||
|
"noPets": "You don't have any foster pets assigned yet."
|
||||||
|
},
|
||||||
|
"admin": {
|
||||||
|
"dashboard": "Dashboard",
|
||||||
|
"pets": "Pets",
|
||||||
|
"applications": "Applications",
|
||||||
|
"sponsors": "Sponsors",
|
||||||
|
"donations": "Donations",
|
||||||
|
"expenses": "Expenses",
|
||||||
|
"vets": "Vets",
|
||||||
|
"events": "Events",
|
||||||
|
"content": "Content",
|
||||||
|
"newsletter": "Newsletter",
|
||||||
|
"happyTails": "Happy Tails",
|
||||||
|
"users": "Users",
|
||||||
|
"volunteers": "Volunteers",
|
||||||
|
"fosters": "Fosters",
|
||||||
|
"mentors": "Mentors",
|
||||||
|
"intake": "Intake",
|
||||||
|
"moderation": "Moderation",
|
||||||
|
"reports": "Reports",
|
||||||
|
"analytics": "Analytics",
|
||||||
|
"traffic": "Live Traffic",
|
||||||
|
"messages": "Messages",
|
||||||
|
"import": "Import",
|
||||||
|
"system": "System",
|
||||||
|
"fosterDashboard": "My Fosters"
|
||||||
|
},
|
||||||
|
"setup": {
|
||||||
|
"welcome": "Welcome to FosterFlow!",
|
||||||
|
"letsGetStarted": "Let's get your rescue set up.",
|
||||||
|
"step1Title": "Organization Info",
|
||||||
|
"step2Title": "Branding",
|
||||||
|
"step3Title": "Application Forms",
|
||||||
|
"step4Title": "Integrations",
|
||||||
|
"step5Title": "Import Data",
|
||||||
|
"next": "Next",
|
||||||
|
"previous": "Previous",
|
||||||
|
"finish": "Finish Setup"
|
||||||
|
},
|
||||||
|
"home": {
|
||||||
|
"hero": "Find Your Forever Friend",
|
||||||
|
"heroSubtitle": "We rescue, rehabilitate, and rehome animals in need.",
|
||||||
|
"viewPets": "View Our Pets",
|
||||||
|
"howToHelp": "How to Help",
|
||||||
|
"stats": {
|
||||||
|
"adopted": "Pets Adopted",
|
||||||
|
"inCare": "Currently in Care",
|
||||||
|
"fosters": "Active Fosters"
|
||||||
|
},
|
||||||
|
"upcomingEvents": "Upcoming Events",
|
||||||
|
"recentHappyTails": "Recent Happy Tails"
|
||||||
|
},
|
||||||
|
"contact": {
|
||||||
|
"title": "Contact Us",
|
||||||
|
"send": "Send Message",
|
||||||
|
"sent": "Message sent! We'll get back to you soon."
|
||||||
|
},
|
||||||
|
"newsletter": {
|
||||||
|
"subscribe": "Subscribe to Our Newsletter",
|
||||||
|
"emailPlaceholder": "Your email address",
|
||||||
|
"subscribed": "Thanks for subscribing!"
|
||||||
|
},
|
||||||
|
"donate": {
|
||||||
|
"title": "Donate",
|
||||||
|
"support": "Support Our Mission",
|
||||||
|
"oneTime": "One-Time Donation",
|
||||||
|
"monthly": "Monthly Donation"
|
||||||
|
},
|
||||||
|
"footer": {
|
||||||
|
"quickLinks": "Quick Links",
|
||||||
|
"connect": "Connect With Us",
|
||||||
|
"copyright": "All rights reserved."
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { createI18n } from '$lib/i18n';
|
||||||
|
|
||||||
|
describe('i18n', () => {
|
||||||
|
const i18n = createI18n({
|
||||||
|
'common.save': 'Save',
|
||||||
|
'common.cancel': 'Cancel',
|
||||||
|
'pets.title': 'Our Pets',
|
||||||
|
'pets.count': '{count} pets available',
|
||||||
|
'greeting': 'Hello, {name}!'
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns translated string by key', () => {
|
||||||
|
expect(i18n.t('common.save')).toBe('Save');
|
||||||
|
expect(i18n.t('pets.title')).toBe('Our Pets');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns key when translation is missing', () => {
|
||||||
|
expect(i18n.t('missing.key')).toBe('missing.key');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('interpolates variables', () => {
|
||||||
|
expect(i18n.t('greeting', { name: 'Justin' })).toBe('Hello, Justin!');
|
||||||
|
expect(i18n.t('pets.count', { count: '5' })).toBe('5 pets available');
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user