feat: add i18n system with English locale

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-26 11:56:48 -05:00
co-authored by Claude Sonnet 4.6
parent c06a78b5fc
commit 0367886ac6
3 changed files with 235 additions and 0 deletions
+46
View File
@@ -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;
}
+163
View File
@@ -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."
}
}
+26
View File
@@ -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');
});
});