feat(i18n): internationalize homepage with English translations

- Replace all hardcoded strings in homepage with t() translation calls
- Create English translation file with complete homepage content:
  - Learn by Doing section
  - What You'll Learn (4 skill cards)
  - The Arcade section
  - Your Journey section
  - Create Custom Flashcards section (features and CTA)
- Add home messages to global message aggregator
- Use useTranslations('home') hook for type-safe translations

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Thomas Hallock 2025-11-01 14:46:10 -05:00
parent 0506360117
commit 40cff143c7
4 changed files with 174 additions and 50 deletions

View File

@ -2,6 +2,7 @@
import Link from 'next/link' import Link from 'next/link'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { useTranslations } from 'next-intl'
import { AbacusReact, useAbacusConfig } from '@soroban/abacus-react' import { AbacusReact, useAbacusConfig } from '@soroban/abacus-react'
import { HeroAbacus } from '@/components/HeroAbacus' import { HeroAbacus } from '@/components/HeroAbacus'
import { HomeHeroProvider } from '@/contexts/HomeHeroContext' import { HomeHeroProvider } from '@/contexts/HomeHeroContext'
@ -74,6 +75,7 @@ function MiniAbacus({
} }
export default function HomePage() { export default function HomePage() {
const t = useTranslations('home')
const [selectedSkillIndex, setSelectedSkillIndex] = useState(1) // Default to "Friends techniques" const [selectedSkillIndex, setSelectedSkillIndex] = useState(1) // Default to "Friends techniques"
const fullTutorial = getTutorialForEditor() const fullTutorial = getTutorialForEditor()
@ -83,32 +85,32 @@ export default function HomePage() {
{ {
...fullTutorial, ...fullTutorial,
id: 'read-numbers-demo', id: 'read-numbers-demo',
title: 'Read and Set Numbers', title: t('skills.readNumbers.tutorialTitle'),
description: 'Master abacus number representation from zero to thousands', description: t('skills.readNumbers.tutorialDesc'),
steps: fullTutorial.steps.filter((step) => step.id.startsWith('basic-')), steps: fullTutorial.steps.filter((step) => step.id.startsWith('basic-')),
}, },
// Skill 1: Friends techniques (5 = 2+3) // Skill 1: Friends techniques (5 = 2+3)
{ {
...fullTutorial, ...fullTutorial,
id: 'friends-of-5-demo', id: 'friends-of-5-demo',
title: 'Friends of 5', title: t('skills.friends.tutorialTitle'),
description: 'Add and subtract using complement pairs: 5 = 2+3', description: t('skills.friends.tutorialDesc'),
steps: fullTutorial.steps.filter((step) => step.id === 'complement-2'), steps: fullTutorial.steps.filter((step) => step.id === 'complement-2'),
}, },
// Skill 2: Multiply & divide (12×34) // Skill 2: Multiply & divide (12×34)
{ {
...fullTutorial, ...fullTutorial,
id: 'multiply-demo', id: 'multiply-demo',
title: 'Multiplication', title: t('skills.multiply.tutorialTitle'),
description: 'Fluent multi-digit calculations with advanced techniques', description: t('skills.multiply.tutorialDesc'),
steps: fullTutorial.steps.filter((step) => step.id.includes('complement')).slice(0, 3), steps: fullTutorial.steps.filter((step) => step.id.includes('complement')).slice(0, 3),
}, },
// Skill 3: Mental calculation (Speed math) // Skill 3: Mental calculation (Speed math)
{ {
...fullTutorial, ...fullTutorial,
id: 'mental-calc-demo', id: 'mental-calc-demo',
title: 'Mental Calculation', title: t('skills.mental.tutorialTitle'),
description: 'Visualize and compute without the physical tool (Anzan)', description: t('skills.mental.tutorialDesc'),
steps: fullTutorial.steps.slice(-3), steps: fullTutorial.steps.slice(-3),
}, },
] ]
@ -133,10 +135,10 @@ export default function HomePage() {
mb: '2', mb: '2',
})} })}
> >
Learn by Doing {t('learnByDoing.title')}
</h2> </h2>
<p className={css({ color: 'gray.400', fontSize: 'md', maxW: '2xl', mx: 'auto' })}> <p className={css({ color: 'gray.400', fontSize: 'md', maxW: '2xl', mx: 'auto' })}>
Interactive tutorials teach you step-by-step. Try this example right now: {t('learnByDoing.subtitle')}
</p> </p>
</div> </div>
@ -197,39 +199,39 @@ export default function HomePage() {
mb: '6', mb: '6',
})} })}
> >
What You'll Learn {t('whatYouLearn.title')}
</h3> </h3>
<div className={grid({ columns: { base: 1, lg: 2 }, gap: '5' })}> <div className={grid({ columns: { base: 1, lg: 2 }, gap: '5' })}>
{[ {[
{ {
title: '📖 Read and set numbers', title: t('skills.readNumbers.title'),
desc: 'Master abacus number representation from zero to thousands', desc: t('skills.readNumbers.desc'),
example: '0-9999', example: t('skills.readNumbers.example'),
badge: 'Foundation', badge: t('skills.readNumbers.badge'),
values: [0, 1, 2, 3, 4, 5, 10, 50, 100, 500, 999], values: [0, 1, 2, 3, 4, 5, 10, 50, 100, 500, 999],
columns: 3, columns: 3,
}, },
{ {
title: '🤝 Friends techniques', title: t('skills.friends.title'),
desc: 'Add and subtract using complement pairs and mental shortcuts', desc: t('skills.friends.desc'),
example: '5 = 2+3', example: t('skills.friends.example'),
badge: 'Core', badge: t('skills.friends.badge'),
values: [2, 5, 3], values: [2, 5, 3],
columns: 1, columns: 1,
}, },
{ {
title: '✖️ Multiply & divide', title: t('skills.multiply.title'),
desc: 'Fluent multi-digit calculations with advanced techniques', desc: t('skills.multiply.desc'),
example: '12×34', example: t('skills.multiply.example'),
badge: 'Advanced', badge: t('skills.multiply.badge'),
values: [12, 24, 36, 48], values: [12, 24, 36, 48],
columns: 2, columns: 2,
}, },
{ {
title: '🧠 Mental calculation', title: t('skills.mental.title'),
desc: 'Visualize and compute without the physical tool (Anzan)', desc: t('skills.mental.desc'),
example: 'Speed math', example: t('skills.mental.example'),
badge: 'Expert', badge: t('skills.mental.badge'),
values: [7, 14, 21, 28, 35], values: [7, 14, 21, 28, 35],
columns: 2, columns: 2,
}, },
@ -366,20 +368,17 @@ export default function HomePage() {
mb: '2', mb: '2',
})} })}
> >
The Arcade {t('arcade.title')}
</h2> </h2>
<p className={css({ color: 'gray.400', fontSize: 'md' })}> <p className={css({ color: 'gray.400', fontSize: 'md' })}>{t('arcade.subtitle')}</p>
Single-player challenges and multiplayer battles in networked rooms. Invite
friends to play or watch live.
</p>
</div> </div>
<div className={grid({ columns: { base: 1, sm: 2, lg: 4 }, gap: '5' })}> <div className={grid({ columns: { base: 1, sm: 2, lg: 4 }, gap: '5' })}>
{getAvailableGames().map((game) => { {getAvailableGames().map((game) => {
const playersText = const playersText =
game.manifest.maxPlayers === 1 game.manifest.maxPlayers === 1
? 'Solo challenge' ? t('arcade.soloChallenge')
: `1-${game.manifest.maxPlayers} players` : t('arcade.playersCount', { min: 1, max: game.manifest.maxPlayers })
return ( return (
<GameCard <GameCard
key={game.manifest.name} key={game.manifest.name}
@ -407,11 +406,9 @@ export default function HomePage() {
mb: '2', mb: '2',
})} })}
> >
Your Journey {t('journey.title')}
</h2> </h2>
<p style={{ color: '#e5e7eb', fontSize: '16px' }}> <p style={{ color: '#e5e7eb', fontSize: '16px' }}>{t('journey.subtitle')}</p>
Progress from beginner to master
</p>
</div> </div>
<LevelSliderDisplay /> <LevelSliderDisplay />
@ -428,10 +425,10 @@ export default function HomePage() {
mb: '2', mb: '2',
})} })}
> >
Create Custom Flashcards {t('flashcards.title')}
</h2> </h2>
<p className={css({ color: 'gray.400', fontSize: 'md', maxW: '2xl', mx: 'auto' })}> <p className={css({ color: 'gray.400', fontSize: 'md', maxW: '2xl', mx: 'auto' })}>
Design beautiful flashcards for learning and practice {t('flashcards.subtitle')}
</p> </p>
</div> </div>
@ -457,19 +454,19 @@ export default function HomePage() {
<div className={grid({ columns: { base: 1, md: 3 }, gap: '4', mb: '6' })}> <div className={grid({ columns: { base: 1, md: 3 }, gap: '4', mb: '6' })}>
{[ {[
{ {
icon: '📄', icon: t('flashcards.features.formats.icon'),
title: 'Multiple Formats', title: t('flashcards.features.formats.title'),
desc: 'PDF, PNG, SVG, HTML', desc: t('flashcards.features.formats.desc'),
}, },
{ {
icon: '🎨', icon: t('flashcards.features.customizable.icon'),
title: 'Customizable', title: t('flashcards.features.customizable.title'),
desc: 'Bead shapes, colors, layouts', desc: t('flashcards.features.customizable.desc'),
}, },
{ {
icon: '📐', icon: t('flashcards.features.paperSizes.icon'),
title: 'All Paper Sizes', title: t('flashcards.features.paperSizes.title'),
desc: 'A3, A4, A5, US Letter', desc: t('flashcards.features.paperSizes.desc'),
}, },
].map((feature, i) => ( ].map((feature, i) => (
<div <div
@ -519,7 +516,7 @@ export default function HomePage() {
}, },
})} })}
> >
<span>Create Flashcards</span> <span>{t('flashcards.cta')}</span>
<span></span> <span></span>
</Link> </Link>
</div> </div>

