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