Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 0 additions & 6 deletions config/scripts/locale-ko-key-overrides.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": "/"
},
Expand Down
107 changes: 107 additions & 0 deletions src/renderer/src/components/pet/PetOverlay.keyframes.test.tsx
Original file line number Diff line number Diff line change
@@ -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(
<T,>(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(<PetOverlay />)
})
return { container, root }
}

function installLocalStorage(): void {
const values = new Map<string, string>()
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('到 {')
})
})
23 changes: 9 additions & 14 deletions src/renderer/src/components/pet/PetOverlay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<CustomPet['sprite']>

Expand Down Expand Up @@ -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 (
<>
<style>
{translate(
'auto.components.pet.PetOverlay.4712d196c6',
'@keyframes pet-{{value0}} { from { background-position: {{value1}}px {{value2}}px; } to { background-position: {{value3}}px {{value4}}px; } }',
{ value0: animKeyframesId, value1: startX, value2: startY, value3: endX, value4: startY }
)}
</style>
<style>{keyframesCss}</style>
<div
style={{
width: renderedW,
Expand Down Expand Up @@ -285,6 +281,10 @@ function defaultPosition(size: number = SIZE): Position {
)
}

// Why: the bob float is runtime CSS, not user-visible copy; keep CSS keywords
// out of i18n so translated locales cannot invalidate the keyframes.
const PET_BOB_KEYFRAMES_CSS =
'@keyframes pet-bob { 0%,100% { transform: translateY(0); } 50% { transform: translateY(-4px); } }'
export function PetOverlay(): React.JSX.Element {
const documentVisible = useDocumentVisible()
const reducedMotion = usePrefersReducedMotion()
Expand Down Expand Up @@ -417,12 +417,7 @@ export function PetOverlay(): React.JSX.Element {
minHeight: 24
}}
>
<style>
{translate(
'auto.components.pet.PetOverlay.de932b0e8f',
'@keyframes pet-bob { 0%,100% { transform: translateY(0); } 50% { transform: translateY(-4px); } }'
)}
</style>
<style>{PET_BOB_KEYFRAMES_CSS}</style>
{sprite ? (
<SpriteFrame
url={url}
Expand Down
4 changes: 0 additions & 4 deletions src/renderer/src/i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -9793,10 +9793,6 @@
"a84d5677ff": "OpenCode",
"2528586aa7": "Claudino"
}
},
"PetOverlay": {
"de932b0e8f": "@keyframes pet-bob { 0%,100% { transform: translateY(0); } 50% { transform: translateY(-4px); } }",
"4712d196c6": "@keyframes pet-{{value0}} { from { background-position: {{value1}}px {{value2}}px; } to { background-position: {{value3}}px {{value4}}px; } }"
}
},
"onboarding": {
Expand Down
4 changes: 0 additions & 4 deletions src/renderer/src/i18n/locales/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -9793,10 +9793,6 @@
"a84d5677ff": "OpenCode",
"2528586aa7": "Claudino"
}
},
"PetOverlay": {
"de932b0e8f": "@keyframes pet-bob { 0%,100% { transform: translateY(0); } 50% { transform: translateY(-4px); } }",
"4712d196c6": "@keyframes pet-{{value0}} { from { background-position: {{value1}}px {{value2}}px; } to { background-position: {{value3}}px {{value4}}px; } }"
}
},
"onboarding": {
Expand Down
4 changes: 0 additions & 4 deletions src/renderer/src/i18n/locales/ja.json
Original file line number Diff line number Diff line change
Expand Up @@ -9793,10 +9793,6 @@
"a84d5677ff": "OpenCode",
"2528586aa7": "クラウディーノ"
}
},
"PetOverlay": {
"de932b0e8f": "@keyframes ペットボブ { 0%,100% { 変換:translateY(0); } 50% { 変換:translateY(-4px); } }",
"4712d196c6": "@keyframes pet-{{value0}} { from { 背景位置: {{value1}}px {{value2}}px; } から { 背景位置: {{value3}}px {{value4}}px; } }"
}
},
"onboarding": {
Expand Down
4 changes: 0 additions & 4 deletions src/renderer/src/i18n/locales/ko.json
Original file line number Diff line number Diff line change
Expand Up @@ -9793,10 +9793,6 @@
"a84d5677ff": "OpenCode",
"2528586aa7": "클라우디노"
}
},
"PetOverlay": {
"de932b0e8f": "@keyframes pet-bob { 0%,100% { transform: translateY(0); } 50% { transform: translateY(-4px); } }",
"4712d196c6": "@keyframes pet-{{value0}} { from { background-position: {{value1}}px {{value2}}px; } to { background-position: {{value3}}px {{value4}}px; } }"
}
},
"onboarding": {
Expand Down
4 changes: 0 additions & 4 deletions src/renderer/src/i18n/locales/zh.json
Original file line number Diff line number Diff line change
Expand Up @@ -9793,10 +9793,6 @@
"a84d5677ff": "OpenCode",
"2528586aa7": "克劳迪诺"
}
},
"PetOverlay": {
"de932b0e8f": "@keyframes pet-bob { 0%,100% { 变换:translateY(0); } 50% { 变换:translateY(-4px); } }",
"4712d196c6": "@keyframes pet-{{value0}} { from { 背景位置:{{value1}}px {{value2}}px; } 到 { 背景位置:{{value3}}px {{value4}}px; } }"
}
},
"onboarding": {
Expand Down
Loading