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