View File

@ -0,0 +1,77 @@
{
"home": {
"learnByDoing": {
"title": "Learn by Doing",
"subtitle": "Interactive tutorials teach you step-by-step. Try this example right now:"
},
"whatYouLearn": {
"title": "What You'll Learn"
},
"skills": {
"readNumbers": {
"title": "📖 Read and set numbers",
"desc": "Master abacus number representation from zero to thousands",
"example": "0-9999",
"badge": "Foundation",
"tutorialTitle": "Read and Set Numbers",
"tutorialDesc": "Master abacus number representation from zero to thousands"
},
"friends": {
"title": "🤝 Friends techniques",
"desc": "Add and subtract using complement pairs and mental shortcuts",
"example": "5 = 2+3",
"badge": "Core",
"tutorialTitle": "Friends of 5",
"tutorialDesc": "Add and subtract using complement pairs: 5 = 2+3"
},
"multiply": {
"title": "✖️ Multiply & divide",
"desc": "Fluent multi-digit calculations with advanced techniques",
"example": "12×34",
"badge": "Advanced",
"tutorialTitle": "Multiplication",
"tutorialDesc": "Fluent multi-digit calculations with advanced techniques"
},
"mental": {
"title": "🧠 Mental calculation",
"desc": "Visualize and compute without the physical tool (Anzan)",
"example": "Speed math",
"badge": "Expert",
"tutorialTitle": "Mental Calculation",
"tutorialDesc": "Visualize and compute without the physical tool (Anzan)"
}
},
"arcade": {
"title": "The Arcade",
"subtitle": "Single-player challenges and multiplayer battles in networked rooms. Invite friends to play or watch live.",
"soloChallenge": "Solo challenge",
"playersCount": "{min}-{max} players"
},
"journey": {
"title": "Your Journey",
"subtitle": "Progress from beginner to master"
},
"flashcards": {
"title": "Create Custom Flashcards",
"subtitle": "Design beautiful flashcards for learning and practice",
"features": {
"formats": {
"icon": "📄",
"title": "Multiple Formats",
"desc": "PDF, PNG, SVG, HTML"
},
"customizable": {
"icon": "🎨",
"title": "Customizable",
"desc": "Bead shapes, colors, layouts"
},
"paperSizes": {
"icon": "📐",
"title": "All Paper Sizes",
"desc": "A3, A4, A5, US Letter"
}
},
"cta": "Create Flashcards"
}
}
}

