Button assets
- Re-introduce button-asset type with 4 content states (normal/hover/
pressed/disabled) stored as keyframes at BUTTON_STATE_TIMES, plus a
separate hit-area editor mode.
- useButtonRuntime composable for per-instance state/disabled flags on
the main stage.
- Sidebar library lists button assets with live 80×80 thumbnails;
renderButtonThumbnail now accepts an images map so image-kind layers
inside a button render instead of producing a fully-transparent PNG.
- Compact (no-thumbnail) mode aligns button rows to the library-row
height.
Scope footer
- Button-edit mode now uses the same sidebar scope-footer pattern as
clip-edit (back button to "Main timeline" + active scope chip);
removed the inline exit button from the timeline scrub-bar.
Timeline
- Track-name column default bumped to 260px to match the assets sidebar
above it (storage key bumped to :v2 so stale values reset).
- scrub-bar z-index raised above sticky track-name columns so rows
scroll under it instead of over.
- scrub-handle flips z-index based on horizontal scroll: above
name-spacer when the playhead is in view (no half-clip at t=0),
below when the playhead has scrolled off the left edge (so the
toggle row cleanly covers it).
- name-spacer extends ±3/1px to fully cover the obscured handle.
- scrub-handle head pokes 3px above the ruler with rounded top corners
via clip-path path().
Track context menu
- Added missing i18n keys (renameScriptTrack / deleteScriptTrack /
renameScriptTrackDialog.{title,confirm}).
- Rename items show a visible pencil icon; empty .check slot removed
so items line up flush-left; toggle items place their active ✓ on
the right (kf-menu-trailing-check).
Dev HUD
- Test progress bar flips to --danger as soon as the first failure is
recorded, instead of only at the end of the run.
Regression tests
- renderButtonThumbnail: null on empty button, renders shape layers,
accepts custom images map.
- i18n: renameScriptTrack / deleteScriptTrack / dialog keys resolve.
- scrub-handle-obscured logic (visible at t=0, hidden when playhead is
scrolled off-screen, still visible when playhead is far to the
right of the scroll offset).
- Updated button state-scrubber expectations to match
BUTTON_STATE_TIMES (normal=1, hover=2, pressed=3, disabled=4) and
that hit-mode parks at normal=1 rather than 0.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
444 lines
14 KiB
TypeScript
444 lines
14 KiB
TypeScript
export const en = {
|
||
app: {
|
||
undo: 'Undo (Ctrl+Z)',
|
||
redo: 'Redo (Ctrl+Shift+Z)',
|
||
jumpToStart: 'Jump to start',
|
||
pause: 'Pause (Space)',
|
||
play: 'Play (Space)',
|
||
stop: 'Stop',
|
||
recentProjects: 'Recent projects',
|
||
saveProject: 'Save project (browser storage)',
|
||
documentSettings: 'Document settings',
|
||
export: 'Export',
|
||
mainTimeline: 'Main timeline',
|
||
dragResize: 'Drag to resize timeline',
|
||
loadFailed: 'Failed to load project.',
|
||
savedToast: '"{name}" saved',
|
||
saveDialog: {
|
||
title: 'Save project',
|
||
message: 'What should the project be called?',
|
||
confirm: 'Save',
|
||
defaultName: 'Project {date}',
|
||
},
|
||
replaceDialog: {
|
||
title: 'Replace project?',
|
||
message:
|
||
'The current project will be replaced by the loaded file. Unsaved changes will be lost.',
|
||
confirm: 'Continue',
|
||
},
|
||
},
|
||
|
||
welcome: {
|
||
heading: 'Welcome to Animatr',
|
||
subheading: 'Recent projects',
|
||
loading: 'Loading…',
|
||
empty: 'No saved projects yet.',
|
||
layerCount: '{n} layers',
|
||
today: 'today {time}',
|
||
deleteTooltip: 'Delete project',
|
||
newProject: 'New project',
|
||
importFile: 'Import file…',
|
||
exportCurrent: 'Export current',
|
||
deleteDialog: {
|
||
title: 'Delete project?',
|
||
message: '"{name}" will be permanently removed from browser storage.',
|
||
confirm: 'Delete',
|
||
},
|
||
},
|
||
|
||
sidebar: {
|
||
addFile: 'Add SVG / image',
|
||
library: 'Library',
|
||
thumbnailsHide: 'Hide thumbnails',
|
||
thumbnailsShow: 'Show thumbnails',
|
||
imageLabel: 'Image',
|
||
addLayerTooltip: 'Insert as layer (or drag to stage)',
|
||
noImages: 'No images imported',
|
||
clips: 'Clips',
|
||
createClipTooltip: 'Create new clip',
|
||
clipLayerCount: '{n} layers',
|
||
renameTooltip: 'Rename',
|
||
deleteTooltip: 'Delete',
|
||
noClips: 'No clips',
|
||
shape: {
|
||
rect: 'Rect',
|
||
circle: 'Circle',
|
||
text: 'Text',
|
||
polygon: 'Polygon',
|
||
freeform: 'Freeform (click to draw, Enter to finish, Esc to cancel)',
|
||
freeformActive: 'Drawing… (Enter to finish, Esc to cancel)',
|
||
button: 'Button (5-frame asset)',
|
||
},
|
||
buttons: 'Buttons',
|
||
noButtons: 'No buttons yet',
|
||
createButtonTooltip: 'Create new button asset',
|
||
hitModeDisabled: 'Switch back to button states to add assets.',
|
||
newButtonDialog: {
|
||
title: 'New button',
|
||
nameMessage: 'Button name:',
|
||
defaultName: 'Button',
|
||
next: 'Next',
|
||
sizeMessage: 'Canvas size (width × height):',
|
||
confirm: 'Create',
|
||
},
|
||
renameButtonDialog: {
|
||
title: 'Rename button',
|
||
confirm: 'Apply',
|
||
},
|
||
deleteButtonDialog: {
|
||
title: 'Delete button?',
|
||
message: 'The button asset will be removed. Any layers that reference it will no longer render.',
|
||
confirm: 'Delete',
|
||
},
|
||
newTextDialog: {
|
||
title: 'New text object',
|
||
message: 'Text:',
|
||
default: 'Hello',
|
||
confirm: 'Insert',
|
||
fallback: 'Text',
|
||
},
|
||
newPolygonDialog: {
|
||
title: 'New polygon',
|
||
message: 'Number of points (3–24):',
|
||
confirm: 'Create',
|
||
},
|
||
newClipDialog: {
|
||
title: 'New clip',
|
||
message: 'Clip name:',
|
||
default: 'Clip',
|
||
confirm: 'Create',
|
||
},
|
||
renameClipDialog: {
|
||
title: 'Rename clip',
|
||
confirm: 'Apply',
|
||
},
|
||
deleteClipDialog: {
|
||
title: 'Delete clip?',
|
||
message: 'The clip and all placed instances will be removed.',
|
||
confirm: 'Delete',
|
||
},
|
||
},
|
||
|
||
properties: {
|
||
keyframeLabel: 'Keyframe',
|
||
interpolated: 'Interpolated',
|
||
removeKeyframe: 'Remove keyframe here',
|
||
deleteLayer: 'Delete layer',
|
||
noLayer: 'No layer selected.',
|
||
fill: 'Fill',
|
||
transparent: 'Transparent',
|
||
opacity: 'Opacity',
|
||
edgeBlur: 'Edge blur',
|
||
sections: {
|
||
transform: 'Transform',
|
||
text: 'Text',
|
||
polygon: 'Polygon',
|
||
appearance: 'Appearance',
|
||
button: 'On Click',
|
||
},
|
||
polygonPoints: 'Points',
|
||
polygonPointsHint: 'Changing the point count rewrites all keyframes; new points are inserted at the midpoint of the last edge.',
|
||
polygonClosed: 'Closed path (fill)',
|
||
stroke: 'Stroke',
|
||
strokeWidth: 'Stroke width',
|
||
shadow: 'Shadow',
|
||
shadowBlur: 'Shadow blur',
|
||
shadowOpacity: 'Shadow opacity',
|
||
shadowAngle: 'Shadow angle',
|
||
shadowDistance: 'Shadow distance',
|
||
shadowColor: 'Shadow color',
|
||
mask: 'Mask',
|
||
maskUse: 'Use as mask',
|
||
maskMode: 'Mask mode',
|
||
maskPositive: 'Positive',
|
||
maskNegative: 'Negative',
|
||
maskHint: 'Masks everything below it in the same group.',
|
||
maskActive: 'Mask is active — affects layers below it in its group.',
|
||
maskNeedsGroup: 'Add this layer to a track group to activate masking. Right-click a track and choose "Add group".',
|
||
},
|
||
|
||
fullscreen: {
|
||
pause: 'Pause',
|
||
play: 'Play',
|
||
jumpToStart: 'Jump to start',
|
||
close: 'Close (Esc)',
|
||
},
|
||
|
||
timeline: {
|
||
colors: {
|
||
none: 'None',
|
||
red: 'Red',
|
||
orange: 'Orange',
|
||
yellow: 'Yellow',
|
||
green: 'Green',
|
||
cyan: 'Cyan',
|
||
blue: 'Blue',
|
||
purple: 'Purple',
|
||
pink: 'Pink',
|
||
gray: 'Gray',
|
||
},
|
||
noTracks: 'No tracks yet.',
|
||
addClipTrack: 'Add clip track',
|
||
addClipTrackTooltip: 'Add another clip track',
|
||
dropNewTrackHint: 'Drop a clip here to add a new track',
|
||
addGroup: 'Add group',
|
||
addGroupTooltip: 'Create a new track group',
|
||
scriptTrackName: 'Script track',
|
||
addScriptTrackTooltip: 'Create a new script track',
|
||
scriptFrameTooltip: 'Script frame — {n} action(s)',
|
||
buttonEditLabel: 'Editing button:',
|
||
buttonEditExit: 'Exit',
|
||
emptyButtonState: 'Empty state — drop shapes or images from the sidebar.',
|
||
buttonModeStates: 'Edit button states (normal / hover / pressed / disabled)',
|
||
buttonModeStatesShort: 'States',
|
||
buttonModeHit: 'Edit the click hit area',
|
||
buttonModeHitShort: 'Hit area',
|
||
hitAreaHeader: 'Hit area',
|
||
hitAreaHint: 'Click the canvas tools to add shapes',
|
||
hitAreaEmpty: 'No hit shapes yet — add a rect or circle from the canvas.',
|
||
hitTrackDelete: 'Delete hit shape',
|
||
addKeyframeTooltip: 'Add keyframe for the selected track at the playhead',
|
||
groupChildCount: '{n} tracks',
|
||
groupEmpty: 'Drop tracks here',
|
||
renameGroupDialog: {
|
||
title: 'Rename group',
|
||
confirm: 'Apply',
|
||
},
|
||
renameScriptTrackDialog: {
|
||
title: 'Rename script track',
|
||
confirm: 'Apply',
|
||
},
|
||
ungroup: 'Ungroup',
|
||
renameGroup: 'Rename group',
|
||
renameScriptTrack: 'Rename script track',
|
||
resizeTrackColumn: 'Drag to resize track name column',
|
||
clipTrackName: 'Clip track',
|
||
toggleTimeUnit: 'Toggle time unit',
|
||
snapOn: 'Snapping on (playhead & keyframes snap together)',
|
||
snapOff: 'Snapping off',
|
||
hideLocked: 'Hide locked tracks',
|
||
showLocked: 'Show locked tracks',
|
||
inPoint: 'In point — drag to set where playback and export start',
|
||
outPoint: 'Out point — drag to set where playback and export end',
|
||
renameMarkerTitle: 'Rename marker',
|
||
renameMarkerMessage: 'Pick a unique name — used as a jump target in scripts.',
|
||
renameMarkerConfirm: 'Rename',
|
||
renameMarkerCollision: 'A marker named "{name}" already exists.',
|
||
renameMarkerRetry: 'Try again',
|
||
stillFrameTooltip: 'Still frame {time} — holds until the next keyframe',
|
||
keyframeTooltip: 'Keyframe {time} — drag to move, right-click for options',
|
||
trimTooltip: '{name} · {duration}s{trims} · Drag edges to trim, double-click to open',
|
||
trimStart: 'Start -{value}s',
|
||
trimEnd: 'End -{value}s',
|
||
easing: 'Easing',
|
||
easingLinear: 'Linear',
|
||
easingEaseIn: 'Ease In',
|
||
easingEaseOut: 'Ease Out',
|
||
easingEaseInOut: 'Ease In-Out',
|
||
firstKfHint: 'First keyframe — easing has no effect',
|
||
holdDisabledHint: 'Still frame — easing disabled',
|
||
lastKfHint: 'Last keyframe — still frame has no effect',
|
||
stillFrame: 'Still frame',
|
||
duplicate: 'Duplicate',
|
||
delete: 'Delete',
|
||
color: 'Color',
|
||
lock: 'Lock',
|
||
unlock: 'Unlock',
|
||
deleteLayer: 'Delete layer',
|
||
deleteClipTrack: 'Delete clip track',
|
||
deleteScriptTrack: 'Delete script track',
|
||
separate: 'Separate Position and Rotation',
|
||
rotation: 'Rotation',
|
||
position: 'Position',
|
||
removeInstanceDialog: {
|
||
title: 'Remove clip instance?',
|
||
message: 'Remove "{name}" at {time}s from the timeline.',
|
||
generic: 'Remove this clip instance from the timeline.',
|
||
confirm: 'Remove',
|
||
},
|
||
},
|
||
|
||
canvas: {
|
||
zoomIn: 'Zoom in',
|
||
zoomOut: 'Zoom out',
|
||
zoomOneToOne: 'Zoom 1:1',
|
||
fitToView: 'Fit to view',
|
||
cropOn: 'Show stage only (crop)',
|
||
cropOff: 'Show everything',
|
||
maskPreviewOn: 'Preview masks (render view)',
|
||
maskPreviewOff: 'Back to edit view',
|
||
stagePreviewOn: 'Turn stage preview on — buttons become interactive',
|
||
stagePreviewOff: 'Turn stage preview off — buttons behave as normal assets',
|
||
vertexCorner: 'Corner',
|
||
vertexCurve: 'Curve (smooth)',
|
||
vertexSplitCurve: 'Split curve',
|
||
hit: {
|
||
addRect: 'Add rect hit area',
|
||
addCircle: 'Add circle hit area',
|
||
addPolygon: 'Add polygon hit area',
|
||
remove: 'Remove selected hit area',
|
||
},
|
||
},
|
||
|
||
settings: {
|
||
title: 'Settings',
|
||
tab: {
|
||
document: 'Document',
|
||
timeline: 'Timeline',
|
||
clipart: 'Clipart',
|
||
theme: 'UI Theme',
|
||
},
|
||
bgVideoLabel: 'Video',
|
||
bgVideoNone: 'No background video',
|
||
bgVideoPickFile: 'Pick video file…',
|
||
bgVideoPickUrl: 'Set URL…',
|
||
bgVideoClear: 'Remove background video',
|
||
bgVideoLocalFile: 'Local video file',
|
||
bgVideoHint:
|
||
'The video plays behind all layers and tracks the playhead. For best results use MP4 (H.264). The source is stored with the project (data URL for local files) or referenced by URL. In the export the video is not embedded — the exported HTML references the same URL.',
|
||
bgVideoStageSizeTitle: 'Apply video dimensions?',
|
||
bgVideoStageSizeMessage:
|
||
'The video is {w}×{h}. Your canvas is currently {cw}×{ch}. Resize the canvas to match the video?',
|
||
bgVideoStageSizeConfirm: 'Resize canvas',
|
||
bgVideoStageSizeCancel: 'Keep current size',
|
||
bgVideoDurationTitle: 'Extend scene duration?',
|
||
bgVideoDurationMessage:
|
||
'The video is {d}s. Scene duration is {cd}s. Extend the scene to match the video length?',
|
||
bgVideoDurationConfirm: 'Extend duration',
|
||
bgVideoDurationCancel: 'Keep current duration',
|
||
bgVideoUrlTitle: 'Background video URL',
|
||
bgVideoUrlMessage: 'Paste the video URL (must be reachable by the browser):',
|
||
bgVideoUrlConfirm: 'Load',
|
||
canvas: 'Canvas',
|
||
width: 'Width',
|
||
height: 'Height',
|
||
background: 'Background',
|
||
duration: 'Duration (s)',
|
||
fps: 'FPS',
|
||
timelineHint: 'Timeline length sets the ruler scale. Playback stops at the last keyframe.',
|
||
themeHint: 'Saved in the browser and reused on next open.',
|
||
themeDark: 'Dark',
|
||
themeLight: 'Light',
|
||
done: 'Done',
|
||
},
|
||
|
||
export: {
|
||
title: 'Export',
|
||
runtimeBadge: '1× per page',
|
||
runtimeDesc: 'Engine — include once; all scenes on the page share the runtime',
|
||
sceneBadge: 'Scene',
|
||
sceneDesc: 'Scene data only — requires the runtime to be loaded',
|
||
exampleTitle: 'Example HTML',
|
||
exampleDesc: 'Standalone HTML — just open it in a browser',
|
||
mp4Badge: 'Server · Frame-accurate',
|
||
mp4Desc: 'H.264, any resolution — requires render server',
|
||
webmBadge: 'Browser · Realtime',
|
||
webmDesc: 'No server — recorded in real time',
|
||
webmNotSupported: 'Browser does not support MediaRecorder',
|
||
cancel: 'Cancel',
|
||
failed: 'Export failed',
|
||
back: 'Back',
|
||
labelWebm: 'Rendering WebM (browser, realtime)…',
|
||
labelMp4: 'Rendering MP4 (server)…',
|
||
},
|
||
|
||
tests: {
|
||
running: 'Tests running',
|
||
title: 'Test Runner',
|
||
runAgain: 'Run again',
|
||
close: 'Close',
|
||
copyTooltip: 'Copy report to clipboard',
|
||
copiedTooltip: 'Report copied',
|
||
copiedBanner: 'Error report copied to clipboard.',
|
||
copyFailedBanner: 'Copy failed — use the clipboard icon to trigger manually.',
|
||
emptyState: 'No results — click "Run again" or press {shortcut}.',
|
||
testsCountLabel: '{n} tests',
|
||
passedLabel: '{n} passed',
|
||
failedLabel: '{n} failed',
|
||
},
|
||
|
||
clipart: {
|
||
title: 'Clipart library',
|
||
open: 'Open clipart library',
|
||
close: 'Close clipart library',
|
||
refresh: 'Reload folder',
|
||
empty: 'Choose a folder in settings to populate the library.',
|
||
noFiles: 'No image files in this folder.',
|
||
loading: 'Loading…',
|
||
unsupported: 'Your browser does not support folder access. Try Chrome or Edge.',
|
||
chooseFolder: 'Choose folder…',
|
||
clearFolder: 'Clear',
|
||
folderLabel: 'Folder',
|
||
noFolder: 'No folder selected',
|
||
settingsHint:
|
||
'Point to a local folder with your SVG/PNG/JPG clipart. The app remembers the folder; permissions may be re-asked after browser restart.',
|
||
},
|
||
|
||
dialog: {
|
||
confirm: 'Confirm',
|
||
input: 'Input',
|
||
ok: 'OK',
|
||
cancel: 'Cancel',
|
||
close: 'Close',
|
||
},
|
||
|
||
about: {
|
||
openTooltip: 'About Animatr',
|
||
title: 'About {name}',
|
||
tagline: 'A timeline-based animation editor that runs in your browser.',
|
||
versionLabel: 'Version',
|
||
buildLabel: 'Build',
|
||
close: 'Close',
|
||
shortcutsHeading: 'Shortcuts',
|
||
shortcut: {
|
||
undo: 'Ctrl+Z',
|
||
redo: 'Ctrl+Shift+Z',
|
||
playPause: 'Space',
|
||
fullscreen: 'Shift+Space',
|
||
tests: 'Ctrl+Shift+Alt+T',
|
||
panCanvas: 'Middle-click / Ctrl+Drag',
|
||
},
|
||
shortcutLabel: {
|
||
undo: 'Undo',
|
||
redo: 'Redo',
|
||
playPause: 'Play / Pause',
|
||
fullscreen: 'Fullscreen preview',
|
||
tests: 'Open test runner',
|
||
panCanvas: 'Pan canvas',
|
||
},
|
||
},
|
||
actions: {
|
||
empty: 'No actions yet. Add one below.',
|
||
add: 'Add action',
|
||
remove: 'Remove',
|
||
moveUp: 'Move up',
|
||
moveDown: 'Move down',
|
||
noMarkers: 'No markers defined',
|
||
type: {
|
||
play: 'Play',
|
||
pause: 'Pause',
|
||
rewindToLoop: 'Rewind to loop start',
|
||
jumpTo: 'Jump to time',
|
||
jumpToFrame: 'Jump to frame',
|
||
jumpToMarker: 'Jump to marker',
|
||
script: 'Run script',
|
||
},
|
||
param: {
|
||
time: 'Seconds',
|
||
frame: 'Frame',
|
||
marker: 'Marker',
|
||
source: 'Script source',
|
||
},
|
||
},
|
||
scriptFrameDialog: {
|
||
title: 'Script frame',
|
||
time: 'Time (s)',
|
||
label: 'Label (optional)',
|
||
labelPlaceholder: 'e.g. intro-end',
|
||
remove: 'Delete frame',
|
||
done: 'Done',
|
||
},
|
||
} as const
|
||
|
||
export type Messages = typeof en
|