diff --git a/config/scripts/locale-ko-key-overrides.json b/config/scripts/locale-ko-key-overrides.json index 071a249e01..953eb8a21f 100644 --- a/config/scripts/locale-ko-key-overrides.json +++ b/config/scripts/locale-ko-key-overrides.json @@ -1559,12 +1559,6 @@ "auto.components.onboarding.ThemeStep.94b9dc561d": { "ko": "설정 -> Terminal" }, - "auto.components.pet.PetOverlay.4712d196c6": { - "ko": "@keyframes pet-{{value0}} { from { background-position: {{value1}}px {{value2}}px; } to { background-position: {{value3}}px {{value4}}px; } }" - }, - "auto.components.pet.PetOverlay.de932b0e8f": { - "ko": "@keyframes pet-bob { 0%,100% { transform: translateY(0); } 50% { transform: translateY(-4px); } }" - }, "auto.components.repo.NestedRepoChecklist.ea54c7bf8f": { "ko": "/" }, diff --git a/src/renderer/src/components/pet/PetOverlay.keyframes.test.tsx b/src/renderer/src/components/pet/PetOverlay.keyframes.test.tsx new file mode 100644 index 0000000000..5e264ff06b --- /dev/null +++ b/src/renderer/src/components/pet/PetOverlay.keyframes.test.tsx @@ -0,0 +1,107 @@ +// @vitest-environment happy-dom + +import { act } from 'react' +import { createRoot, type Root } from 'react-dom/client' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' + +import { i18n } from '../../i18n/i18n' + +const storeState = vi.hoisted(() => ({ + agentStatusByPaneKey: {}, + agentStatusEpoch: 0, + retainedAgentsByPaneKey: {}, + petSize: 180 +})) + +vi.mock('../../store', () => ({ + useAppStore: Object.assign( + (selector: (state: typeof storeState) => T): T => { + return selector(storeState) + }, + { + getState: () => storeState + } + ) +})) + +vi.mock('./usePetUrl', () => ({ + usePetUrl: () => ({ + url: 'blob:custom-pet', + ready: true, + sprite: { + frameWidth: 32, + frameHeight: 24, + columns: 6, + rows: 1, + sheetWidth: 192, + sheetHeight: 24, + fps: 6, + defaultAnimation: 'idle', + animations: { + idle: { row: 0, frames: 6 } + } + }, + detected: null + }) +})) + +import { PetOverlay } from './PetOverlay' + +function renderPetOverlay(): { container: HTMLDivElement; root: Root } { + const container = document.createElement('div') + document.body.appendChild(container) + const root = createRoot(container) + act(() => { + root.render() + }) + return { container, root } +} + +function installLocalStorage(): void { + const values = new Map() + Object.defineProperty(window, 'localStorage', { + configurable: true, + value: { + clear: () => values.clear(), + getItem: (key: string) => values.get(key) ?? null, + removeItem: (key: string) => values.delete(key), + setItem: (key: string, value: string) => values.set(key, value) + } + }) +} + +describe('PetOverlay sprite keyframes', () => { + let root: Root | null = null + let container: HTMLDivElement | null = null + + beforeEach(async () => { + await i18n.changeLanguage('zh') + installLocalStorage() + }) + + afterEach(async () => { + if (root) { + act(() => root?.unmount()) + } + container?.remove() + root = null + container = null + await i18n.changeLanguage('en') + }) + + it('keeps custom spritesheet keyframes valid in translated locales', () => { + ;({ container, root } = renderPetOverlay()) + + const css = Array.from(container.querySelectorAll('style')) + .map((style) => style.textContent ?? '') + .join('\n') + + expect(css).toContain('@keyframes pet-bob') + expect(css).toContain('transform: translateY(0)') + expect(css).toContain('background-position:') + expect(css).toContain('to { background-position:') + expect(css).not.toContain('变换') + expect(css).not.toContain('背景位置') + expect(css).not.toContain('到 {') + }) +}) diff --git a/src/renderer/src/components/pet/PetOverlay.tsx b/src/renderer/src/components/pet/PetOverlay.tsx index e55004a478..89fc81dc6d 100644 --- a/src/renderer/src/components/pet/PetOverlay.tsx +++ b/src/renderer/src/components/pet/PetOverlay.tsx @@ -6,7 +6,6 @@ import type { CustomPet } from '../../../../shared/types' import { useAppStore } from '../../store' import { AGENT_STATUS_STALE_AFTER_MS } from '../../../../shared/agent-status-types' import { selectPetAnimationName, type PetAnimationName } from './pet-agent-state' -import { translate } from '@/i18n/i18n' type Sprite = NonNullable @@ -66,15 +65,12 @@ function SpriteFrame({ const startY = -(row * sprite.frameHeight * scale) const endX = -(frames * sprite.frameWidth * scale) const duration = Math.max(0.1, frames / Math.max(0.1, sprite.fps)) + // Why: sprite keyframes are runtime CSS, not user-visible copy; translated + // CSS keywords make the browser discard the animation. + const keyframesCss = `@keyframes pet-${animKeyframesId} { from { background-position: ${startX}px ${startY}px; } to { background-position: ${endX}px ${startY}px; } }` return ( <> - +
- + {sprite ? (