View File

@ -0,0 +1,15 @@
import de from './de.json'
import en from './en.json'
import es from './es.json'
import hi from './hi.json'
import ja from './ja.json'
import la from './la.json'
export const homeMessages = {
en: en.home,
de: de.home,
ja: ja.home,
hi: hi.home,
es: es.home,
la: la.home,
} as const

View File

@ -0,0 +1,35 @@
import { rithmomachiaMessages } from '@/arcade-games/rithmomachia/messages'
import { homeMessages } from '@/i18n/locales/home/messages'
export type Locale = 'en' | 'de' | 'ja' | 'hi' | 'es' | 'la'
/**
* Deep merge messages from multiple sources
*/
function mergeMessages(...sources: Record<string, any>[]): Record<string, any> {
return sources.reduce((acc, source) => {
for (const [key, value] of Object.entries(source)) {
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
acc[key] = mergeMessages(acc[key] || {}, value)
} else {
acc[key] = value
}
}
return acc
}, {})
}
/**
* Get all messages for a locale by aggregating co-located translations
*/
export async function getMessages(locale: Locale) {
// Common app-wide messages (minimal for now, can expand later)
const common = {
common: {
// Add app-wide translations here as needed
},
}
// Merge all co-located feature messages
return mergeMessages(common, { home: homeMessages[locale] }, rithmomachiaMessages[locale])
}