feat: internationalize tutorial player
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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',
|
||||
|
||||
96
apps/web/src/i18n/locales/tutorial/de.json
Normal file
96
apps/web/src/i18n/locales/tutorial/de.json
Normal 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}"
|
||||
}
|
||||
}
|
||||
}
|
||||
96
apps/web/src/i18n/locales/tutorial/en.json
Normal file
96
apps/web/src/i18n/locales/tutorial/en.json
Normal 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}"
|
||||
}
|
||||
}
|
||||
}
|
||||
96
apps/web/src/i18n/locales/tutorial/es.json
Normal file
96
apps/web/src/i18n/locales/tutorial/es.json
Normal 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}"
|
||||
}
|
||||
}
|
||||
}
|
||||
96
apps/web/src/i18n/locales/tutorial/hi.json
Normal file
96
apps/web/src/i18n/locales/tutorial/hi.json
Normal 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}"
|
||||
}
|
||||
}
|
||||
}
|
||||
96
apps/web/src/i18n/locales/tutorial/ja.json
Normal file
96
apps/web/src/i18n/locales/tutorial/ja.json
Normal 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}"
|
||||
}
|
||||
}
|
||||
}
|
||||
96
apps/web/src/i18n/locales/tutorial/la.json
Normal file
96
apps/web/src/i18n/locales/tutorial/la.json
Normal 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}"
|
||||
}
|
||||
}
|
||||
}
|
||||
15
apps/web/src/i18n/locales/tutorial/messages.ts
Normal file
15
apps/web/src/i18n/locales/tutorial/messages.ts
Normal 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
|
||||
@@ -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]
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user