hensei-web/components/character/CharacterHovercard/index.tsx
Justin Edmund 3d67622353
Fix i18n migration to next-intl (#430)
## Summary
- Fixed translation key format compatibility with next-intl
- Fixed pluralization format from i18next to next-intl format
- Fixed dynamic translation key error handling
- Updated server components to match API response structure
- Fixed useSearchParams import location

## Changes
- Changed pluralization from `{{count}} items` to `{count} items` format
- Added proper error handling for missing translation keys
- Fixed import paths for next-intl hooks
- Fixed PartyPageClient trying to set non-existent appState.parties

## Test plan
- [x] Verified translations render correctly
- [x] Tested pluralization works with different counts
- [x] Confirmed no console errors about missing translations
- [x] Tested party page functionality

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2025-09-03 16:25:59 -07:00

203 lines
5.7 KiB
TypeScript

'use client'
import React from 'react'
import { useRouter, usePathname, useSearchParams } from 'next/navigation'
import { getCookie } from 'cookies-next'
import { useTranslations } from 'next-intl'
import {
Hovercard,
HovercardContent,
HovercardTrigger,
} from '~components/common/Hovercard'
import Button from '~components/common/Button'
import WeaponLabelIcon from '~components/weapon/WeaponLabelIcon'
import UncapIndicator from '~components/uncap/UncapIndicator'
import {
overMastery,
aetherialMastery,
permanentMastery,
} from '~data/overMastery'
import { ExtendedMastery } from '~types'
import styles from './index.module.scss'
import HovercardHeader from '~components/HovercardHeader'
interface Props {
gridCharacter: GridCharacter
children: React.ReactNode
onTriggerClick: () => void
}
const CharacterHovercard = (props: Props) => {
const router = useRouter()
const pathname = usePathname()
const searchParams = useSearchParams()
const t = useTranslations('common')
const routerLocale = getCookie('NEXT_LOCALE')
const locale =
routerLocale && ['en', 'ja'].includes(routerLocale) ? routerLocale : 'en'
const Element = ['null', 'wind', 'fire', 'water', 'earth', 'dark', 'light']
const tintElement = Element[props.gridCharacter.object.element]
function goTo() {
const urlSafeName = props.gridCharacter.object.name.en.replaceAll(' ', '_')
const url = `https://gbf.wiki/${urlSafeName}`
window.open(url, '_blank')
}
function masteryElement(dictionary: ItemSkill[], mastery: ExtendedMastery) {
const canonicalMastery = dictionary.find(
(item) => item.id === mastery.modifier
)
if (canonicalMastery) {
return (
<li className={styles.extendedMastery} key={canonicalMastery.id}>
<img
alt={canonicalMastery.name[locale]}
src={`${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/mastery/${canonicalMastery.slug}.png`}
/>
<span>
<strong>{canonicalMastery.name[locale]}</strong>&nbsp;
{`+${mastery.strength}${canonicalMastery.suffix}`}
</span>
</li>
)
}
}
const overMasterySection = () => {
if (
props.gridCharacter &&
props.gridCharacter.over_mastery &&
props.gridCharacter.over_mastery.length > 0
) {
return (
<section className={styles.mastery}>
<h5 className={tintElement}>
{t('modals.characters.subtitles.ring')}
</h5>
<ul>
{[...Array(4)].map((e, i) => {
const ringStat: ExtendedMastery =
props.gridCharacter.over_mastery[i]
if (ringStat && ringStat.modifier && ringStat.modifier > 0) {
if (i === 0 || i === 1) {
return masteryElement(overMastery.a, ringStat)
} else if (i === 2) {
return masteryElement(overMastery.b, ringStat)
} else {
return masteryElement(overMastery.c, ringStat)
}
}
})}
</ul>
</section>
)
}
}
const aetherialMasterySection = () => {
if (
props.gridCharacter &&
props.gridCharacter.over_mastery &&
props.gridCharacter.aetherial_mastery &&
props.gridCharacter.aetherial_mastery?.modifier > 0
) {
return (
<section className={styles.mastery}>
<h5 className={tintElement}>
{t('modals.characters.subtitles.earring')}
</h5>
<ul>
{masteryElement(
aetherialMastery,
props.gridCharacter.aetherial_mastery
)}
</ul>
</section>
)
}
}
const permanentMasterySection = () => {
if (props.gridCharacter && props.gridCharacter.perpetuity) {
return (
<section className={styles.mastery}>
<h5 className={tintElement}>
{t('modals.characters.subtitles.permanent')}
</h5>
<ul>
{[...Array(4)].map((e, i) => {
return masteryElement(permanentMastery, {
modifier: i + 1,
strength: permanentMastery[i].maxValue,
})
})}
</ul>
</section>
)
}
}
const awakeningSection = () => {
if (props.gridCharacter.awakening) {
const gridAwakening = props.gridCharacter.awakening
return (
<section className={styles.awakening}>
<h5 className={tintElement}>
{t('modals.characters.subtitles.awakening')}
</h5>
<div>
{gridAwakening.type.slug !== 'character-balanced' && (
<img
alt={gridAwakening.type.name[locale]}
src={`${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/awakening/${gridAwakening.type.slug}.jpg`}
/>
)}
<span>
<strong>{`${gridAwakening.type.name[locale]}`}</strong>&nbsp;
{`Lv${gridAwakening.level}`}
</span>
</div>
</section>
)
}
}
const wikiButton = (
<Button
className={tintElement}
text={t('buttons.wiki')}
onClick={goTo}
bound={true}
/>
)
return (
<Hovercard openDelay={350}>
<HovercardTrigger asChild onClick={props.onTriggerClick}>
{props.children}
</HovercardTrigger>
<HovercardContent className={styles.content} side="top">
<HovercardHeader
gridObject={props.gridCharacter}
object={props.gridCharacter.object}
type="character"
/>
{wikiButton}
{awakeningSection()}
{overMasterySection()}
{aetherialMasterySection()}
{permanentMasterySection()}
</HovercardContent>
</Hovercard>
)
}
export default CharacterHovercard