- Unsaved-changes indicator: a small accent dot appears on the save
button whenever the scene has changed since the last save/load.
Save/load/new all mark the project clean on next tick.
- Cmd/Ctrl+S saves the project (reuses the same flow as the button;
prompts for a name on first save). Tooltip now mentions the
shortcut.
- Fullscreen preview plays to the OUT marker (or scene.duration when
no OUT is set) instead of looping at the last keyframe. Dropped
the runtime's effectiveEnd helper; RUNTIME_VERSION 13 → 14.
- "Still frame" keyframes no longer disable easing. A held kf's
easing still drives the segment INTO it from the previous kf, so
you can ease into a pause. Only the first kf disables easing
(nothing precedes it). Dropped `timeline.holdDisabledHint`.
- Settings dialog: hint text 11px → 13px (and dropped italic) so
the inline help is readable; section headings 10px → 13px.
- Canvas stage shadow: `shadowCfg` always emits explicit values plus
a `shadowEnabled` flag instead of returning `{}`. Konva's default
shadowOpacity is 1, so bailing with `{}` could leave stale shadow
attrs on the shape. Preview and editor now gate on the same
condition deterministically.
- Sidebar empty-state copy: "No clips" → "No clips yet" to match
"No buttons yet".
Regression tests: runtime rangeOut fallback is scene.duration (not
last kf); sidebar empty-states both contain "yet"; the
holdDisabledHint key is gone; saveProject tooltips mention the
shortcut.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
450 lines
14 KiB
TypeScript
450 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 (Ctrl/Cmd+S)',
|
||
saveProjectDirty: 'Save project (unsaved changes, Ctrl/Cmd+S)',
|
||
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}',
|
||
},
|
||
renameProject: 'Rename project',
|
||
renameProjectDialog: {
|
||
title: 'Rename project',
|
||
message: 'Choose a new name for this project.',
|
||
confirm: 'Rename',
|
||
},
|
||
untitledProject: 'Untitled project',
|
||
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 yet',
|
||
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',
|
||
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
|