feat: internationalize tutorial player

This commit is contained in:
Thomas Hallock
2025-11-01 15:53:30 -05:00
parent e48d394940
commit 26d41cfd05
11 changed files with 725 additions and 103 deletions

View File

@@ -1,10 +1,12 @@
'use client'
import { useTranslations } from 'next-intl'
import type { PedagogicalSegment } from '../DecompositionWithReasons'
import { useTutorialUI } from '../TutorialUIContext'
export function CoachBar() {
const ui = useTutorialUI()
const t = useTranslations('tutorial.coachBar')
const seg: PedagogicalSegment | null = ui.activeSegment
if (!ui.showCoachBar || !seg || !seg.readable?.summary) return null
@@ -14,13 +16,13 @@ export function CoachBar() {
return (
<aside className="coachbar" role="status" aria-live="polite" data-test-id="coachbar">
<div className="coachbar__row">
<div className="coachbar__title">{r.title ?? 'Step'}</div>
<div className="coachbar__title">{r.title ?? t('titleFallback')}</div>
{ui.canHideCoachBar && (
<button
type="button"
className="coachbar__hide"
onClick={() => ui.setShowCoachBar(false)}
aria-label="Hide guidance"
aria-label={t('hideAria')}
>
</button>

View File

@@ -1,6 +1,7 @@
'use client'
import * as HoverCard from '@radix-ui/react-hover-card'
import { useTranslations } from 'next-intl'
import type React from 'react'
import { useMemo, useState } from 'react'
import type { TermProvenance, UnifiedStepData } from '../../utils/unifiedStepGenerator'
@@ -61,114 +62,106 @@ export function ReasonTooltip({
const [showDetails, setShowDetails] = useState(false)
const [isOpen, setIsOpen] = useState(false)
const ui = useTutorialUIGate()
const t = useTranslations('tutorial.reasonTooltip')
const rule = reason?.rule ?? segment?.plan[0]?.rule
// Use readable format from segment, enhanced with provenance
const readable = segment?.readable
// Generate enhanced tooltip content using provenance
const getEnhancedTooltipContent = () => {
const enhancedContent = useMemo(() => {
if (!provenance) return null
// For Direct operations, use the enhanced provenance title
if (rule === 'Direct') {
const title = `Add the ${provenance.rhsPlaceName} digit — ${provenance.rhsDigit} ${provenance.rhsPlaceName} (${provenance.rhsValue})`
const subtitle = `From addend ${provenance.rhs}`
const rodChip = readable?.chips.find((c) => /^(this )?rod shows$/i.test(c.label))
const enhancedChips = [
{
label: "Digit we're using",
value: `${provenance.rhsDigit} (${provenance.rhsPlaceName})`,
},
...(() => {
const rodChip = readable?.chips.find((c) => /^(this )?rod shows$/i.test(c.label))
return rodChip ? [{ label: 'Rod shows', value: rodChip.value }] : []
})(),
{
label: 'So we add here',
value: `+${provenance.rhsDigit} ${provenance.rhsPlaceName}${provenance.rhsValue}`,
},
]
return { title, subtitle, chips: enhancedChips }
return {
title: t('directTitle', {
place: provenance.rhsPlaceName,
digit: provenance.rhsDigit,
value: provenance.rhsValue,
}),
subtitle: t('directSubtitle', { addend: provenance.rhs }),
chips: [
{
label: t('digitChip'),
value: `${provenance.rhsDigit} (${provenance.rhsPlaceName})`,
},
...(rodChip ? [{ label: t('rodChip'), value: rodChip.value }] : []),
{
label: t('addHereChip'),
value: `+${provenance.rhsDigit} ${provenance.rhsPlaceName}${provenance.rhsValue}`,
},
],
}
}
// For complement operations, enhance the existing readable content with provenance context
if (readable) {
// Keep the readable title but add provenance context to subtitle
const title = readable.title
const subtitle =
`${readable.subtitle || ''} • From ${provenance.rhsPlaceName} digit ${provenance.rhsDigit} of ${provenance.rhs}`.trim()
const subtitleParts = [
readable.subtitle,
t('subtitleContext', {
addend: provenance.rhs,
place: provenance.rhsPlaceName,
digit: provenance.rhsDigit,
}),
].filter(Boolean)
// Enhance the chips by adding provenance context at the beginning
const enhancedChips = [
{
label: 'Source digit',
value: `${provenance.rhsDigit} from ${provenance.rhs} (${provenance.rhsPlaceName} place)`,
},
...readable.chips,
]
return { title, subtitle, chips: enhancedChips }
return {
title: readable.title,
subtitle: subtitleParts.join(' • '),
chips: [
{
label: t('sourceDigit'),
value: `${provenance.rhsDigit} from ${provenance.rhs} (${provenance.rhsPlaceName} place)`,
},
...readable.chips,
],
}
}
return null
}
}, [provenance, readable, rule, t])
const enhancedContent = useMemo(getEnhancedTooltipContent, [])
const getRuleInfo = (ruleName: PedagogicalRule) => {
switch (ruleName) {
const ruleInfo = useMemo(() => {
switch (rule) {
case 'Direct':
return {
emoji: '✨',
name: 'Direct Move',
description: 'Simple bead movement',
name: t('ruleInfo.Direct.name'),
description: t('ruleInfo.Direct.description'),
color: 'green',
}
case 'FiveComplement':
return {
emoji: '🤝',
name: 'Five Friend',
description: 'Using pairs that make 5',
name: t('ruleInfo.FiveComplement.name'),
description: t('ruleInfo.FiveComplement.description'),
color: 'blue',
}
case 'TenComplement':
return {
emoji: '🔟',
name: 'Ten Friend',
description: 'Using pairs that make 10',
name: t('ruleInfo.TenComplement.name'),
description: t('ruleInfo.TenComplement.description'),
color: 'purple',
}
case 'Cascade':
return {
emoji: '🌊',
name: 'Chain Reaction',
description: 'One move triggers another',
name: t('ruleInfo.Cascade.name'),
description: t('ruleInfo.Cascade.description'),
color: 'orange',
}
default:
return {
emoji: '💭',
name: 'Strategy',
description: 'Abacus technique',
name: t('ruleInfo.Fallback.name'),
description: t('ruleInfo.Fallback.description'),
color: 'gray',
}
}
}
}, [rule, t])
const ruleInfo = useMemo(
() =>
rule
? getRuleInfo(rule)
: {
emoji: '💭',
name: 'Strategy',
description: 'Abacus technique',
color: 'gray',
},
[rule, getRuleInfo]
)
const fromPrefix = t('fromPrefix')
if (!rule) {
return <>{children}</>
@@ -226,12 +219,14 @@ export function ReasonTooltip({
{/* Optional provenance nudge (avoid duplicating subtitle) */}
{provenance &&
!(enhancedContent?.subtitle || readable?.subtitle || '').includes('From ') && (
!(enhancedContent?.subtitle || readable?.subtitle || '').includes(`${fromPrefix} `) && (
<div className="reason-tooltip__reasoning">
<p className="reason-tooltip__explanation-text">
From <strong>{provenance.rhs}</strong>: use the{' '}
<strong>{provenance.rhsPlaceName}</strong> digit (
<strong>{provenance.rhsDigit}</strong>).
{t('reasoning', {
addend: provenance.rhs,
place: provenance.rhsPlaceName,
digit: provenance.rhsDigit,
})}
</p>
</div>
)}
@@ -250,7 +245,7 @@ export function ReasonTooltip({
type="button"
>
<span className="reason-tooltip__details-label">
More details
{t('details.toggle')}
<span
className="reason-tooltip__chevron"
style={{
@@ -282,7 +277,7 @@ export function ReasonTooltip({
{segment?.plan?.some((p) => p.rule === 'Cascade') && readable?.carryPath && (
<div className="reason-tooltip__carry-path">
<p className="reason-tooltip__carry-description">
<strong>Carry path:</strong> {readable.carryPath}
<strong>{t('details.carryPath')}</strong> {readable.carryPath}
</p>
</div>
)}
@@ -297,7 +292,7 @@ export function ReasonTooltip({
type="button"
>
<span className="reason-tooltip__math-label">
Show the math
{t('details.showMath')}
<span
className="reason-tooltip__chevron"
style={{
@@ -338,7 +333,7 @@ export function ReasonTooltip({
type="button"
>
<span className="reason-tooltip__section-title">
Step-by-step breakdown
{t('details.steps')}
<span
className="reason-tooltip__chevron"
style={{
@@ -373,7 +368,9 @@ export function ReasonTooltip({
segment?.readable?.validation &&
!segment.readable.validation.ok && (
<div className="reason-tooltip__dev-warn">
Summary/guard mismatch: {segment.readable.validation.issues.join('; ')}
{t('devWarning', {
issues: segment.readable.validation.issues.join('; '),
})}
</div>
)}
@@ -386,7 +383,7 @@ export function ReasonTooltip({
<code className="reason-tooltip__expanded">{segment.expression}</code>
</div>
<div className="reason-tooltip__label">
{originalValue} becomes {segment.expression}
{t('formula', { original: originalValue, expanded: segment.expression })}
</div>
</div>
)}

View File

@@ -7,6 +7,7 @@ import {
type StepBeadHighlight,
useAbacusDisplay,
} from '@soroban/abacus-react'
import { useTranslations } from 'next-intl'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { css } from '../../../styled-system/css'
import { hstack, stack, vstack } from '../../../styled-system/patterns'
@@ -243,6 +244,7 @@ function TutorialPlayerContent({
onEvent,
className,
}: TutorialPlayerProps) {
const t = useTranslations('tutorial.player')
const [_startTime] = useState(Date.now())
const isProgrammaticChange = useRef(false)
const [showHelpForCurrentStep, setShowHelpForCurrentStep] = useState(false)
@@ -912,7 +914,7 @@ function TutorialPlayerContent({
})
if (!isCorrectBead && !silentErrors) {
const errorMessage = "That's not the highlighted bead. Try clicking the highlighted bead."
const errorMessage = t('error.highlight')
dispatch({
type: 'SET_ERROR',
error: errorMessage,
@@ -932,7 +934,7 @@ function TutorialPlayerContent({
}
}
},
[currentStep, dispatch]
[currentStep, dispatch, silentErrors, t]
)
const handleBeadRef = useCallback((bead: any, element: SVGElement | null) => {
@@ -1125,7 +1127,7 @@ function TutorialPlayerContent({
}, [currentStep.highlightBeads, dynamicColumnHighlights, abacusColumns, theme])
if (!currentStep) {
return <div>No steps available</div>
return <div>{t('noSteps')}</div>
}
return (
@@ -1156,7 +1158,11 @@ function TutorialPlayerContent({
<div>
<h1 className={css({ fontSize: 'xl', fontWeight: 'bold' })}>{tutorial.title}</h1>
<p className={css({ fontSize: 'sm', color: 'gray.600' })}>
Step {currentStepIndex + 1} of {tutorial.steps.length}: {currentStep.title}
{t('header.step', {
current: currentStepIndex + 1,
total: tutorial.steps.length,
title: currentStep.title,
})}
</p>
</div>
@@ -1178,7 +1184,7 @@ function TutorialPlayerContent({
_hover: { bg: 'blue.50' },
})}
>
Debug
{t('controls.debug')}
</button>
<button
onClick={toggleStepList}
@@ -1194,7 +1200,7 @@ function TutorialPlayerContent({
_hover: { bg: 'gray.50' },
})}
>
Steps
{t('controls.steps')}
</button>
{/* Multi-step navigation controls */}
@@ -1212,8 +1218,10 @@ function TutorialPlayerContent({
pl: 3,
})}
>
Multi-Step: {currentMultiStep + 1} /{' '}
{currentStep.multiStepInstructions.length}
{t('controls.multiStep.label', {
current: currentMultiStep + 1,
total: currentStep.multiStepInstructions.length,
})}
</div>
<button
onClick={() => dispatch({ type: 'RESET_MULTI_STEP' })}
@@ -1231,7 +1239,7 @@ function TutorialPlayerContent({
_hover: currentMultiStep === 0 ? {} : { bg: 'orange.50' },
})}
>
First
{t('controls.multiStep.first')}
</button>
<button
onClick={() => previousMultiStep()}
@@ -1249,7 +1257,7 @@ function TutorialPlayerContent({
_hover: currentMultiStep === 0 ? {} : { bg: 'orange.50' },
})}
>
Prev
{t('controls.multiStep.prev')}
</button>
<button
onClick={() => advanceMultiStep()}
@@ -1284,7 +1292,7 @@ function TutorialPlayerContent({
: { bg: 'green.50' },
})}
>
Next
{t('controls.multiStep.next')}
</button>
</>
)}
@@ -1294,7 +1302,7 @@ function TutorialPlayerContent({
checked={uiState.autoAdvance}
onChange={toggleAutoAdvance}
/>
Auto-advance
{t('controls.autoAdvance')}
</label>
</>
)}
@@ -1329,7 +1337,7 @@ function TutorialPlayerContent({
overflowY: 'auto',
})}
>
<h3 className={css({ fontWeight: 'bold', mb: 3 })}>Tutorial Steps</h3>
<h3 className={css({ fontWeight: 'bold', mb: 3 })}>{t('sidebar.title')}</h3>
<div className={stack({ gap: 2 })}>
{tutorial.steps && Array.isArray(tutorial.steps) ? (
tutorial.steps.map((step, index) => (
@@ -1371,7 +1379,7 @@ function TutorialPlayerContent({
py: 4,
})}
>
No tutorial steps available
{t('sidebar.empty')}
</div>
)}
</div>
@@ -1459,7 +1467,7 @@ function TutorialPlayerContent({
textShadow: theme === 'dark' ? 'none' : '0 1px 2px rgba(0,0,0,0.1)',
})}
>
Guidance
{t('guidance.title')}
</p>
{/* Pedagogical decomposition with interactive reasoning */}
@@ -1725,11 +1733,14 @@ function TutorialPlayerContent({
_hover: navigationState.canGoPrevious ? { bg: 'gray.50' } : {},
})}
>
Previous
{t('navigation.previous')}
</button>
<div className={css({ fontSize: 'sm', color: 'gray.600' })}>
Step {currentStepIndex + 1} of {navigationState.totalSteps}
{t('navigation.stepCounter', {
current: currentStepIndex + 1,
total: navigationState.totalSteps,
})}
</div>
<button
@@ -1749,7 +1760,9 @@ function TutorialPlayerContent({
_hover: navigationState.canGoNext || isStepCompleted ? { bg: 'blue.600' } : {},
})}
>
{navigationState.canGoNext ? 'Next →' : 'Complete Tutorial'}
{navigationState.canGoNext
? t('navigation.next')
: t('navigation.complete')}
</button>
</div>
</div>
@@ -1768,12 +1781,12 @@ function TutorialPlayerContent({
overflowY: 'auto',
})}
>
<h3 className={css({ fontWeight: 'bold', mb: 3 })}>Debug Panel</h3>
<h3 className={css({ fontWeight: 'bold', mb: 3 })}>{t('debugPanel.title')}</h3>
<div className={stack({ gap: 4 })}>
{/* Current state */}
<div>
<h4 className={css({ fontWeight: 'medium', mb: 2 })}>Current State</h4>
<h4 className={css({ fontWeight: 'medium', mb: 2 })}>{t('debugPanel.currentState')}</h4>
<div
className={css({
fontSize: 'sm',
@@ -1784,18 +1797,31 @@ function TutorialPlayerContent({
})}
>
<div>
Step: {currentStepIndex + 1}/{navigationState.totalSteps}
{t('debugPanel.step', {
current: currentStepIndex + 1,
total: navigationState.totalSteps,
})}
</div>
<div>{t('debugPanel.value', { value: currentValue })}</div>
<div>{t('debugPanel.target', { value: currentStep.targetValue })}</div>
<div>
{t('debugPanel.completed', {
status: t(
`debugPanel.completedStatus.${isStepCompleted ? 'yes' : 'no'}`
),
})}
</div>
<div>
{t('debugPanel.time', {
seconds: Math.round((Date.now() - stepStartTime) / 1000),
})}
</div>
<div>Value: {currentValue}</div>
<div>Target: {currentStep.targetValue}</div>
<div>Completed: {isStepCompleted ? 'Yes' : 'No'}</div>
<div>Time: {Math.round((Date.now() - stepStartTime) / 1000)}s</div>
</div>
</div>
{/* Event log */}
<div>
<h4 className={css({ fontWeight: 'medium', mb: 2 })}>Event Log</h4>
<h4 className={css({ fontWeight: 'medium', mb: 2 })}>{t('debugPanel.eventLog')}</h4>
<div
className={css({
maxH: '300px',

View File

@@ -0,0 +1,96 @@
{
"tutorial": {
"player": {
"noSteps": "Keine Schritte verfügbar",
"header": {
"step": "Schritt {current} von {total}: {title}"
},
"controls": {
"debug": "Debug",
"steps": "Schritte",
"multiStep": {
"label": "Mehrschritt: {current} / {total}",
"first": "⏮ Anfang",
"prev": "⏪ Zurück",
"next": "Weiter ⏩"
},
"autoAdvance": "Automatisch fortfahren"
},
"sidebar": {
"title": "Tutorial-Schritte",
"empty": "Keine Tutorial-Schritte verfügbar"
},
"guidance": {
"title": "Anleitung"
},
"error": {
"highlight": "Das ist nicht die markierte Perle. Klicke auf die markierte Perle."
},
"navigation": {
"previous": "← Zurück",
"next": "Weiter →",
"complete": "Tutorial abschließen",
"stepCounter": "Schritt {current} von {total}"
},
"debugPanel": {
"title": "Debug-Bereich",
"currentState": "Aktueller Zustand",
"eventLog": "Ereignisprotokoll",
"step": "Schritt: {current}/{total}",
"value": "Wert: {value}",
"target": "Ziel: {value}",
"completed": "Abgeschlossen: {status}",
"completedStatus": {
"yes": "Ja",
"no": "Nein"
},
"time": "Zeit: {seconds}s"
}
},
"coachBar": {
"titleFallback": "Schritt",
"hideAria": "Anleitung ausblenden"
},
"reasonTooltip": {
"fromPrefix": "Von",
"directTitle": "Addiere die {place}-Ziffer — {digit} {place} ({value})",
"directSubtitle": "Von Summand {addend}",
"subtitleContext": "Vom {place}-Digit {digit} von {addend}",
"digitChip": "Verwendete Ziffer",
"rodChip": "Stab zeigt",
"addHereChip": "Hier addieren",
"sourceDigit": "Quellziffer",
"reasoning": "Von {addend}: Nutze die {place}-Ziffer ({digit}).",
"details": {
"toggle": "Weitere Details",
"carryPath": "Übertragsweg:",
"showMath": "Rechnung anzeigen",
"steps": "Schritt-für-Schritt-Aufschlüsselung"
},
"ruleInfo": {
"Direct": {
"name": "Direkter Zug",
"description": "Einfaches Perlenverschieben"
},
"FiveComplement": {
"name": "Fünferfreund",
"description": "Paare verwenden, die 5 ergeben"
},
"TenComplement": {
"name": "Zehnerfreund",
"description": "Paare verwenden, die 10 ergeben"
},
"Cascade": {
"name": "Kettenreaktion",
"description": "Ein Zug löst den nächsten aus"
},
"Fallback": {
"name": "Strategie",
"description": "Abakustechnik"
}
},
"formula": "{original} wird zu {expanded}",
"devWarning": "⚠ Zusammenfassung/Wächter stimmen nicht überein: {issues}"
}
}
}

View File

@@ -0,0 +1,96 @@
{
"tutorial": {
"player": {
"noSteps": "No steps available",
"header": {
"step": "Step {current} of {total}: {title}"
},
"controls": {
"debug": "Debug",
"steps": "Steps",
"multiStep": {
"label": "Multi-step: {current} / {total}",
"first": "⏮ First",
"prev": "⏪ Prev",
"next": "Next ⏩"
},
"autoAdvance": "Auto-advance"
},
"sidebar": {
"title": "Tutorial Steps",
"empty": "No tutorial steps available"
},
"guidance": {
"title": "Guidance"
},
"error": {
"highlight": "That's not the highlighted bead. Try clicking the highlighted bead."
},
"navigation": {
"previous": "← Previous",
"next": "Next →",
"complete": "Complete Tutorial",
"stepCounter": "Step {current} of {total}"
},
"debugPanel": {
"title": "Debug Panel",
"currentState": "Current State",
"eventLog": "Event Log",
"step": "Step: {current}/{total}",
"value": "Value: {value}",
"target": "Target: {value}",
"completed": "Completed: {status}",
"completedStatus": {
"yes": "Yes",
"no": "No"
},
"time": "Time: {seconds}s"
}
},
"coachBar": {
"titleFallback": "Step",
"hideAria": "Hide guidance"
},
"reasonTooltip": {
"fromPrefix": "From",
"directTitle": "Add the {place} digit — {digit} {place} ({value})",
"directSubtitle": "From addend {addend}",
"subtitleContext": "From {place} digit {digit} of {addend}",
"digitChip": "Digit we're using",
"rodChip": "Rod shows",
"addHereChip": "So we add here",
"sourceDigit": "Source digit",
"reasoning": "From {addend}: use the {place} digit ({digit}).",
"details": {
"toggle": "More details",
"carryPath": "Carry path:",
"showMath": "Show the math",
"steps": "Step-by-step breakdown"
},
"ruleInfo": {
"Direct": {
"name": "Direct Move",
"description": "Simple bead movement"
},
"FiveComplement": {
"name": "Five Friend",
"description": "Using pairs that make 5"
},
"TenComplement": {
"name": "Ten Friend",
"description": "Using pairs that make 10"
},
"Cascade": {
"name": "Chain Reaction",
"description": "One move triggers another"
},
"Fallback": {
"name": "Strategy",
"description": "Abacus technique"
}
},
"formula": "{original} becomes {expanded}",
"devWarning": "⚠ Summary/guard mismatch: {issues}"
}
}
}

View File

@@ -0,0 +1,96 @@
{
"tutorial": {
"player": {
"noSteps": "No hay pasos disponibles",
"header": {
"step": "Paso {current} de {total}: {title}"
},
"controls": {
"debug": "Depuración",
"steps": "Pasos",
"multiStep": {
"label": "Multipasos: {current} / {total}",
"first": "⏮ Inicio",
"prev": "⏪ Anterior",
"next": "Siguiente ⏩"
},
"autoAdvance": "Avance automático"
},
"sidebar": {
"title": "Pasos del tutorial",
"empty": "No hay pasos del tutorial disponibles"
},
"guidance": {
"title": "Guía"
},
"error": {
"highlight": "Esa no es la cuenta resaltada. Haz clic en la cuenta resaltada."
},
"navigation": {
"previous": "← Anterior",
"next": "Siguiente →",
"complete": "Completar tutorial",
"stepCounter": "Paso {current} de {total}"
},
"debugPanel": {
"title": "Panel de depuración",
"currentState": "Estado actual",
"eventLog": "Registro de eventos",
"step": "Paso: {current}/{total}",
"value": "Valor: {value}",
"target": "Objetivo: {value}",
"completed": "Completado: {status}",
"completedStatus": {
"yes": "Sí",
"no": "No"
},
"time": "Tiempo: {seconds}s"
}
},
"coachBar": {
"titleFallback": "Paso",
"hideAria": "Ocultar guía"
},
"reasonTooltip": {
"fromPrefix": "De",
"directTitle": "Suma el dígito de {place} — {digit} {place} ({value})",
"directSubtitle": "Del sumando {addend}",
"subtitleContext": "Del dígito de {place} {digit} de {addend}",
"digitChip": "Dígito que usamos",
"rodChip": "La varilla muestra",
"addHereChip": "Sumamos aquí",
"sourceDigit": "Dígito de origen",
"reasoning": "De {addend}: usa el dígito de {place} ({digit}).",
"details": {
"toggle": "Más detalles",
"carryPath": "Ruta del acarreo:",
"showMath": "Mostrar el cálculo",
"steps": "Desglose paso a paso"
},
"ruleInfo": {
"Direct": {
"name": "Movimiento directo",
"description": "Movimiento simple de cuentas"
},
"FiveComplement": {
"name": "Amigo del cinco",
"description": "Usa parejas que suman 5"
},
"TenComplement": {
"name": "Amigo del diez",
"description": "Usa parejas que suman 10"
},
"Cascade": {
"name": "Reacción en cadena",
"description": "Un movimiento provoca otro"
},
"Fallback": {
"name": "Estrategia",
"description": "Técnica de ábaco"
}
},
"formula": "{original} se convierte en {expanded}",
"devWarning": "⚠ Resumen y verificación no coinciden: {issues}"
}
}
}

View File

@@ -0,0 +1,96 @@
{
"tutorial": {
"player": {
"noSteps": "कोई चरण उपलब्ध नहीं है",
"header": {
"step": "चरण {current} / {total}: {title}"
},
"controls": {
"debug": "डिबग",
"steps": "चरण",
"multiStep": {
"label": "बहु-चरण: {current} / {total}",
"first": "⏮ पहला",
"prev": "⏪ पिछला",
"next": "अगला ⏩"
},
"autoAdvance": "स्वचालित आगे बढ़ें"
},
"sidebar": {
"title": "ट्यूटोरियल चरण",
"empty": "कोई ट्यूटोरियल चरण उपलब्ध नहीं हैं"
},
"guidance": {
"title": "मार्गदर्शन"
},
"error": {
"highlight": "यह हाइलाइट किया गया मोती नहीं है। कृपया हाइलाइट किए गए मोती पर क्लिक करें।"
},
"navigation": {
"previous": "← पिछला",
"next": "अगला →",
"complete": "ट्यूटोरियल पूर्ण करें",
"stepCounter": "चरण {current} / {total}"
},
"debugPanel": {
"title": "डिबग पैनल",
"currentState": "वर्तमान स्थिति",
"eventLog": "घटना लॉग",
"step": "चरण: {current}/{total}",
"value": "मान: {value}",
"target": "लक्ष्य: {value}",
"completed": "पूर्ण: {status}",
"completedStatus": {
"yes": "हाँ",
"no": "नहीं"
},
"time": "समय: {seconds} सेकंड"
}
},
"coachBar": {
"titleFallback": "चरण",
"hideAria": "मार्गदर्शन छुपाएँ"
},
"reasonTooltip": {
"fromPrefix": "से",
"directTitle": "{place} अंक जोड़ें — {digit} {place} ({value})",
"directSubtitle": "योग करने वाला {addend} से",
"subtitleContext": "{addend} का {place} अंक {digit}",
"digitChip": "हम जो अंक उपयोग कर रहे हैं",
"rodChip": "दंड दिखाता है",
"addHereChip": "यहाँ जोड़ें",
"sourceDigit": "स्रोत अंक",
"reasoning": "{addend} से: {place} अंक ({digit}) का उपयोग करें।",
"details": {
"toggle": "अधिक विवरण",
"carryPath": "कैरी मार्ग:",
"showMath": "गणना दिखाएँ",
"steps": "क्रमबद्ध विवरण"
},
"ruleInfo": {
"Direct": {
"name": "प्रत्यक्ष चाल",
"description": "सरल मोती चलना"
},
"FiveComplement": {
"name": "पाँच का मित्र",
"description": "ऐसे युग्म जो 5 बनाते हैं"
},
"TenComplement": {
"name": "दस का मित्र",
"description": "ऐसे युग्म जो 10 बनाते हैं"
},
"Cascade": {
"name": "श्रृंखला प्रतिक्रिया",
"description": "एक चाल दूसरी को शुरू करती है"
},
"Fallback": {
"name": "रणनीति",
"description": "अबेकस तकनीक"
}
},
"formula": "{original} {expanded} बन जाता है",
"devWarning": "⚠ सारांश/गार्ड मेल नहीं खाता: {issues}"
}
}
}

View File

@@ -0,0 +1,96 @@
{
"tutorial": {
"player": {
"noSteps": "手順がありません",
"header": {
"step": "ステップ {current}/{total}{title}"
},
"controls": {
"debug": "デバッグ",
"steps": "ステップ一覧",
"multiStep": {
"label": "複数ステップ: {current} / {total}",
"first": "⏮ 最初",
"prev": "⏪ 前へ",
"next": "次へ ⏩"
},
"autoAdvance": "自動で進む"
},
"sidebar": {
"title": "チュートリアルのステップ",
"empty": "利用できるチュートリアルステップがありません"
},
"guidance": {
"title": "ガイダンス"
},
"error": {
"highlight": "強調された珠ではありません。ハイライトされた珠をクリックしてください。"
},
"navigation": {
"previous": "← 前へ",
"next": "次へ →",
"complete": "チュートリアルを完了",
"stepCounter": "ステップ {current}/{total}"
},
"debugPanel": {
"title": "デバッグパネル",
"currentState": "現在の状態",
"eventLog": "イベントログ",
"step": "ステップ: {current}/{total}",
"value": "値: {value}",
"target": "目標: {value}",
"completed": "完了: {status}",
"completedStatus": {
"yes": "はい",
"no": "いいえ"
},
"time": "時間: {seconds}秒"
}
},
"coachBar": {
"titleFallback": "ステップ",
"hideAria": "ガイダンスを隠す"
},
"reasonTooltip": {
"fromPrefix": "追加元",
"directTitle": "{place}の桁を加える — {digit} {place} ({value})",
"directSubtitle": "加数 {addend} から",
"subtitleContext": "{addend} の {place} の桁 {digit}",
"digitChip": "使う桁",
"rodChip": "そろばん列",
"addHereChip": "ここに加える",
"sourceDigit": "元の桁",
"reasoning": "{addend} から: {place}の桁 ({digit}) を使う。",
"details": {
"toggle": "詳細を表示",
"carryPath": "繰り上がり経路:",
"showMath": "計算を表示",
"steps": "ステップごとの説明"
},
"ruleInfo": {
"Direct": {
"name": "直接移動",
"description": "単純な珠の移動"
},
"FiveComplement": {
"name": "五の友",
"description": "5になる組み合わせを使う"
},
"TenComplement": {
"name": "十の友",
"description": "10になる組み合わせを使う"
},
"Cascade": {
"name": "連鎖反応",
"description": "1つの動きが次を引き起こす"
},
"Fallback": {
"name": "戦略",
"description": "そろばんテクニック"
}
},
"formula": "{original} は {expanded} になる",
"devWarning": "⚠ 要約とガードが一致しません: {issues}"
}
}
}

View File

@@ -0,0 +1,96 @@
{
"tutorial": {
"player": {
"noSteps": "Passus nulli praesto sunt",
"header": {
"step": "Gradus {current} ex {total}: {title}"
},
"controls": {
"debug": "Exploratio",
"steps": "Passus",
"multiStep": {
"label": "Gradus multi: {current} / {total}",
"first": "⏮ Primus",
"prev": "⏪ Prius",
"next": "Posterius ⏩"
},
"autoAdvance": "Sponte procedere"
},
"sidebar": {
"title": "Passus praeceptorii",
"empty": "Nulli passus praeceptorii praesto sunt"
},
"guidance": {
"title": "Directio"
},
"error": {
"highlight": "Illa non est gemma illustrata. Gemmam illustratam preme."
},
"navigation": {
"previous": "← Prius",
"next": "Posterius →",
"complete": "Praeceptorium confice",
"stepCounter": "Gradus {current} ex {total}"
},
"debugPanel": {
"title": "Tabula explorationis",
"currentState": "Status praesens",
"eventLog": "Index eventuum",
"step": "Gradus: {current}/{total}",
"value": "Valor: {value}",
"target": "Meta: {value}",
"completed": "Perfectum: {status}",
"completedStatus": {
"yes": "Ita",
"no": "Non"
},
"time": "Tempus: {seconds}s"
}
},
"coachBar": {
"titleFallback": "Gradus",
"hideAria": "Directionem late"
},
"reasonTooltip": {
"fromPrefix": "Ab",
"directTitle": "Adde numerum loci {place} — {digit} {place} ({value})",
"directSubtitle": "Ab addendo {addend}",
"subtitleContext": "Ex numero loci {place} {digit} de {addend}",
"digitChip": "Numerus quo utimur",
"rodChip": "Tignum ostendit",
"addHereChip": "Hic addimus",
"sourceDigit": "Numerus fons",
"reasoning": "Ab {addend}: utere numero loci {place} ({digit}).",
"details": {
"toggle": "Plura indicia",
"carryPath": "Iter translationis:",
"showMath": "Calculum ostende",
"steps": "Explicatio gradatim"
},
"ruleInfo": {
"Direct": {
"name": "Motus directus",
"description": "Simplex motus globuli"
},
"FiveComplement": {
"name": "Amicus quinque",
"description": "Paria quae quinque efficiunt"
},
"TenComplement": {
"name": "Amicus decem",
"description": "Paria quae decem efficiunt"
},
"Cascade": {
"name": "Cataracta",
"description": "Unus motus alium movet"
},
"Fallback": {
"name": "Consilium",
"description": "Ars abaci"
}
},
"formula": "{original} fit {expanded}",
"devWarning": "⚠ Summarium cum custodia non congruit: {issues}"
}
}
}

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 tutorialMessages = {
en: en.tutorial,
de: de.tutorial,
ja: ja.tutorial,
hi: hi.tutorial,
es: es.tutorial,
la: la.tutorial,
} as const

View File

@@ -1,5 +1,6 @@
import { rithmomachiaMessages } from '@/arcade-games/rithmomachia/messages'
import { homeMessages } from '@/i18n/locales/home/messages'
import { tutorialMessages } from '@/i18n/locales/tutorial/messages'
export type Locale = 'en' | 'de' | 'ja' | 'hi' | 'es' | 'la'
@@ -31,5 +32,10 @@ export async function getMessages(locale: Locale) {
}
// Merge all co-located feature messages
return mergeMessages(common, { home: homeMessages[locale] }, rithmomachiaMessages[locale])
return mergeMessages(
common,
{ home: homeMessages[locale] },
{ tutorial: tutorialMessages[locale] },
rithmomachiaMessages[locale]
)
}