feat(i18n): migrate from react-i18next to next-intl
- Install next-intl package - Configure next-intl middleware for cookie-based locale detection - Set up routing config with supported locales (en, de, ja, hi, es, la) - Create request config for server-side locale resolution - Migrate Rithmomachia guide sections to next-intl - Update translation calls to use next-intl's useTranslations hook - Remove old react-i18next config 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
5acb928314
commit
9016b76024
|
|
@ -1,3 +1,7 @@
|
|||
const createNextIntlPlugin = require('next-intl/plugin')
|
||||
|
||||
const withNextIntl = createNextIntlPlugin('./src/i18n/request.ts')
|
||||
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
eslint: {
|
||||
|
|
@ -63,4 +67,4 @@ const nextConfig = {
|
|||
},
|
||||
}
|
||||
|
||||
module.exports = nextConfig
|
||||
module.exports = withNextIntl(nextConfig)
|
||||
|
|
|
|||
|
|
@ -58,7 +58,6 @@
|
|||
"better-sqlite3": "^12.4.1",
|
||||
"drizzle-orm": "^0.44.6",
|
||||
"emojibase-data": "^16.0.3",
|
||||
"i18next": "^25.6.0",
|
||||
"jose": "^6.1.0",
|
||||
"js-yaml": "^4.1.0",
|
||||
"lib0": "^0.2.114",
|
||||
|
|
@ -67,11 +66,11 @@
|
|||
"nanoid": "^5.1.6",
|
||||
"next": "^14.2.32",
|
||||
"next-auth": "5.0.0-beta.29",
|
||||
"next-intl": "^4.4.0",
|
||||
"python-bridge": "^1.1.0",
|
||||
"qrcode.react": "^4.2.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-i18next": "^16.2.3",
|
||||
"react-resizable-layout": "^0.7.3",
|
||||
"socket.io": "^4.8.1",
|
||||
"socket.io-client": "^4.8.1",
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { useTranslation } from 'react-i18next'
|
||||
import { useTranslations } from 'next-intl'
|
||||
import { css } from '../../../../../styled-system/css'
|
||||
import { RithmomachiaBoard, type ExamplePiece } from '../RithmomachiaBoard'
|
||||
|
||||
|
|
@ -16,7 +16,7 @@ function squaresToCropArea(topLeft: string, bottomRight: string) {
|
|||
}
|
||||
|
||||
export function CaptureSection({ useNativeAbacusNumbers }: { useNativeAbacusNumbers: boolean }) {
|
||||
const { t } = useTranslation()
|
||||
const t = useTranslations('rithmomachia.guide')
|
||||
|
||||
// Example board positions for captures
|
||||
const equalityExample: ExamplePiece[] = [
|
||||
|
|
@ -63,13 +63,10 @@ export function CaptureSection({ useNativeAbacusNumbers }: { useNativeAbacusNumb
|
|||
mb: '16px',
|
||||
})}
|
||||
>
|
||||
{t('guide.capture.title', 'How to Capture')}
|
||||
{t('capture.title')}
|
||||
</h3>
|
||||
<p className={css({ fontSize: '15px', lineHeight: '1.6', mb: '24px', color: '#374151' })}>
|
||||
{t(
|
||||
'guide.capture.description',
|
||||
'You can only capture an enemy piece if your piece value has a mathematical relation to theirs:'
|
||||
)}
|
||||
{t('capture.description')}
|
||||
</p>
|
||||
|
||||
<h4
|
||||
|
|
@ -81,7 +78,7 @@ export function CaptureSection({ useNativeAbacusNumbers }: { useNativeAbacusNumb
|
|||
mt: '20px',
|
||||
})}
|
||||
>
|
||||
{t('guide.capture.simpleTitle', 'Simple Relations (no helper needed)')}
|
||||
{t('capture.simpleTitle')}
|
||||
</h4>
|
||||
|
||||
{/* Equality */}
|
||||
|
|
@ -95,10 +92,10 @@ export function CaptureSection({ useNativeAbacusNumbers }: { useNativeAbacusNumb
|
|||
})}
|
||||
>
|
||||
<p className={css({ fontSize: '14px', fontWeight: 'bold', color: '#111827', mb: '8px' })}>
|
||||
{t('guide.capture.equality', 'Equal')}
|
||||
{t('capture.equality')}
|
||||
</p>
|
||||
<p className={css({ fontSize: '13px', color: '#6b7280', mb: '12px' })}>
|
||||
{t('guide.capture.equalityExample', 'Your 25 captures their 25')}
|
||||
{t('capture.equalityExample')}
|
||||
</p>
|
||||
<div className={css({ display: 'flex', justifyContent: 'center' })}>
|
||||
<RithmomachiaBoard
|
||||
|
|
@ -119,10 +116,7 @@ export function CaptureSection({ useNativeAbacusNumbers }: { useNativeAbacusNumb
|
|||
fontStyle: 'italic',
|
||||
})}
|
||||
>
|
||||
{t(
|
||||
'guide.capture.equalityCaption',
|
||||
'White Circle (25) can capture Black Circle (25) by equality'
|
||||
)}
|
||||
{t('capture.equalityCaption')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
|
@ -137,10 +131,10 @@ export function CaptureSection({ useNativeAbacusNumbers }: { useNativeAbacusNumb
|
|||
})}
|
||||
>
|
||||
<p className={css({ fontSize: '14px', fontWeight: 'bold', color: '#111827', mb: '8px' })}>
|
||||
{t('guide.capture.multiple', 'Multiple / Divisor')}
|
||||
{t('capture.multiple')}
|
||||
</p>
|
||||
<p className={css({ fontSize: '13px', color: '#6b7280', mb: '12px' })}>
|
||||
{t('guide.capture.multipleExample', 'Your 64 captures their 16 (64 ÷ 16 = 4)')}
|
||||
{t('capture.multipleExample')}
|
||||
</p>
|
||||
<div className={css({ display: 'flex', justifyContent: 'center' })}>
|
||||
<RithmomachiaBoard
|
||||
|
|
@ -161,10 +155,7 @@ export function CaptureSection({ useNativeAbacusNumbers }: { useNativeAbacusNumb
|
|||
fontStyle: 'italic',
|
||||
})}
|
||||
>
|
||||
{t(
|
||||
'guide.capture.multipleCaption',
|
||||
'White Square (64) can capture Black Triangle (16) because 64 ÷ 16 = 4'
|
||||
)}
|
||||
{t('capture.multipleCaption')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
|
@ -177,7 +168,7 @@ export function CaptureSection({ useNativeAbacusNumbers }: { useNativeAbacusNumb
|
|||
mt: '24px',
|
||||
})}
|
||||
>
|
||||
{t('guide.capture.advancedTitle', 'Advanced Relations (need one helper piece)')}
|
||||
{t('capture.advancedTitle')}
|
||||
</h4>
|
||||
|
||||
{/* Sum */}
|
||||
|
|
@ -191,10 +182,10 @@ export function CaptureSection({ useNativeAbacusNumbers }: { useNativeAbacusNumb
|
|||
})}
|
||||
>
|
||||
<p className={css({ fontSize: '14px', fontWeight: 'bold', color: '#111827', mb: '8px' })}>
|
||||
{t('guide.capture.sum', 'Sum')}
|
||||
{t('capture.sum')}
|
||||
</p>
|
||||
<p className={css({ fontSize: '13px', color: '#6b7280', mb: '12px' })}>
|
||||
{t('guide.capture.sumExample', 'Your 9 + helper 16 = enemy 25')}
|
||||
{t('capture.sumExample')}
|
||||
</p>
|
||||
<div className={css({ display: 'flex', justifyContent: 'center' })}>
|
||||
<RithmomachiaBoard
|
||||
|
|
@ -215,10 +206,7 @@ export function CaptureSection({ useNativeAbacusNumbers }: { useNativeAbacusNumb
|
|||
fontStyle: 'italic',
|
||||
})}
|
||||
>
|
||||
{t(
|
||||
'guide.capture.sumCaption',
|
||||
'White Circle (9) can capture Black Circle (25) using helper Triangle (16): 9 + 16 = 25'
|
||||
)}
|
||||
{t('capture.sumCaption')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
|
@ -233,10 +221,10 @@ export function CaptureSection({ useNativeAbacusNumbers }: { useNativeAbacusNumb
|
|||
})}
|
||||
>
|
||||
<p className={css({ fontSize: '14px', fontWeight: 'bold', color: '#111827', mb: '8px' })}>
|
||||
{t('guide.capture.difference', 'Difference')}
|
||||
{t('capture.difference')}
|
||||
</p>
|
||||
<p className={css({ fontSize: '13px', color: '#6b7280', mb: '12px' })}>
|
||||
{t('guide.capture.differenceExample', 'Your 30 - helper 10 = enemy 20')}
|
||||
{t('capture.differenceExample')}
|
||||
</p>
|
||||
<div className={css({ display: 'flex', justifyContent: 'center' })}>
|
||||
<RithmomachiaBoard
|
||||
|
|
@ -257,10 +245,7 @@ export function CaptureSection({ useNativeAbacusNumbers }: { useNativeAbacusNumb
|
|||
fontStyle: 'italic',
|
||||
})}
|
||||
>
|
||||
{t(
|
||||
'guide.capture.differenceCaption',
|
||||
'White Triangle (30) can capture Black Triangle (20) using helper Circle (10): 30 - 10 = 20'
|
||||
)}
|
||||
{t('capture.differenceCaption')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
|
@ -275,10 +260,10 @@ export function CaptureSection({ useNativeAbacusNumbers }: { useNativeAbacusNumb
|
|||
})}
|
||||
>
|
||||
<p className={css({ fontSize: '14px', fontWeight: 'bold', color: '#111827', mb: '8px' })}>
|
||||
{t('guide.capture.product', 'Product')}
|
||||
{t('capture.product')}
|
||||
</p>
|
||||
<p className={css({ fontSize: '13px', color: '#6b7280', mb: '12px' })}>
|
||||
{t('guide.capture.productExample', 'Your 5 × helper 5 = enemy 25')}
|
||||
{t('capture.productExample')}
|
||||
</p>
|
||||
<div className={css({ display: 'flex', justifyContent: 'center' })}>
|
||||
<RithmomachiaBoard
|
||||
|
|
@ -299,10 +284,7 @@ export function CaptureSection({ useNativeAbacusNumbers }: { useNativeAbacusNumb
|
|||
fontStyle: 'italic',
|
||||
})}
|
||||
>
|
||||
{t(
|
||||
'guide.capture.productCaption',
|
||||
'White Circle (5) can capture Black Circle (25) using helper Circle (5): 5 × 5 = 25'
|
||||
)}
|
||||
{t('capture.productCaption')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
|
@ -317,10 +299,10 @@ export function CaptureSection({ useNativeAbacusNumbers }: { useNativeAbacusNumb
|
|||
})}
|
||||
>
|
||||
<p className={css({ fontSize: '14px', fontWeight: 'bold', color: '#111827', mb: '8px' })}>
|
||||
{t('guide.capture.ratio', 'Ratio')}
|
||||
{t('capture.ratio')}
|
||||
</p>
|
||||
<p className={css({ fontSize: '13px', color: '#6b7280', mb: '12px' })}>
|
||||
{t('guide.capture.ratioExample', 'Your 20 ÷ helper 4 = enemy 5')}
|
||||
{t('capture.ratioExample')}
|
||||
</p>
|
||||
<div className={css({ display: 'flex', justifyContent: 'center' })}>
|
||||
<RithmomachiaBoard
|
||||
|
|
@ -341,10 +323,7 @@ export function CaptureSection({ useNativeAbacusNumbers }: { useNativeAbacusNumb
|
|||
fontStyle: 'italic',
|
||||
})}
|
||||
>
|
||||
{t(
|
||||
'guide.capture.ratioCaption',
|
||||
'White Triangle (20) can capture Black Circle (5) using helper Circle (4): 20 ÷ 4 = 5'
|
||||
)}
|
||||
{t('capture.ratioCaption')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
|
@ -358,13 +337,10 @@ export function CaptureSection({ useNativeAbacusNumbers }: { useNativeAbacusNumb
|
|||
})}
|
||||
>
|
||||
<p className={css({ fontSize: '14px', fontWeight: 'bold', color: '#1e40af', mb: '8px' })}>
|
||||
{t('guide.capture.helpersTitle', '💡 What are helpers?')}
|
||||
{t('capture.helpersTitle')}
|
||||
</p>
|
||||
<p className={css({ fontSize: '14px', color: '#1e3a8a', lineHeight: '1.6' })}>
|
||||
{t(
|
||||
'guide.capture.helpersDescription',
|
||||
'Helpers are your other pieces still on the board. They stay where they are and just provide their value for the math. The game shows you valid captures when you select a piece.'
|
||||
)}
|
||||
{t('capture.helpersDescription')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { useTranslation } from 'react-i18next'
|
||||
import { useTranslations } from 'next-intl'
|
||||
import { css } from '../../../../../styled-system/css'
|
||||
import { RithmomachiaBoard, type ExamplePiece } from '../RithmomachiaBoard'
|
||||
|
||||
|
|
@ -16,7 +16,7 @@ function squaresToCropArea(topLeft: string, bottomRight: string) {
|
|||
}
|
||||
|
||||
export function HarmonySection({ useNativeAbacusNumbers }: { useNativeAbacusNumbers: boolean }) {
|
||||
const { t } = useTranslation()
|
||||
const t = useTranslations('rithmomachia.guide')
|
||||
|
||||
// Example board positions for harmonies (White pieces in Black's territory: rows 5-8)
|
||||
const arithmeticExample: ExamplePiece[] = [
|
||||
|
|
@ -47,19 +47,13 @@ export function HarmonySection({ useNativeAbacusNumbers }: { useNativeAbacusNumb
|
|||
mb: '16px',
|
||||
})}
|
||||
>
|
||||
{t('guide.harmony.title', 'Harmonies: The Elegant Victory')}
|
||||
{t('harmony.title')}
|
||||
</h3>
|
||||
<p className={css({ fontSize: '15px', lineHeight: '1.6', mb: '8px', color: '#374151' })}>
|
||||
{t(
|
||||
'guide.harmony.intro',
|
||||
'A Harmony (also called a "Proper Victory") is the most sophisticated way to win. Get 3 of your pieces into enemy territory arranged in a straight line where their values form a mathematical pattern.'
|
||||
)}
|
||||
{t('harmony.intro')}
|
||||
</p>
|
||||
<p className={css({ fontSize: '14px', lineHeight: '1.6', mb: '24px', color: '#6b7280' })}>
|
||||
{t(
|
||||
'guide.harmony.introDetail',
|
||||
'Think of it like getting three numbers in a sequence—but the sequences follow special mathematical rules from ancient philosophy and music theory.'
|
||||
)}
|
||||
{t('harmony.introDetail')}
|
||||
</p>
|
||||
|
||||
<div className={css({ display: 'flex', flexDirection: 'column', gap: '24px' })}>
|
||||
|
|
@ -75,13 +69,10 @@ export function HarmonySection({ useNativeAbacusNumbers }: { useNativeAbacusNumb
|
|||
<h4
|
||||
className={css({ fontSize: '18px', fontWeight: 'bold', color: '#15803d', mb: '8px' })}
|
||||
>
|
||||
{t('guide.harmony.arithmetic', '1. Arithmetic Progression (Easiest to Understand)')}
|
||||
{t('harmony.arithmetic')}
|
||||
</h4>
|
||||
<p className={css({ fontSize: '14px', color: '#166534', mb: '12px', lineHeight: '1.6' })}>
|
||||
{t(
|
||||
'guide.harmony.arithmeticDesc',
|
||||
'The middle number is exactly halfway between the other two. In other words, the differences are equal.'
|
||||
)}
|
||||
{t('harmony.arithmeticDesc')}
|
||||
</p>
|
||||
|
||||
<div
|
||||
|
|
@ -101,7 +92,7 @@ export function HarmonySection({ useNativeAbacusNumbers }: { useNativeAbacusNumb
|
|||
mb: '8px',
|
||||
})}
|
||||
>
|
||||
{t('guide.harmony.howToCheck', 'How to check:')}
|
||||
{t('harmony.howToCheck')}
|
||||
</p>
|
||||
<p
|
||||
className={css({
|
||||
|
|
@ -112,18 +103,17 @@ export function HarmonySection({ useNativeAbacusNumbers }: { useNativeAbacusNumb
|
|||
mb: '12px',
|
||||
})}
|
||||
>
|
||||
{t('guide.harmony.arithmeticFormula', 'Middle × 2 = First + Last')}
|
||||
{t('harmony.arithmeticFormula')}
|
||||
</p>
|
||||
|
||||
<div className={css({ fontSize: '13px', color: '#166534', lineHeight: '1.8' })}>
|
||||
<p className={css({ fontWeight: 'bold', mb: '4px' })}>
|
||||
{t('guide.harmony.example', 'Example:')} 6, 9, 12
|
||||
{t('harmony.example')} 6, 9, 12
|
||||
</p>
|
||||
<p>
|
||||
{t('guide.harmony.differences', 'Differences:')} 9−6=3, 12−9=3{' '}
|
||||
{t('guide.harmony.equal', '(equal!)')}
|
||||
{t('harmony.differences')} 9−6=3, 12−9=3 {t('harmony.equal')}
|
||||
</p>
|
||||
<p>{t('guide.harmony.check', 'Check:')} 9×2 = 18 = 6+12 ✓</p>
|
||||
<p>{t('harmony.check')} 9×2 = 18 = 6+12 ✓</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -146,10 +136,7 @@ export function HarmonySection({ useNativeAbacusNumbers }: { useNativeAbacusNumb
|
|||
mb: '12px',
|
||||
})}
|
||||
>
|
||||
{t(
|
||||
'guide.harmony.arithmeticCaption',
|
||||
'White pieces 6, 9, 12 in a row in enemy territory form an arithmetic progression'
|
||||
)}
|
||||
{t('harmony.arithmeticCaption')}
|
||||
</p>
|
||||
|
||||
<div
|
||||
|
|
@ -161,11 +148,7 @@ export function HarmonySection({ useNativeAbacusNumbers }: { useNativeAbacusNumb
|
|||
})}
|
||||
>
|
||||
<p className={css({ fontSize: '13px', color: '#15803d' })}>
|
||||
<strong>{t('guide.harmony.strategyTip', 'Strategy tip:')}</strong>{' '}
|
||||
{t(
|
||||
'guide.harmony.arithmeticTip',
|
||||
'Your small circles (2-9) and many triangles naturally form arithmetic progressions. Look for three pieces where the gaps are equal!'
|
||||
)}
|
||||
<strong>{t('harmony.strategyTip')}</strong> {t('harmony.arithmeticTip')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -182,13 +165,10 @@ export function HarmonySection({ useNativeAbacusNumbers }: { useNativeAbacusNumb
|
|||
<h4
|
||||
className={css({ fontSize: '18px', fontWeight: 'bold', color: '#92400e', mb: '8px' })}
|
||||
>
|
||||
{t('guide.harmony.geometric', '2. Geometric Progression (Powers and Multiples)')}
|
||||
{t('harmony.geometric')}
|
||||
</h4>
|
||||
<p className={css({ fontSize: '14px', color: '#78350f', mb: '12px', lineHeight: '1.6' })}>
|
||||
{t(
|
||||
'guide.harmony.geometricDesc',
|
||||
'Each number is multiplied by the same amount to get the next. The ratios are equal.'
|
||||
)}
|
||||
{t('harmony.geometricDesc')}
|
||||
</p>
|
||||
|
||||
<div
|
||||
|
|
@ -208,7 +188,7 @@ export function HarmonySection({ useNativeAbacusNumbers }: { useNativeAbacusNumb
|
|||
mb: '8px',
|
||||
})}
|
||||
>
|
||||
{t('guide.harmony.howToCheck', 'How to check:')}
|
||||
{t('harmony.howToCheck')}
|
||||
</p>
|
||||
<p
|
||||
className={css({
|
||||
|
|
@ -219,18 +199,17 @@ export function HarmonySection({ useNativeAbacusNumbers }: { useNativeAbacusNumb
|
|||
mb: '12px',
|
||||
})}
|
||||
>
|
||||
{t('guide.harmony.geometricFormula', 'Middle² = First × Last')}
|
||||
{t('harmony.geometricFormula')}
|
||||
</p>
|
||||
|
||||
<div className={css({ fontSize: '13px', color: '#78350f', lineHeight: '1.8' })}>
|
||||
<p className={css({ fontWeight: 'bold', mb: '4px' })}>
|
||||
{t('guide.harmony.example', 'Example:')} 4, 8, 16
|
||||
{t('harmony.example')} 4, 8, 16
|
||||
</p>
|
||||
<p>
|
||||
{t('guide.harmony.ratios', 'Ratios:')} 8÷4=2, 16÷8=2{' '}
|
||||
{t('guide.harmony.equal', '(equal!)')}
|
||||
{t('harmony.ratios')} 8÷4=2, 16÷8=2 {t('harmony.equal')}
|
||||
</p>
|
||||
<p>{t('guide.harmony.check', 'Check:')} 8² = 64 = 4×16 ✓</p>
|
||||
<p>{t('harmony.check')} 8² = 64 = 4×16 ✓</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -253,10 +232,7 @@ export function HarmonySection({ useNativeAbacusNumbers }: { useNativeAbacusNumb
|
|||
mb: '12px',
|
||||
})}
|
||||
>
|
||||
{t(
|
||||
'guide.harmony.geometricCaption',
|
||||
'White pieces 4, 8, 16 in a row in enemy territory form a geometric progression'
|
||||
)}
|
||||
{t('harmony.geometricCaption')}
|
||||
</p>
|
||||
|
||||
<div
|
||||
|
|
@ -268,11 +244,7 @@ export function HarmonySection({ useNativeAbacusNumbers }: { useNativeAbacusNumb
|
|||
})}
|
||||
>
|
||||
<p className={css({ fontSize: '13px', color: '#92400e' })}>
|
||||
<strong>{t('guide.harmony.strategyTip', 'Strategy tip:')}</strong>{' '}
|
||||
{t(
|
||||
'guide.harmony.geometricTip',
|
||||
'Square values (4, 9, 16, 25, 36, 49, 64, 81) work great here! For example, 4-16-64 (squares of 2, 4, 8).'
|
||||
)}
|
||||
<strong>{t('harmony.strategyTip')}</strong> {t('harmony.geometricTip')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -289,13 +261,10 @@ export function HarmonySection({ useNativeAbacusNumbers }: { useNativeAbacusNumb
|
|||
<h4
|
||||
className={css({ fontSize: '18px', fontWeight: 'bold', color: '#1e40af', mb: '8px' })}
|
||||
>
|
||||
{t('guide.harmony.harmonic', '3. Harmonic Progression (Music-Based, Trickiest)')}
|
||||
{t('harmony.harmonic')}
|
||||
</h4>
|
||||
<p className={css({ fontSize: '14px', color: '#1e3a8a', mb: '12px', lineHeight: '1.6' })}>
|
||||
{t(
|
||||
'guide.harmony.harmonicDesc',
|
||||
'Named after musical harmonies. The pattern is: the ratio of the outer numbers equals the ratio of their differences from the middle.'
|
||||
)}
|
||||
{t('harmony.harmonicDesc')}
|
||||
</p>
|
||||
|
||||
<div
|
||||
|
|
@ -315,7 +284,7 @@ export function HarmonySection({ useNativeAbacusNumbers }: { useNativeAbacusNumb
|
|||
mb: '8px',
|
||||
})}
|
||||
>
|
||||
{t('guide.harmony.howToCheck', 'How to check:')}
|
||||
{t('harmony.howToCheck')}
|
||||
</p>
|
||||
<p
|
||||
className={css({
|
||||
|
|
@ -326,14 +295,14 @@ export function HarmonySection({ useNativeAbacusNumbers }: { useNativeAbacusNumb
|
|||
mb: '12px',
|
||||
})}
|
||||
>
|
||||
{t('guide.harmony.harmonicFormula', '2 × First × Last = Middle × (First + Last)')}
|
||||
{t('harmony.harmonicFormula')}
|
||||
</p>
|
||||
|
||||
<div className={css({ fontSize: '13px', color: '#1e3a8a', lineHeight: '1.8' })}>
|
||||
<p className={css({ fontWeight: 'bold', mb: '4px' })}>
|
||||
{t('guide.harmony.example', 'Example:')} 6, 8, 12
|
||||
{t('harmony.example')} 6, 8, 12
|
||||
</p>
|
||||
<p>{t('guide.harmony.check', 'Check:')} 2×6×12 = 144 = 8×(6+12) = 8×18 ✓</p>
|
||||
<p>{t('harmony.check')} 2×6×12 = 144 = 8×(6+12) = 8×18 ✓</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -356,10 +325,7 @@ export function HarmonySection({ useNativeAbacusNumbers }: { useNativeAbacusNumb
|
|||
mb: '12px',
|
||||
})}
|
||||
>
|
||||
{t(
|
||||
'guide.harmony.harmonicCaption',
|
||||
'White pieces 6, 8, 12 in a row in enemy territory form a harmonic progression'
|
||||
)}
|
||||
{t('harmony.harmonicCaption')}
|
||||
</p>
|
||||
|
||||
<div
|
||||
|
|
@ -371,11 +337,7 @@ export function HarmonySection({ useNativeAbacusNumbers }: { useNativeAbacusNumb
|
|||
})}
|
||||
>
|
||||
<p className={css({ fontSize: '13px', color: '#1e40af' })}>
|
||||
<strong>{t('guide.harmony.strategyTip', 'Strategy tip:')}</strong>{' '}
|
||||
{t(
|
||||
'guide.harmony.harmonicTip',
|
||||
'Harmonic progressions are rarer. Memorize common triads: (3,4,6), (4,6,12), (6,8,12), (6,10,15), (8,12,24).'
|
||||
)}
|
||||
<strong>{t('harmony.strategyTip')}</strong> {t('harmony.harmonicTip')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -392,43 +354,23 @@ export function HarmonySection({ useNativeAbacusNumbers }: { useNativeAbacusNumb
|
|||
})}
|
||||
>
|
||||
<p className={css({ fontSize: '16px', fontWeight: 'bold', color: '#991b1b', mb: '12px' })}>
|
||||
{t('guide.harmony.rulesTitle', '⚠️ Harmony Rules You Must Follow')}
|
||||
{t('harmony.rulesTitle')}
|
||||
</p>
|
||||
<ul className={css({ fontSize: '14px', color: '#7f1d1d', lineHeight: '1.8', pl: '20px' })}>
|
||||
<li>
|
||||
<strong>{t('guide.harmony.enemyTerritoryTitle', 'Enemy Territory Only:')}</strong>{' '}
|
||||
{t(
|
||||
'guide.harmony.enemyTerritory',
|
||||
"All 3 pieces must be in your opponent's half (White needs rows 5-8, Black needs rows 1-4)"
|
||||
)}
|
||||
<strong>{t('harmony.enemyTerritoryTitle')}</strong> {t('harmony.enemyTerritory')}
|
||||
</li>
|
||||
<li>
|
||||
<strong>{t('guide.harmony.straightLineTitle', 'Straight Line:')}</strong>{' '}
|
||||
{t(
|
||||
'guide.harmony.straightLine',
|
||||
'The 3 pieces must form a row, column, or diagonal—no scattered formations'
|
||||
)}
|
||||
<strong>{t('harmony.straightLineTitle')}</strong> {t('harmony.straightLine')}
|
||||
</li>
|
||||
<li>
|
||||
<strong>{t('guide.harmony.adjacentTitle', 'Adjacent Placement:')}</strong>{' '}
|
||||
{t(
|
||||
'guide.harmony.adjacent',
|
||||
'In this implementation, the 3 pieces must be next to each other (no gaps)'
|
||||
)}
|
||||
<strong>{t('harmony.adjacentTitle')}</strong> {t('harmony.adjacent')}
|
||||
</li>
|
||||
<li>
|
||||
<strong>{t('guide.harmony.survivalTitle', 'Survival Rule:')}</strong>{' '}
|
||||
{t(
|
||||
'guide.harmony.survival',
|
||||
'When you declare a harmony, your opponent gets ONE turn to break it by capturing or moving a piece'
|
||||
)}
|
||||
<strong>{t('harmony.survivalTitle')}</strong> {t('harmony.survival')}
|
||||
</li>
|
||||
<li>
|
||||
<strong>{t('guide.harmony.victoryTitle', 'Victory:')}</strong>{' '}
|
||||
{t(
|
||||
'guide.harmony.victoryRule',
|
||||
'If your harmony survives until your next turn starts—you win!'
|
||||
)}
|
||||
<strong>{t('harmony.victoryTitle')}</strong> {t('harmony.victoryRule')}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
|
@ -443,7 +385,7 @@ export function HarmonySection({ useNativeAbacusNumbers }: { useNativeAbacusNumb
|
|||
mb: '16px',
|
||||
})}
|
||||
>
|
||||
{t('guide.harmony.strategyTitle', 'Strategy: How to Build Harmonies')}
|
||||
{t('harmony.strategyTitle')}
|
||||
</h3>
|
||||
|
||||
<div className={css({ display: 'flex', flexDirection: 'column', gap: '16px' })}>
|
||||
|
|
@ -452,13 +394,10 @@ export function HarmonySection({ useNativeAbacusNumbers }: { useNativeAbacusNumb
|
|||
<h4
|
||||
className={css({ fontSize: '15px', fontWeight: 'bold', color: '#374151', mb: '8px' })}
|
||||
>
|
||||
{t('guide.harmony.startWith2Title', 'Start with 2, Add the Third')}
|
||||
{t('harmony.startWith2Title')}
|
||||
</h4>
|
||||
<p className={css({ fontSize: '14px', color: '#6b7280', lineHeight: '1.6' })}>
|
||||
{t(
|
||||
'guide.harmony.startWith2',
|
||||
"Get two pieces into enemy territory first. Calculate which third piece would complete a progression, then advance that piece. Your opponent may not notice the threat until it's too late!"
|
||||
)}
|
||||
{t('harmony.startWith2')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
|
@ -467,13 +406,10 @@ export function HarmonySection({ useNativeAbacusNumbers }: { useNativeAbacusNumb
|
|||
<h4
|
||||
className={css({ fontSize: '15px', fontWeight: 'bold', color: '#374151', mb: '8px' })}
|
||||
>
|
||||
{t('guide.harmony.useCommonTitle', 'Use Common Values')}
|
||||
{t('harmony.useCommonTitle')}
|
||||
</h4>
|
||||
<p className={css({ fontSize: '14px', color: '#6b7280', lineHeight: '1.6' })}>
|
||||
{t(
|
||||
'guide.harmony.useCommon',
|
||||
'Pieces like 6, 8, 9, 12, 16 appear in multiple progressions. If you have these in enemy territory, calculate all possible third pieces that would complete a pattern.'
|
||||
)}
|
||||
{t('harmony.useCommon')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
|
@ -482,13 +418,10 @@ export function HarmonySection({ useNativeAbacusNumbers }: { useNativeAbacusNumb
|
|||
<h4
|
||||
className={css({ fontSize: '15px', fontWeight: 'bold', color: '#374151', mb: '8px' })}
|
||||
>
|
||||
{t('guide.harmony.protectTitle', 'Protect the Line')}
|
||||
{t('harmony.protectTitle')}
|
||||
</h4>
|
||||
<p className={css({ fontSize: '14px', color: '#6b7280', lineHeight: '1.6' })}>
|
||||
{t(
|
||||
'guide.harmony.protect',
|
||||
'While building your harmony, position other pieces to defend your advancing pieces. One capture breaks the progression!'
|
||||
)}
|
||||
{t('harmony.protect')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
|
@ -497,13 +430,10 @@ export function HarmonySection({ useNativeAbacusNumbers }: { useNativeAbacusNumb
|
|||
<h4
|
||||
className={css({ fontSize: '15px', fontWeight: 'bold', color: '#374151', mb: '8px' })}
|
||||
>
|
||||
{t('guide.harmony.blockTitle', "Block Opponent's Harmonies")}
|
||||
{t('harmony.blockTitle')}
|
||||
</h4>
|
||||
<p className={css({ fontSize: '14px', color: '#6b7280', lineHeight: '1.6' })}>
|
||||
{t(
|
||||
'guide.harmony.block',
|
||||
'If your opponent has 2 pieces in your territory forming part of a progression, identify which third piece would complete it. Block that square or capture one of the two pieces immediately.'
|
||||
)}
|
||||
{t('harmony.block')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
|
@ -512,13 +442,10 @@ export function HarmonySection({ useNativeAbacusNumbers }: { useNativeAbacusNumb
|
|||
<h4
|
||||
className={css({ fontSize: '15px', fontWeight: 'bold', color: '#374151', mb: '8px' })}
|
||||
>
|
||||
{t('guide.harmony.calculateTitle', 'Calculate Before You Declare')}
|
||||
{t('harmony.calculateTitle')}
|
||||
</h4>
|
||||
<p className={css({ fontSize: '14px', color: '#6b7280', lineHeight: '1.6' })}>
|
||||
{t(
|
||||
'guide.harmony.calculate',
|
||||
'Before declaring harmony, examine if your opponent can capture any of the 3 pieces on their turn. If they can, either protect those pieces first or wait for a safer moment.'
|
||||
)}
|
||||
{t('harmony.calculate')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -534,7 +461,7 @@ export function HarmonySection({ useNativeAbacusNumbers }: { useNativeAbacusNumb
|
|||
mb: '16px',
|
||||
})}
|
||||
>
|
||||
{t('guide.harmony.quickRefTitle', '💡 Quick Reference: Common Harmonies in Your Army')}
|
||||
{t('harmony.quickRefTitle')}
|
||||
</h3>
|
||||
|
||||
<div
|
||||
|
|
@ -561,7 +488,7 @@ export function HarmonySection({ useNativeAbacusNumbers }: { useNativeAbacusNumb
|
|||
mb: '8px',
|
||||
})}
|
||||
>
|
||||
{t('guide.harmony.arithmetic', 'Arithmetic')}
|
||||
{t('harmony.arithmetic')}
|
||||
</h4>
|
||||
<ul
|
||||
className={css({
|
||||
|
|
@ -598,7 +525,7 @@ export function HarmonySection({ useNativeAbacusNumbers }: { useNativeAbacusNumb
|
|||
mb: '8px',
|
||||
})}
|
||||
>
|
||||
{t('guide.harmony.geometric', 'Geometric')}
|
||||
{t('harmony.geometric')}
|
||||
</h4>
|
||||
<ul
|
||||
className={css({
|
||||
|
|
@ -635,7 +562,7 @@ export function HarmonySection({ useNativeAbacusNumbers }: { useNativeAbacusNumb
|
|||
mb: '8px',
|
||||
})}
|
||||
>
|
||||
{t('guide.harmony.harmonic', 'Harmonic')}
|
||||
{t('harmony.harmonic')}
|
||||
</h4>
|
||||
<ul
|
||||
className={css({
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import { useTranslation } from 'react-i18next'
|
||||
import { useTranslations } from 'next-intl'
|
||||
import { css } from '../../../../../styled-system/css'
|
||||
import { RithmomachiaBoard, type ExamplePiece } from '../RithmomachiaBoard'
|
||||
|
||||
export function OverviewSection({ useNativeAbacusNumbers }: { useNativeAbacusNumbers: boolean }) {
|
||||
const { t } = useTranslation()
|
||||
const t = useTranslations('rithmomachia.guide')
|
||||
|
||||
// Initial board setup - full starting position
|
||||
const initialSetup: ExamplePiece[] = [
|
||||
|
|
@ -75,13 +75,10 @@ export function OverviewSection({ useNativeAbacusNumbers }: { useNativeAbacusNum
|
|||
mb: '16px',
|
||||
})}
|
||||
>
|
||||
{t('guide.overview.goalTitle', 'Goal of the Game')}
|
||||
{t('overview.goalTitle')}
|
||||
</h3>
|
||||
<p className={css({ fontSize: '16px', lineHeight: '1.6', mb: '20px', color: '#374151' })}>
|
||||
{t(
|
||||
'guide.overview.goal',
|
||||
'Arrange 3 of your pieces in enemy territory to form a mathematical progression, survive one opponent turn, and win.'
|
||||
)}
|
||||
{t('overview.goal')}
|
||||
</p>
|
||||
|
||||
<h3
|
||||
|
|
@ -93,7 +90,7 @@ export function OverviewSection({ useNativeAbacusNumbers }: { useNativeAbacusNum
|
|||
mt: '24px',
|
||||
})}
|
||||
>
|
||||
{t('guide.overview.boardTitle', 'The Board')}
|
||||
{t('overview.boardTitle')}
|
||||
</h3>
|
||||
|
||||
<div className={css({ mb: '20px' })}>
|
||||
|
|
@ -106,10 +103,7 @@ export function OverviewSection({ useNativeAbacusNumbers }: { useNativeAbacusNum
|
|||
</div>
|
||||
|
||||
<p className={css({ fontSize: '14px', color: '#6b7280', mb: '20px', fontStyle: 'italic' })}>
|
||||
{t(
|
||||
'guide.overview.boardCaption',
|
||||
'The starting position - Black on the left, White on the right'
|
||||
)}
|
||||
{t('overview.boardCaption')}
|
||||
</p>
|
||||
|
||||
<ul
|
||||
|
|
@ -121,19 +115,9 @@ export function OverviewSection({ useNativeAbacusNumbers }: { useNativeAbacusNum
|
|||
color: '#374151',
|
||||
})}
|
||||
>
|
||||
<li>{t('guide.overview.boardSize', '8 rows × 16 columns (columns A-P, rows 1-8)')}</li>
|
||||
<li>
|
||||
{t(
|
||||
'guide.overview.territory',
|
||||
'Your half: Black controls rows 5-8, White controls rows 1-4'
|
||||
)}
|
||||
</li>
|
||||
<li>
|
||||
{t(
|
||||
'guide.overview.enemyTerritory',
|
||||
'Enemy territory: Where you need to build your winning progression'
|
||||
)}
|
||||
</li>
|
||||
<li>{t('overview.boardSize')}</li>
|
||||
<li>{t('overview.territory')}</li>
|
||||
<li>{t('overview.enemyTerritory')}</li>
|
||||
</ul>
|
||||
|
||||
<h3
|
||||
|
|
@ -145,7 +129,7 @@ export function OverviewSection({ useNativeAbacusNumbers }: { useNativeAbacusNum
|
|||
mt: '24px',
|
||||
})}
|
||||
>
|
||||
{t('guide.overview.howToPlayTitle', 'How to Play')}
|
||||
{t('overview.howToPlayTitle')}
|
||||
</h3>
|
||||
<ol
|
||||
className={css({
|
||||
|
|
@ -155,11 +139,11 @@ export function OverviewSection({ useNativeAbacusNumbers }: { useNativeAbacusNum
|
|||
color: '#374151',
|
||||
})}
|
||||
>
|
||||
<li>{t('guide.overview.step1', 'Move your pieces toward the center')}</li>
|
||||
<li>{t('guide.overview.step2', 'Look for ways to capture using math')}</li>
|
||||
<li>{t('guide.overview.step3', 'Push into enemy territory')}</li>
|
||||
<li>{t('guide.overview.step4', 'Watch for chances to make a progression')}</li>
|
||||
<li>{t('guide.overview.step5', 'Win by forming a progression that survives one turn!')}</li>
|
||||
<li>{t('overview.step1')}</li>
|
||||
<li>{t('overview.step2')}</li>
|
||||
<li>{t('overview.step3')}</li>
|
||||
<li>{t('overview.step4')}</li>
|
||||
<li>{t('overview.step5')}</li>
|
||||
</ol>
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { useTranslation } from 'react-i18next'
|
||||
import { useTranslations } from 'next-intl'
|
||||
import { css } from '../../../../../styled-system/css'
|
||||
import { PieceRenderer } from '../PieceRenderer'
|
||||
import { RithmomachiaBoard } from '../RithmomachiaBoard'
|
||||
|
|
@ -18,7 +18,7 @@ function squaresToCropArea(topLeft: string, bottomRight: string) {
|
|||
}
|
||||
|
||||
export function PiecesSection({ useNativeAbacusNumbers }: { useNativeAbacusNumbers: boolean }) {
|
||||
const { t } = useTranslation()
|
||||
const t = useTranslations('rithmomachia.guide')
|
||||
const pieces: {
|
||||
type: PieceType
|
||||
name: string
|
||||
|
|
@ -28,22 +28,22 @@ export function PiecesSection({ useNativeAbacusNumbers }: { useNativeAbacusNumbe
|
|||
}[] = [
|
||||
{
|
||||
type: 'C',
|
||||
name: t('guide.pieces.circle', 'Circle'),
|
||||
movement: t('guide.pieces.circleMove', 'Moves diagonally'),
|
||||
name: t('pieces.circle'),
|
||||
movement: t('pieces.circleMove'),
|
||||
count: 12,
|
||||
exampleValues: [3, 5, 7, 9],
|
||||
},
|
||||
{
|
||||
type: 'T',
|
||||
name: t('guide.pieces.triangle', 'Triangle'),
|
||||
movement: t('guide.pieces.triangleMove', 'Moves in straight lines'),
|
||||
name: t('pieces.triangle'),
|
||||
movement: t('pieces.triangleMove'),
|
||||
count: 6,
|
||||
exampleValues: [12, 16, 20, 30],
|
||||
},
|
||||
{
|
||||
type: 'S',
|
||||
name: t('guide.pieces.square', 'Square'),
|
||||
movement: t('guide.pieces.squareMove', 'Moves in any direction'),
|
||||
name: t('pieces.square'),
|
||||
movement: t('pieces.squareMove'),
|
||||
count: 6,
|
||||
exampleValues: [25, 28, 45, 66],
|
||||
},
|
||||
|
|
@ -59,13 +59,10 @@ export function PiecesSection({ useNativeAbacusNumbers }: { useNativeAbacusNumbe
|
|||
mb: '16px',
|
||||
})}
|
||||
>
|
||||
{t('guide.pieces.title', 'Your Pieces (25 total)')}
|
||||
{t('pieces.title')}
|
||||
</h3>
|
||||
<p className={css({ fontSize: '15px', mb: '24px', color: '#374151' })}>
|
||||
{t(
|
||||
'guide.pieces.description',
|
||||
'Each side has 25 pieces with different movement patterns. The shape tells you how it moves:'
|
||||
)}
|
||||
{t('pieces.description')}
|
||||
</p>
|
||||
|
||||
<div className={css({ display: 'flex', flexDirection: 'column', gap: '24px' })}>
|
||||
|
|
@ -122,7 +119,7 @@ export function PiecesSection({ useNativeAbacusNumbers }: { useNativeAbacusNumbe
|
|||
fontStyle: 'italic',
|
||||
})}
|
||||
>
|
||||
{t('guide.pieces.exampleValues', 'Example values')}:
|
||||
{t('pieces.exampleValues')}:
|
||||
</p>
|
||||
<div className={css({ display: 'flex', gap: '12px', flexWrap: 'wrap' })}>
|
||||
{piece.exampleValues.map((value) => (
|
||||
|
|
@ -159,13 +156,10 @@ export function PiecesSection({ useNativeAbacusNumbers }: { useNativeAbacusNumbe
|
|||
})}
|
||||
>
|
||||
<h4 className={css({ fontSize: '18px', fontWeight: 'bold', color: '#92400e', mb: '12px' })}>
|
||||
{t('guide.pieces.pyramidTitle', '⭐ Pyramids: The Multi-Faced Pieces')}
|
||||
{t('pieces.pyramidTitle')}
|
||||
</h4>
|
||||
<p className={css({ fontSize: '14px', color: '#78350f', lineHeight: '1.6', mb: '16px' })}>
|
||||
{t(
|
||||
'guide.pieces.pyramidIntro',
|
||||
'Unlike other pieces with a single value, Pyramids contain 4 face values representing perfect squares. When capturing an enemy piece, you choose which face to use for the mathematical relation.'
|
||||
)}
|
||||
{t('pieces.pyramidIntro')}
|
||||
</p>
|
||||
|
||||
<div
|
||||
|
|
@ -188,7 +182,7 @@ export function PiecesSection({ useNativeAbacusNumbers }: { useNativeAbacusNumbe
|
|||
textAlign: 'center',
|
||||
})}
|
||||
>
|
||||
{t('guide.pieces.blackPyramid', 'Black Pyramid Faces')}:
|
||||
{t('pieces.blackPyramid')}:
|
||||
</p>
|
||||
<div className={css({ width: '80px', height: '80px', margin: '0 auto' })}>
|
||||
<PieceRenderer
|
||||
|
|
@ -208,7 +202,7 @@ export function PiecesSection({ useNativeAbacusNumbers }: { useNativeAbacusNumbe
|
|||
fontWeight: 'bold',
|
||||
})}
|
||||
>
|
||||
{t('guide.pieces.blackPyramidValues', '36 (6²), 25 (5²), 16 (4²), 4 (2²)')}
|
||||
{t('pieces.blackPyramidValues')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
|
@ -223,7 +217,7 @@ export function PiecesSection({ useNativeAbacusNumbers }: { useNativeAbacusNumbe
|
|||
textAlign: 'center',
|
||||
})}
|
||||
>
|
||||
{t('guide.pieces.whitePyramid', 'White Pyramid Faces')}:
|
||||
{t('pieces.whitePyramid')}:
|
||||
</p>
|
||||
<div className={css({ width: '80px', height: '80px', margin: '0 auto' })}>
|
||||
<PieceRenderer
|
||||
|
|
@ -243,7 +237,7 @@ export function PiecesSection({ useNativeAbacusNumbers }: { useNativeAbacusNumbe
|
|||
fontWeight: 'bold',
|
||||
})}
|
||||
>
|
||||
{t('guide.pieces.whitePyramidValues', '64 (8²), 49 (7²), 36 (6²), 25 (5²)')}
|
||||
{t('pieces.whitePyramidValues')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -266,35 +260,15 @@ export function PiecesSection({ useNativeAbacusNumbers }: { useNativeAbacusNumbe
|
|||
mb: '12px',
|
||||
})}
|
||||
>
|
||||
{t('guide.pieces.pyramidHowItWorks', 'How face selection works:')}
|
||||
{t('pieces.pyramidHowItWorks')}
|
||||
</p>
|
||||
<ul
|
||||
className={css({ fontSize: '13px', color: '#78350f', lineHeight: '1.8', pl: '20px' })}
|
||||
>
|
||||
<li>
|
||||
{t(
|
||||
'guide.pieces.pyramidRule1',
|
||||
"When your Pyramid attempts a capture, you must declare which face value you're using before the relation is checked"
|
||||
)}
|
||||
</li>
|
||||
<li>
|
||||
{t(
|
||||
'guide.pieces.pyramidRule2',
|
||||
'The chosen face value becomes "your piece\'s value" for all mathematical relations (equality, multiple/divisor, sum, difference, product, ratio)'
|
||||
)}
|
||||
</li>
|
||||
<li>
|
||||
{t(
|
||||
'guide.pieces.pyramidRule3',
|
||||
'You can choose different faces for different captures—the Pyramid doesn\'t "lock in" to one value'
|
||||
)}
|
||||
</li>
|
||||
<li>
|
||||
{t(
|
||||
'guide.pieces.pyramidRule4',
|
||||
'This flexibility makes Pyramids excellent for creating unexpected capture opportunities and versatile helpers'
|
||||
)}
|
||||
</li>
|
||||
<li>{t('pieces.pyramidRule1')}</li>
|
||||
<li>{t('pieces.pyramidRule2')}</li>
|
||||
<li>{t('pieces.pyramidRule3')}</li>
|
||||
<li>{t('pieces.pyramidRule4')}</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
|
|
@ -308,11 +282,7 @@ export function PiecesSection({ useNativeAbacusNumbers }: { useNativeAbacusNumbe
|
|||
})}
|
||||
>
|
||||
<p className={css({ fontSize: '13px', color: '#78350f', lineHeight: '1.6' })}>
|
||||
<strong>{t('guide.pieces.example', 'Example:')}</strong>{' '}
|
||||
{t(
|
||||
'guide.pieces.pyramidExample',
|
||||
"White's Pyramid can capture Black's 16 using face 64 (multiple: 64÷16=4), face 36 (multiple: 36÷9=4, with Black's 9), or face 25 with equality if capturing Black's 25."
|
||||
)}
|
||||
<strong>{t('pieces.example')}</strong> {t('pieces.pyramidExample')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
|
@ -326,16 +296,10 @@ export function PiecesSection({ useNativeAbacusNumbers }: { useNativeAbacusNumbe
|
|||
mb: '12px',
|
||||
})}
|
||||
>
|
||||
{t(
|
||||
'guide.pieces.pyramidVisualTitle',
|
||||
"Visual Example: Pyramid's Multiple Capture Options"
|
||||
)}
|
||||
{t('pieces.pyramidVisualTitle')}
|
||||
</h5>
|
||||
<p className={css({ fontSize: '13px', color: '#78350f', mb: '12px', lineHeight: '1.6' })}>
|
||||
{t(
|
||||
'guide.pieces.pyramidVisualDesc',
|
||||
"White's Pyramid (faces: 64, 49, 36, 25) is positioned to capture Black pieces. Notice the flexibility:"
|
||||
)}
|
||||
{t('pieces.pyramidVisualDesc')}
|
||||
</p>
|
||||
|
||||
<div className={css({ display: 'flex', justifyContent: 'center', mb: '12px' })}>
|
||||
|
|
@ -372,29 +336,14 @@ export function PiecesSection({ useNativeAbacusNumbers }: { useNativeAbacusNumbe
|
|||
mb: '8px',
|
||||
})}
|
||||
>
|
||||
{t('guide.pieces.pyramidCaptureOptions', 'Capture options from H5:')}
|
||||
{t('pieces.pyramidCaptureOptions')}
|
||||
</p>
|
||||
<ul
|
||||
className={css({ fontSize: '13px', color: '#78350f', lineHeight: '1.8', pl: '20px' })}
|
||||
>
|
||||
<li>
|
||||
{t(
|
||||
'guide.pieces.pyramidOption1',
|
||||
'Move to I5: Choose face 64 → captures 16 by multiple (64÷16=4)'
|
||||
)}
|
||||
</li>
|
||||
<li>
|
||||
{t(
|
||||
'guide.pieces.pyramidOption2',
|
||||
'Move to H6: Choose face 49 → captures 49 by equality (49=49)'
|
||||
)}
|
||||
</li>
|
||||
<li>
|
||||
{t(
|
||||
'guide.pieces.pyramidOption3',
|
||||
'Move to G5: Choose face 25 → captures 25 by equality (25=25)'
|
||||
)}
|
||||
</li>
|
||||
<li>{t('pieces.pyramidOption1')}</li>
|
||||
<li>{t('pieces.pyramidOption2')}</li>
|
||||
<li>{t('pieces.pyramidOption3')}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { useTranslation } from 'react-i18next'
|
||||
import { useTranslations } from 'next-intl'
|
||||
import { css } from '../../../../../styled-system/css'
|
||||
import { RithmomachiaBoard, type ExamplePiece } from '../RithmomachiaBoard'
|
||||
|
||||
|
|
@ -16,7 +16,7 @@ function squaresToCropArea(topLeft: string, bottomRight: string) {
|
|||
}
|
||||
|
||||
export function VictorySection({ useNativeAbacusNumbers }: { useNativeAbacusNumbers: boolean }) {
|
||||
const { t } = useTranslation()
|
||||
const t = useTranslations('rithmomachia.guide')
|
||||
|
||||
// Example winning position: White has formed a geometric progression in Black's territory
|
||||
const winningExample: ExamplePiece[] = [
|
||||
|
|
@ -39,7 +39,7 @@ export function VictorySection({ useNativeAbacusNumbers }: { useNativeAbacusNumb
|
|||
mb: '16px',
|
||||
})}
|
||||
>
|
||||
{t('guide.victory.title', 'How to Win')}
|
||||
{t('victory.title')}
|
||||
</h3>
|
||||
|
||||
<div className={css({ display: 'flex', flexDirection: 'column', gap: '24px' })}>
|
||||
|
|
@ -56,13 +56,10 @@ export function VictorySection({ useNativeAbacusNumbers }: { useNativeAbacusNumb
|
|||
})}
|
||||
>
|
||||
<span>👑</span>
|
||||
<span>{t('guide.victory.harmony', 'Victory #1: Harmony (Progression)')}</span>
|
||||
<span>{t('victory.harmony')}</span>
|
||||
</h4>
|
||||
<p className={css({ fontSize: '15px', lineHeight: '1.6', color: '#374151', mb: '12px' })}>
|
||||
{t(
|
||||
'guide.victory.harmonyDesc',
|
||||
"Form a mathematical progression with 3 pieces in enemy territory. If it survives your opponent's next turn, you win!"
|
||||
)}
|
||||
{t('victory.harmonyDesc')}
|
||||
</p>
|
||||
|
||||
{/* Visual example of winning harmony */}
|
||||
|
|
@ -84,7 +81,7 @@ export function VictorySection({ useNativeAbacusNumbers }: { useNativeAbacusNumb
|
|||
textAlign: 'center',
|
||||
})}
|
||||
>
|
||||
{t('guide.victory.exampleTitle', 'Example: White Wins!')}
|
||||
{t('victory.exampleTitle')}
|
||||
</p>
|
||||
<div className={css({ display: 'flex', justifyContent: 'center' })}>
|
||||
<RithmomachiaBoard
|
||||
|
|
@ -105,10 +102,7 @@ export function VictorySection({ useNativeAbacusNumbers }: { useNativeAbacusNumb
|
|||
fontStyle: 'italic',
|
||||
})}
|
||||
>
|
||||
{t(
|
||||
'guide.victory.exampleCaption',
|
||||
'White pieces 4, 8, 16 form a geometric progression in enemy territory. Black cannot break it - White wins!'
|
||||
)}
|
||||
{t('victory.exampleCaption')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
|
@ -121,10 +115,7 @@ export function VictorySection({ useNativeAbacusNumbers }: { useNativeAbacusNumb
|
|||
})}
|
||||
>
|
||||
<p className={css({ fontSize: '13px', color: '#15803d' })}>
|
||||
{t(
|
||||
'guide.victory.harmonyNote',
|
||||
'This is the primary victory condition in Rithmomachia'
|
||||
)}
|
||||
{t('victory.harmonyNote')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -142,13 +133,10 @@ export function VictorySection({ useNativeAbacusNumbers }: { useNativeAbacusNumb
|
|||
})}
|
||||
>
|
||||
<span>🚫</span>
|
||||
<span>{t('guide.victory.exhaustion', 'Victory #2: Exhaustion')}</span>
|
||||
<span>{t('victory.exhaustion')}</span>
|
||||
</h4>
|
||||
<p className={css({ fontSize: '15px', lineHeight: '1.6', color: '#374151' })}>
|
||||
{t(
|
||||
'guide.victory.exhaustionDesc',
|
||||
'If your opponent has no legal moves at the start of their turn, they lose.'
|
||||
)}
|
||||
{t('victory.exhaustionDesc')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -162,7 +150,7 @@ export function VictorySection({ useNativeAbacusNumbers }: { useNativeAbacusNumb
|
|||
mt: '32px',
|
||||
})}
|
||||
>
|
||||
{t('guide.victory.strategyTitle', 'Quick Strategy Tips')}
|
||||
{t('victory.strategyTitle')}
|
||||
</h3>
|
||||
<ul
|
||||
className={css({
|
||||
|
|
@ -172,31 +160,11 @@ export function VictorySection({ useNativeAbacusNumbers }: { useNativeAbacusNumb
|
|||
color: '#374151',
|
||||
})}
|
||||
>
|
||||
<li>{t('guide.victory.tip1', 'Control the center — easier to invade enemy territory')}</li>
|
||||
<li>
|
||||
{t(
|
||||
'guide.victory.tip2',
|
||||
'Small pieces are fast — circles (3, 5, 7, 9) can slip into enemy half quickly'
|
||||
)}
|
||||
</li>
|
||||
<li>
|
||||
{t(
|
||||
'guide.victory.tip3',
|
||||
'Large pieces are powerful — harder to capture due to their size'
|
||||
)}
|
||||
</li>
|
||||
<li>
|
||||
{t(
|
||||
'guide.victory.tip4',
|
||||
"Watch for harmony threats — don't let opponent get 3 pieces deep in your territory"
|
||||
)}
|
||||
</li>
|
||||
<li>
|
||||
{t(
|
||||
'guide.victory.tip5',
|
||||
'Pyramids are flexible — choose the right face value for each situation'
|
||||
)}
|
||||
</li>
|
||||
<li>{t('victory.tip1')}</li>
|
||||
<li>{t('victory.tip2')}</li>
|
||||
<li>{t('victory.tip3')}</li>
|
||||
<li>{t('victory.tip4')}</li>
|
||||
<li>{t('victory.tip5')}</li>
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,30 +0,0 @@
|
|||
import i18n from 'i18next'
|
||||
import { initReactI18next } from 'react-i18next'
|
||||
import en from './locales/en.json'
|
||||
import de from './locales/de.json'
|
||||
import ja from './locales/ja.json'
|
||||
import hi from './locales/hi.json'
|
||||
import es from './locales/es.json'
|
||||
import la from './locales/la.json'
|
||||
|
||||
export const defaultNS = 'translation'
|
||||
export const resources = {
|
||||
en: { translation: en },
|
||||
de: { translation: de },
|
||||
ja: { translation: ja },
|
||||
hi: { translation: hi },
|
||||
es: { translation: es },
|
||||
la: { translation: la },
|
||||
} as const
|
||||
|
||||
i18n.use(initReactI18next).init({
|
||||
lng: 'en',
|
||||
fallbackLng: 'en',
|
||||
defaultNS,
|
||||
resources,
|
||||
interpolation: {
|
||||
escapeValue: false, // React already escapes
|
||||
},
|
||||
})
|
||||
|
||||
export default i18n
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
/**
|
||||
* Rithmomachia translations aggregated by locale
|
||||
* Co-located with the game code
|
||||
*/
|
||||
|
||||
// Import existing locale files
|
||||
import enGuide from './i18n/locales/en.json'
|
||||
import deGuide from './i18n/locales/de.json'
|
||||
import jaGuide from './i18n/locales/ja.json'
|
||||
import hiGuide from './i18n/locales/hi.json'
|
||||
import esGuide from './i18n/locales/es.json'
|
||||
import laGuide from './i18n/locales/la.json'
|
||||
|
||||
export const rithmomachiaMessages = {
|
||||
en: { rithmomachia: enGuide },
|
||||
de: { rithmomachia: deGuide },
|
||||
ja: { rithmomachia: jaGuide },
|
||||
hi: { rithmomachia: hiGuide },
|
||||
es: { rithmomachia: esGuide },
|
||||
la: { rithmomachia: laGuide },
|
||||
} as const
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
import { getRequestConfig } from 'next-intl/server'
|
||||
import { headers, cookies } from 'next/headers'
|
||||
import { defaultLocale, LOCALE_COOKIE_NAME, locales, type Locale } from './routing'
|
||||
import { getMessages } from './messages'
|
||||
|
||||
export async function getRequestLocale(): Promise<Locale> {
|
||||
// Get locale from header (set by middleware) or cookie
|
||||
const headersList = await headers()
|
||||
const cookieStore = await cookies()
|
||||
|
||||
let locale = headersList.get('x-locale') as Locale | null
|
||||
|
||||
if (!locale) {
|
||||
locale = cookieStore.get(LOCALE_COOKIE_NAME)?.value as Locale | undefined
|
||||
}
|
||||
|
||||
// Validate and fallback to default
|
||||
if (!locale || !locales.includes(locale)) {
|
||||
locale = defaultLocale
|
||||
}
|
||||
|
||||
return locale
|
||||
}
|
||||
|
||||
export default getRequestConfig(async () => {
|
||||
const locale = await getRequestLocale()
|
||||
|
||||
return {
|
||||
locale,
|
||||
messages: await getMessages(locale),
|
||||
}
|
||||
})
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
// Supported locales
|
||||
export const locales = ['en', 'de', 'ja', 'hi', 'es', 'la'] as const
|
||||
export type Locale = (typeof locales)[number]
|
||||
|
||||
// Default locale
|
||||
export const defaultLocale: Locale = 'en'
|
||||
|
||||
// Locale cookie name
|
||||
export const LOCALE_COOKIE_NAME = 'NEXT_LOCALE'
|
||||
|
|
@ -1,14 +1,43 @@
|
|||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { createGuestToken, GUEST_COOKIE_NAME } from './lib/guest-token'
|
||||
import { defaultLocale, LOCALE_COOKIE_NAME, locales, type Locale } from './i18n/routing'
|
||||
|
||||
/**
|
||||
* Middleware to:
|
||||
* 1. Ensure every visitor gets a guest token
|
||||
* 2. Add pathname to headers for Server Components
|
||||
* 1. Detect and set locale based on Accept-Language header or cookie
|
||||
* 2. Ensure every visitor gets a guest token
|
||||
* 3. Add pathname and locale to headers for Server Components
|
||||
*/
|
||||
export async function middleware(request: NextRequest) {
|
||||
const response = NextResponse.next()
|
||||
|
||||
// Detect locale from cookie or Accept-Language header
|
||||
let locale = request.cookies.get(LOCALE_COOKIE_NAME)?.value as Locale | undefined
|
||||
|
||||
if (!locale || !locales.includes(locale)) {
|
||||
// Parse Accept-Language header
|
||||
const acceptLanguage = request.headers.get('accept-language')
|
||||
if (acceptLanguage) {
|
||||
const preferred = acceptLanguage
|
||||
.split(',')
|
||||
.map((lang) => lang.split(';')[0].trim().slice(0, 2))
|
||||
.find((lang) => locales.includes(lang as Locale))
|
||||
locale = (preferred as Locale) || defaultLocale
|
||||
} else {
|
||||
locale = defaultLocale
|
||||
}
|
||||
|
||||
// Set locale cookie
|
||||
response.cookies.set(LOCALE_COOKIE_NAME, locale, {
|
||||
path: '/',
|
||||
maxAge: 60 * 60 * 24 * 365, // 1 year
|
||||
sameSite: 'lax',
|
||||
})
|
||||
}
|
||||
|
||||
// Add locale to headers for Server Components
|
||||
response.headers.set('x-locale', locale)
|
||||
|
||||
// Add pathname to headers so Server Components can access it
|
||||
response.headers.set('x-pathname', request.nextUrl.pathname)
|
||||
|
||||
|
|
@ -63,9 +92,10 @@ export const config = {
|
|||
* - _next/static (static files)
|
||||
* - _next/image (image optimization files)
|
||||
* - favicon.ico (favicon file)
|
||||
* - api routes (next-intl doesn't need to handle these)
|
||||
*
|
||||
* Note: API routes ARE included so guest cookies are set for API requests
|
||||
* Note: This matcher handles both i18n routing and guest tokens
|
||||
*/
|
||||
'/((?!_next/static|_next/image|favicon.ico).*)',
|
||||
'/((?!api|_next|_vercel|.*\\..*).*)',
|
||||
],
|
||||
}
|
||||
|
|
|
|||
202
pnpm-lock.yaml
202
pnpm-lock.yaml
|
|
@ -149,9 +149,6 @@ importers:
|
|||
emojibase-data:
|
||||
specifier: ^16.0.3
|
||||
version: 16.0.3(emojibase@16.0.0)
|
||||
i18next:
|
||||
specifier: ^25.6.0
|
||||
version: 25.6.0(typescript@5.9.3)
|
||||
jose:
|
||||
specifier: ^6.1.0
|
||||
version: 6.1.0
|
||||
|
|
@ -176,6 +173,9 @@ importers:
|
|||
next-auth:
|
||||
specifier: 5.0.0-beta.29
|
||||
version: 5.0.0-beta.29(next@14.2.33(@babel/core@7.28.4)(@playwright/test@1.56.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)
|
||||
next-intl:
|
||||
specifier: ^4.4.0
|
||||
version: 4.4.0(next@14.2.33(@babel/core@7.28.4)(@playwright/test@1.56.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)(typescript@5.9.3)
|
||||
python-bridge:
|
||||
specifier: ^1.1.0
|
||||
version: 1.1.0
|
||||
|
|
@ -188,9 +188,6 @@ importers:
|
|||
react-dom:
|
||||
specifier: ^18.2.0
|
||||
version: 18.3.1(react@18.3.1)
|
||||
react-i18next:
|
||||
specifier: ^16.2.3
|
||||
version: 16.2.3(i18next@25.6.0(typescript@5.9.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3)
|
||||
react-resizable-layout:
|
||||
specifier: ^0.7.3
|
||||
version: 0.7.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
|
|
@ -290,7 +287,7 @@ importers:
|
|||
version: 5.9.3
|
||||
vitest:
|
||||
specifier: ^1.0.0
|
||||
version: 1.6.1(@types/node@20.19.19)(@vitest/ui@3.2.4)(happy-dom@18.0.1)(jsdom@27.0.0(postcss@8.5.6))(terser@5.44.0)
|
||||
version: 1.6.1(@types/node@20.19.19)(happy-dom@18.0.1)(jsdom@27.0.0(postcss@8.5.6))(terser@5.44.0)
|
||||
|
||||
packages/abacus-react:
|
||||
dependencies:
|
||||
|
|
@ -1819,6 +1816,24 @@ packages:
|
|||
'@floating-ui/utils@0.2.10':
|
||||
resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==}
|
||||
|
||||
'@formatjs/ecma402-abstract@2.3.6':
|
||||
resolution: {integrity: sha512-HJnTFeRM2kVFVr5gr5kH1XP6K0JcJtE7Lzvtr3FS/so5f1kpsqqqxy5JF+FRaO6H2qmcMfAUIox7AJteieRtVw==}
|
||||
|
||||
'@formatjs/fast-memoize@2.2.7':
|
||||
resolution: {integrity: sha512-Yabmi9nSvyOMrlSeGGWDiH7rf3a7sIwplbvo/dlz9WCIjzIQAfy1RMf4S0X3yG724n5Ghu2GmEl5NJIV6O9sZQ==}
|
||||
|
||||
'@formatjs/icu-messageformat-parser@2.11.4':
|
||||
resolution: {integrity: sha512-7kR78cRrPNB4fjGFZg3Rmj5aah8rQj9KPzuLsmcSn4ipLXQvC04keycTI1F7kJYDwIXtT2+7IDEto842CfZBtw==}
|
||||
|
||||
'@formatjs/icu-skeleton-parser@1.8.16':
|
||||
resolution: {integrity: sha512-H13E9Xl+PxBd8D5/6TVUluSpxGNvFSlN/b3coUp0e0JpuWXXnQDiavIpY3NnvSp4xhEMoXyyBvVfdFX8jglOHQ==}
|
||||
|
||||
'@formatjs/intl-localematcher@0.5.10':
|
||||
resolution: {integrity: sha512-af3qATX+m4Rnd9+wHcjJ4w2ijq+rAVP3CCinJQvFv1kgSu1W6jypUmvleJxcewdxmutM8dmIRZFxO/IQBZmP2Q==}
|
||||
|
||||
'@formatjs/intl-localematcher@0.6.2':
|
||||
resolution: {integrity: sha512-XOMO2Hupl0wdd172Y06h6kLpBz6Dv+J4okPLl4LPtzbr8f66WbIoy4ev98EBuZ6ZK4h5ydTN6XneT4QVpD7cdA==}
|
||||
|
||||
'@humanwhocodes/config-array@0.13.0':
|
||||
resolution: {integrity: sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==}
|
||||
engines: {node: '>=10.10.0'}
|
||||
|
|
@ -3154,6 +3169,9 @@ packages:
|
|||
'@rushstack/eslint-patch@1.13.0':
|
||||
resolution: {integrity: sha512-2ih5qGw5SZJ+2fLZxP6Lr6Na2NTIgPRL/7Kmyuw0uIyBQnuhQ8fi8fzUTd38eIQmqp+GYLC00cI6WgtqHxBwmw==}
|
||||
|
||||
'@schummar/icu-type-parser@1.21.5':
|
||||
resolution: {integrity: sha512-bXHSaW5jRTmke9Vd0h5P7BtWZG9Znqb8gSDxZnxaGSJnGwPLDPfS+3g0BKzeWqzgZPsIVZkM7m2tbo18cm5HBw==}
|
||||
|
||||
'@semantic-release/changelog@6.0.3':
|
||||
resolution: {integrity: sha512-dZuR5qByyfe3Y03TpmCvAxCyTnp7r5XwtHRf/8vD9EAn4ZWbavUX8adMtXYzE86EVh0gyLA7lm5yW4IV30XUag==}
|
||||
engines: {node: '>=14.17'}
|
||||
|
|
@ -6046,9 +6064,6 @@ packages:
|
|||
engines: {node: '>=12'}
|
||||
hasBin: true
|
||||
|
||||
html-parse-stringify@3.0.1:
|
||||
resolution: {integrity: sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==}
|
||||
|
||||
html-tags@3.3.1:
|
||||
resolution: {integrity: sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ==}
|
||||
engines: {node: '>=8'}
|
||||
|
|
@ -6095,14 +6110,6 @@ packages:
|
|||
resolution: {integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==}
|
||||
engines: {node: '>=16.17.0'}
|
||||
|
||||
i18next@25.6.0:
|
||||
resolution: {integrity: sha512-tTn8fLrwBYtnclpL5aPXK/tAYBLWVvoHM1zdfXoRNLcI+RvtMsoZRV98ePlaW3khHYKuNh/Q65W/+NVFUeIwVw==}
|
||||
peerDependencies:
|
||||
typescript: ^5
|
||||
peerDependenciesMeta:
|
||||
typescript:
|
||||
optional: true
|
||||
|
||||
iconv-lite@0.4.24:
|
||||
resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
|
@ -6174,6 +6181,9 @@ packages:
|
|||
resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
intl-messageformat@10.7.18:
|
||||
resolution: {integrity: sha512-m3Ofv/X/tV8Y3tHXLohcuVuhWKo7BBq62cqY15etqmLxg2DZ34AGGgQDeR+SCta2+zICb1NX83af0GJmbQ1++g==}
|
||||
|
||||
into-stream@7.0.0:
|
||||
resolution: {integrity: sha512-2dYz766i9HprMBasCMvHMuazJ7u4WzhJwo5kb3iPSiW/iRYV6uPari3zHoqZlnuaR7V1bEiNMxikhp37rdBXbw==}
|
||||
engines: {node: '>=12'}
|
||||
|
|
@ -7009,6 +7019,10 @@ packages:
|
|||
resolution: {integrity: sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
||||
negotiator@1.0.0:
|
||||
resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
||||
neo-async@2.6.2:
|
||||
resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==}
|
||||
|
||||
|
|
@ -7031,6 +7045,16 @@ packages:
|
|||
nodemailer:
|
||||
optional: true
|
||||
|
||||
next-intl@4.4.0:
|
||||
resolution: {integrity: sha512-QHqnP9V9Pe7Tn0PdVQ7u1Z8k9yCkW5SJKeRy2g5gxzhSt/C01y3B9qNxuj3Fsmup/yreIHe6osxU6sFa+9WIkQ==}
|
||||
peerDependencies:
|
||||
next: ^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0 || ^16.0.0
|
||||
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 || ^19.0.0
|
||||
typescript: ^5.0.0
|
||||
peerDependenciesMeta:
|
||||
typescript:
|
||||
optional: true
|
||||
|
||||
next@14.2.33:
|
||||
resolution: {integrity: sha512-GiKHLsD00t4ACm1p00VgrI0rUFAC9cRDGReKyERlM57aeEZkOQGcZTpIbsGn0b562FTPJWmYfKwplfO9EaT6ng==}
|
||||
engines: {node: '>=18.17.0'}
|
||||
|
|
@ -7843,22 +7867,6 @@ packages:
|
|||
react: ^0.14.8 || ^15.0.1 || ^16.0.0 || ^17.0.1 || ^18.0.0
|
||||
react-dom: ^0.14.8 || ^15.0.1 || ^16.0.0 || ^17.0.1 || ^18.0.0
|
||||
|
||||
react-i18next@16.2.3:
|
||||
resolution: {integrity: sha512-O0t2zvmIz7nHWKNfIL+O/NTIbpTaOPY0vZov779hegbep3IZ+xcqkeVPKWBSXwzdkiv77q8zmq9toKIUys1x3A==}
|
||||
peerDependencies:
|
||||
i18next: '>= 25.5.2'
|
||||
react: '>= 16.8.0'
|
||||
react-dom: '*'
|
||||
react-native: '*'
|
||||
typescript: ^5
|
||||
peerDependenciesMeta:
|
||||
react-dom:
|
||||
optional: true
|
||||
react-native:
|
||||
optional: true
|
||||
typescript:
|
||||
optional: true
|
||||
|
||||
react-is@16.13.1:
|
||||
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
|
||||
|
||||
|
|
@ -9041,6 +9049,11 @@ packages:
|
|||
'@types/react':
|
||||
optional: true
|
||||
|
||||
use-intl@4.4.0:
|
||||
resolution: {integrity: sha512-smFekJWtokDRBLC5/ZumlBREzdXOkw06+56Ifj2uRe9266Mk+yWQm2PcJO+EwlOE5sHIXHixOTzN6V8E0RGUbw==}
|
||||
peerDependencies:
|
||||
react: ^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 || ^19.0.0
|
||||
|
||||
use-resize-observer@9.1.0:
|
||||
resolution: {integrity: sha512-R25VqO9Wb3asSD4eqtcxk8sJalvIOYBqS8MNZlpDSQ4l4xMQxC/J7Id9HoTqPq8FwULIn0PVW+OAqF2dyYbjow==}
|
||||
peerDependencies:
|
||||
|
|
@ -9178,10 +9191,6 @@ packages:
|
|||
vm-browserify@1.1.2:
|
||||
resolution: {integrity: sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==}
|
||||
|
||||
void-elements@3.1.0:
|
||||
resolution: {integrity: sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
w3c-xmlserializer@5.0.0:
|
||||
resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==}
|
||||
engines: {node: '>=18'}
|
||||
|
|
@ -10678,6 +10687,36 @@ snapshots:
|
|||
|
||||
'@floating-ui/utils@0.2.10': {}
|
||||
|
||||
'@formatjs/ecma402-abstract@2.3.6':
|
||||
dependencies:
|
||||
'@formatjs/fast-memoize': 2.2.7
|
||||
'@formatjs/intl-localematcher': 0.6.2
|
||||
decimal.js: 10.6.0
|
||||
tslib: 2.8.1
|
||||
|
||||
'@formatjs/fast-memoize@2.2.7':
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
|
||||
'@formatjs/icu-messageformat-parser@2.11.4':
|
||||
dependencies:
|
||||
'@formatjs/ecma402-abstract': 2.3.6
|
||||
'@formatjs/icu-skeleton-parser': 1.8.16
|
||||
tslib: 2.8.1
|
||||
|
||||
'@formatjs/icu-skeleton-parser@1.8.16':
|
||||
dependencies:
|
||||
'@formatjs/ecma402-abstract': 2.3.6
|
||||
tslib: 2.8.1
|
||||
|
||||
'@formatjs/intl-localematcher@0.5.10':
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
|
||||
'@formatjs/intl-localematcher@0.6.2':
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
|
||||
'@humanwhocodes/config-array@0.13.0':
|
||||
dependencies:
|
||||
'@humanwhocodes/object-schema': 2.0.3
|
||||
|
|
@ -12149,6 +12188,8 @@ snapshots:
|
|||
|
||||
'@rushstack/eslint-patch@1.13.0': {}
|
||||
|
||||
'@schummar/icu-type-parser@1.21.5': {}
|
||||
|
||||
'@semantic-release/changelog@6.0.3(semantic-release@22.0.12(typescript@5.9.3))':
|
||||
dependencies:
|
||||
'@semantic-release/error': 3.0.0
|
||||
|
|
@ -16034,10 +16075,6 @@ snapshots:
|
|||
relateurl: 0.2.7
|
||||
terser: 5.44.0
|
||||
|
||||
html-parse-stringify@3.0.1:
|
||||
dependencies:
|
||||
void-elements: 3.1.0
|
||||
|
||||
html-tags@3.3.1: {}
|
||||
|
||||
html-webpack-plugin@5.6.4(webpack@5.102.0(esbuild@0.25.10)):
|
||||
|
|
@ -16092,12 +16129,6 @@ snapshots:
|
|||
|
||||
human-signals@5.0.0: {}
|
||||
|
||||
i18next@25.6.0(typescript@5.9.3):
|
||||
dependencies:
|
||||
'@babel/runtime': 7.28.4
|
||||
optionalDependencies:
|
||||
typescript: 5.9.3
|
||||
|
||||
iconv-lite@0.4.24:
|
||||
dependencies:
|
||||
safer-buffer: 2.1.2
|
||||
|
|
@ -16155,6 +16186,13 @@ snapshots:
|
|||
hasown: 2.0.2
|
||||
side-channel: 1.1.0
|
||||
|
||||
intl-messageformat@10.7.18:
|
||||
dependencies:
|
||||
'@formatjs/ecma402-abstract': 2.3.6
|
||||
'@formatjs/fast-memoize': 2.2.7
|
||||
'@formatjs/icu-messageformat-parser': 2.11.4
|
||||
tslib: 2.8.1
|
||||
|
||||
into-stream@7.0.0:
|
||||
dependencies:
|
||||
from2: 2.3.0
|
||||
|
|
@ -16973,6 +17011,8 @@ snapshots:
|
|||
|
||||
negotiator@0.6.4: {}
|
||||
|
||||
negotiator@1.0.0: {}
|
||||
|
||||
neo-async@2.6.2: {}
|
||||
|
||||
nerf-dart@1.0.0: {}
|
||||
|
|
@ -16983,6 +17023,16 @@ snapshots:
|
|||
next: 14.2.33(@babel/core@7.28.4)(@playwright/test@1.56.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
react: 18.3.1
|
||||
|
||||
next-intl@4.4.0(next@14.2.33(@babel/core@7.28.4)(@playwright/test@1.56.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)(typescript@5.9.3):
|
||||
dependencies:
|
||||
'@formatjs/intl-localematcher': 0.5.10
|
||||
negotiator: 1.0.0
|
||||
next: 14.2.33(@babel/core@7.28.4)(@playwright/test@1.56.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
react: 18.3.1
|
||||
use-intl: 4.4.0(react@18.3.1)
|
||||
optionalDependencies:
|
||||
typescript: 5.9.3
|
||||
|
||||
next@14.2.33(@babel/core@7.28.4)(@playwright/test@1.56.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
|
||||
dependencies:
|
||||
'@next/env': 14.2.33
|
||||
|
|
@ -17777,17 +17827,6 @@ snapshots:
|
|||
react-dom: 18.3.1(react@18.3.1)
|
||||
react-is: 18.1.0
|
||||
|
||||
react-i18next@16.2.3(i18next@25.6.0(typescript@5.9.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3):
|
||||
dependencies:
|
||||
'@babel/runtime': 7.28.4
|
||||
html-parse-stringify: 3.0.1
|
||||
i18next: 25.6.0(typescript@5.9.3)
|
||||
react: 18.3.1
|
||||
use-sync-external-store: 1.6.0(react@18.3.1)
|
||||
optionalDependencies:
|
||||
react-dom: 18.3.1(react@18.3.1)
|
||||
typescript: 5.9.3
|
||||
|
||||
react-is@16.13.1: {}
|
||||
|
||||
react-is@17.0.2: {}
|
||||
|
|
@ -19116,6 +19155,13 @@ snapshots:
|
|||
optionalDependencies:
|
||||
'@types/react': 18.3.26
|
||||
|
||||
use-intl@4.4.0(react@18.3.1):
|
||||
dependencies:
|
||||
'@formatjs/fast-memoize': 2.2.7
|
||||
'@schummar/icu-type-parser': 1.21.5
|
||||
intl-messageformat: 10.7.18
|
||||
react: 18.3.1
|
||||
|
||||
use-resize-observer@9.1.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
|
||||
dependencies:
|
||||
'@juggle/resize-observer': 3.4.0
|
||||
|
|
@ -19232,9 +19278,43 @@ snapshots:
|
|||
- supports-color
|
||||
- terser
|
||||
|
||||
vm-browserify@1.1.2: {}
|
||||
vitest@1.6.1(@types/node@20.19.19)(happy-dom@18.0.1)(jsdom@27.0.0(postcss@8.5.6))(terser@5.44.0):
|
||||
dependencies:
|
||||
'@vitest/expect': 1.6.1
|
||||
'@vitest/runner': 1.6.1
|
||||
'@vitest/snapshot': 1.6.1
|
||||
'@vitest/spy': 1.6.1
|
||||
'@vitest/utils': 1.6.1
|
||||
acorn-walk: 8.3.4
|
||||
chai: 4.5.0
|
||||
debug: 4.4.3
|
||||
execa: 8.0.1
|
||||
local-pkg: 0.5.1
|
||||
magic-string: 0.30.19
|
||||
pathe: 1.1.2
|
||||
picocolors: 1.1.1
|
||||
std-env: 3.9.0
|
||||
strip-literal: 2.1.1
|
||||
tinybench: 2.9.0
|
||||
tinypool: 0.8.4
|
||||
vite: 5.4.20(@types/node@20.19.19)(terser@5.44.0)
|
||||
vite-node: 1.6.1(@types/node@20.19.19)(terser@5.44.0)
|
||||
why-is-node-running: 2.3.0
|
||||
optionalDependencies:
|
||||
'@types/node': 20.19.19
|
||||
happy-dom: 18.0.1
|
||||
jsdom: 27.0.0(postcss@8.5.6)
|
||||
transitivePeerDependencies:
|
||||
- less
|
||||
- lightningcss
|
||||
- sass
|
||||
- sass-embedded
|
||||
- stylus
|
||||
- sugarss
|
||||
- supports-color
|
||||
- terser
|
||||
|
||||
void-elements@3.1.0: {}
|
||||
vm-browserify@1.1.2: {}
|
||||
|
||||
w3c-xmlserializer@5.0.0:
|
||||
dependencies:
|
||||
|
|
|
|||
Loading…
Reference in New Issue