diff --git a/CHANGELOG.md b/CHANGELOG.md index 55f49b2d9..666f72840 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ - Nested container navigation — each double-click goes one level deeper - Dashed border around entered container for visual feedback - Layer panel click syncs canvas scope automatically +- Vue SDK internationalization primitives — `useI18n()`, locale detection, persisted locale selection, lazy-loaded locale JSON files, and exported locale metadata for custom editor shells +- App language picker in the menu bar — switch UI locale without reloading - Added a vector curve editor and improved drawing experience with the pen tool - Resume pen drawing from existing open path endpoints — click an endpoint to continue the curve - Close open paths by dragging one endpoint to the other @@ -16,6 +18,13 @@ ### Fixes - Fix imported `.fig` file open and page-switch regressions — loaded documents now keep graph/store state in sync, remap imported canvas/page children correctly, and recompute imported auto-layout descendants when switching pages +- Fix first canvas render happening before fonts load — wait for fonts before the initial draw to avoid Safari and text measurement glitches +- Preserve `fig-kiwi` version on `.fig` roundtrip — imports keep the original header version instead of rewriting everything to a hardcoded value; new files default to version 101 +- Normalize auto-layout text export for Figma — text children inside auto-layout frames now serialize with `NONE` auto-resize to match Figma behavior and avoid overflow on reimport +- Fix keyboard editing regressions after the refactor — canvas shortcuts no longer fire while editing text, and Delete/Backspace no longer delete nodes during text entry +- Fix MCP page switching persistence — `switch_page` now survives across tool calls in the same session +- Improve CJK font fallback coverage — load multiple Google Fonts for broader Han/Japanese/Korean text support +- Normalize more visible UI strings for localized app chrome — menus, panels, variables dialog, code panel, chat setup, and editor controls now respect the selected locale instead of falling back to English in common flows - Fix imported text rendering in browser and headless export — preserve stored bounds until fonts are ready, restore missing font-loaded guards, use natural width for `WIDTH_AND_HEIGHT` text, and clip text to node bounds - Fix browser/headless rendering mismatch for imported toolbar/instance content by correcting runtime imported layout recomputation instead of diverging browser rendering behavior - Fix `set_layout` tool not defaulting to HUG sizing when enabling auto-layout — frames now shrink/grow to fit children instead of keeping fixed dimensions diff --git a/bun.lock b/bun.lock index 5f50da6e8..54dbf83e0 100644 --- a/bun.lock +++ b/bun.lock @@ -153,7 +153,10 @@ "name": "@open-pencil/vue", "version": "0.10.0", "dependencies": { + "@nanostores/i18n": "^1.2.2", + "@nanostores/vue": "^1.1.0", "@vueuse/core": "^14.2.1", + "nanostores": "^1.2.0", }, "peerDependencies": { "@open-pencil/core": "workspace:*", @@ -676,6 +679,10 @@ "@multiformats/multiaddr-to-uri": ["@multiformats/multiaddr-to-uri@11.0.2", "", { "dependencies": { "@multiformats/multiaddr": "^12.3.0" } }, "sha512-SiLFD54zeOJ0qMgo9xv1Tl9O5YktDKAVDP4q4hL16mSq4O4sfFNagNADz8eAofxd6TfQUzGQ3TkRRG9IY2uHRg=="], + "@nanostores/i18n": ["@nanostores/i18n@1.2.2", "", { "peerDependencies": { "nanostores": "^0.9.0 || ^0.10.0 || ^0.11.0 || ^1.0.0" } }, "sha512-5LLxl95+ZI46MrM/Kn7YjORKsD7+Xy2tgjZ7/oDT/BGPEiaBM9lK89/afeK+BqaQL0Xd9Xaa5MPuuVSyWAo+/w=="], + + "@nanostores/vue": ["@nanostores/vue@1.1.0", "", { "peerDependencies": { "@nanostores/logger": "^0.4.0 || ^1.0.0", "@vue/devtools-api": ">=7.6.2", "nanostores": "^0.11.3 || ^1.0.0", "vue": ">=3.3.1" }, "optionalPeers": ["@nanostores/logger", "@vue/devtools-api"] }, "sha512-14eY8/YA0AbtWkAjk8zHKxbjcuPubF+M7y9afQzg2b5VqRJcashwA0100vprau13BTBSynUmU6ZsU6x03YidjA=="], + "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.1", "", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@tybys/wasm-util": "^0.10.1" } }, "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A=="], "@noble/ciphers": ["@noble/ciphers@1.3.0", "", {}, "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw=="], @@ -1976,6 +1983,8 @@ "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], + "nanostores": ["nanostores@1.2.0", "", {}, "sha512-F0wCzbsH80G7XXo0Jd9/AVQC7ouWY6idUCTnMwW5t/Rv9W8qmO6endavDwg7TNp5GbugwSukFMVZqzPSrSMndg=="], + "negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], "netmask": ["netmask@2.0.2", "", {}, "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg=="], diff --git a/packages/vue/package.json b/packages/vue/package.json index c111279d5..88253b9b6 100644 --- a/packages/vue/package.json +++ b/packages/vue/package.json @@ -47,6 +47,9 @@ } }, "dependencies": { - "@vueuse/core": "^14.2.1" + "@nanostores/i18n": "^1.2.2", + "@nanostores/vue": "^1.1.0", + "@vueuse/core": "^14.2.1", + "nanostores": "^1.2.0" } } diff --git a/packages/vue/src/commands/useEditorCommands.ts b/packages/vue/src/commands/useEditorCommands.ts index 06f886507..0c64597c1 100644 --- a/packages/vue/src/commands/useEditorCommands.ts +++ b/packages/vue/src/commands/useEditorCommands.ts @@ -1,6 +1,8 @@ +import { useStore } from '@nanostores/vue' import { computed } from 'vue' import { useEditor } from '@open-pencil/vue/context/editorContext' +import { commandMessages } from '@open-pencil/vue/i18n' import { usePageList } from '@open-pencil/vue/PageList/usePageList' import { useSelectionCapabilities } from '@open-pencil/vue/selection/useSelectionCapabilities' import { useSelectionState } from '@open-pencil/vue/selection/useSelectionState' @@ -76,64 +78,86 @@ export function useEditorCommands() { const capabilities = useSelectionCapabilities() const { pages } = usePageList() + const t = useStore(commandMessages) + const commands: Record = { 'edit.undo': { id: 'edit.undo', - label: 'Undo', + get label() { + return t.value.undo + }, enabled: capabilities.canUndo, run: () => editor.undoAction() }, 'edit.redo': { id: 'edit.redo', - label: 'Redo', + get label() { + return t.value.redo + }, enabled: capabilities.canRedo, run: () => editor.redoAction() }, 'selection.selectAll': { id: 'selection.selectAll', - label: 'Select all', + get label() { + return t.value.selectAll + }, enabled: capabilities.canSelectAll, run: () => editor.selectAll() }, 'selection.duplicate': { id: 'selection.duplicate', - label: 'Duplicate', + get label() { + return t.value.duplicate + }, enabled: capabilities.canDuplicate, run: () => editor.duplicateSelected() }, 'selection.delete': { id: 'selection.delete', - label: 'Delete', + get label() { + return t.value.delete + }, enabled: capabilities.canDelete, run: () => editor.deleteSelected() }, 'selection.group': { id: 'selection.group', - label: 'Group', + get label() { + return t.value.group + }, enabled: capabilities.canGroup, run: () => editor.groupSelected() }, 'selection.ungroup': { id: 'selection.ungroup', - label: 'Ungroup', + get label() { + return t.value.ungroup + }, enabled: capabilities.canUngroup, run: () => editor.ungroupSelected() }, 'selection.createComponent': { id: 'selection.createComponent', - label: 'Create component', + get label() { + return t.value.createComponent + }, enabled: capabilities.canCreateComponent, run: () => editor.createComponentFromSelection() }, 'selection.createComponentSet': { id: 'selection.createComponentSet', - label: 'Create component set', + get label() { + return t.value.createComponentSet + }, enabled: capabilities.canCreateComponentSet, run: () => editor.createComponentSetFromComponents() }, 'selection.createInstance': { id: 'selection.createInstance', - label: 'Create instance', + get label() { + return t.value.createInstance + }, enabled: capabilities.canCreateInstance, run: () => { const node = selection.selectedNode.value @@ -142,49 +166,65 @@ export function useEditorCommands() { }, 'selection.detachInstance': { id: 'selection.detachInstance', - label: 'Detach instance', + get label() { + return t.value.detachInstance + }, enabled: capabilities.canDetachInstance, run: () => editor.detachInstance() }, 'selection.goToMainComponent': { id: 'selection.goToMainComponent', - label: 'Go to main component', + get label() { + return t.value.goToMainComponent + }, enabled: capabilities.canGoToMainComponent, run: () => editor.goToMainComponent() }, 'selection.wrapInAutoLayout': { id: 'selection.wrapInAutoLayout', - label: 'Add auto layout', + get label() { + return t.value.addAutoLayout + }, enabled: capabilities.canWrapInAutoLayout, run: () => editor.wrapInAutoLayout() }, 'selection.bringToFront': { id: 'selection.bringToFront', - label: 'Bring to front', + get label() { + return t.value.bringToFront + }, enabled: capabilities.canBringToFront, run: () => editor.bringToFront() }, 'selection.sendToBack': { id: 'selection.sendToBack', - label: 'Send to back', + get label() { + return t.value.sendToBack + }, enabled: capabilities.canSendToBack, run: () => editor.sendToBack() }, 'selection.toggleVisibility': { id: 'selection.toggleVisibility', - label: 'Toggle visibility', + get label() { + return t.value.toggleVisibility + }, enabled: capabilities.canToggleVisibility, run: () => editor.toggleVisibility() }, 'selection.toggleLock': { id: 'selection.toggleLock', - label: 'Toggle lock', + get label() { + return t.value.toggleLock + }, enabled: capabilities.canToggleLock, run: () => editor.toggleLock() }, 'selection.moveToPage': { id: 'selection.moveToPage', - label: 'Move to page', + get label() { + return t.value.moveToPage + }, enabled: capabilities.canMoveToPage, run: () => { const targetPage = otherPages.value[0] @@ -193,19 +233,25 @@ export function useEditorCommands() { }, 'view.zoom100': { id: 'view.zoom100', - label: 'Zoom to 100%', + get label() { + return t.value.zoomTo100 + }, enabled: computed(() => true), run: () => editor.zoomTo100() }, 'view.zoomFit': { id: 'view.zoomFit', - label: 'Zoom to fit', + get label() { + return t.value.zoomToFit + }, enabled: computed(() => true), run: () => editor.zoomToFit() }, 'view.zoomSelection': { id: 'view.zoomSelection', - label: 'Zoom to selection', + get label() { + return t.value.zoomToSelection + }, enabled: capabilities.canZoomToSelection, run: () => editor.zoomToSelection() } diff --git a/packages/vue/src/commands/useMenuModel.ts b/packages/vue/src/commands/useMenuModel.ts index 9735065b9..8f88f35b9 100644 --- a/packages/vue/src/commands/useMenuModel.ts +++ b/packages/vue/src/commands/useMenuModel.ts @@ -1,7 +1,9 @@ +import { useStore } from '@nanostores/vue' import { computed } from 'vue' import { useEditorCommands } from '@open-pencil/vue/commands/useEditorCommands' import { useEditor } from '@open-pencil/vue/context/editorContext' +import { menuMessages } from '@open-pencil/vue/i18n' import { useSelectionState } from '@open-pencil/vue/selection/useSelectionState' /** @@ -37,6 +39,8 @@ export function useMenuModel() { const { hasSelection, isGroup, isInstance, isComponent, canCreateComponentSet, selectedNode } = useSelectionState() + const t = useStore(menuMessages) + const editMenu = computed(() => [ commandMenuItem('edit.undo'), commandMenuItem('edit.redo'), @@ -68,10 +72,10 @@ export function useMenuModel() { const arrangeMenu = computed(() => [commandMenuItem('selection.wrapInAutoLayout')]) const appMenu = computed(() => [ - { label: 'Edit', items: editMenu.value }, - { label: 'View', items: viewMenu.value }, - { label: 'Object', items: objectMenu.value }, - { label: 'Arrange', items: arrangeMenu.value } + { label: t.value.edit, items: editMenu.value }, + { label: t.value.view, items: viewMenu.value }, + { label: t.value.object, items: objectMenu.value }, + { label: t.value.arrange, items: arrangeMenu.value } ]) const canvasMenu = computed(() => { @@ -81,11 +85,11 @@ export function useMenuModel() { })) return [ - { ...commandMenuItem('selection.duplicate', '⌘D'), label: 'Duplicate' }, + commandMenuItem('selection.duplicate', '⌘D'), commandMenuItem('selection.delete', '⌫'), { separator: true }, ...(moveToPageSubmenu.length > 0 && hasSelection.value - ? [{ label: 'Move to page', sub: moveToPageSubmenu } satisfies MenuActionNode] + ? [{ label: t.value.moveToPage, sub: moveToPageSubmenu } satisfies MenuActionNode] : []), commandMenuItem('selection.bringToFront', ']'), commandMenuItem('selection.sendToBack', '['), @@ -101,7 +105,7 @@ export function useMenuModel() { ...(isComponent.value && selectedNode.value ? [ { - label: 'Create instance', + label: t.value.createInstance, action: () => commandMenuItem('selection.createInstance').action?.(), disabled: !commandMenuItem('selection.createInstance').disabled } satisfies MenuActionNode @@ -120,8 +124,8 @@ export function useMenuModel() { }) const selectionLabelMenu = computed(() => ({ - visibility: (editor.getSelectedNode()?.visible ?? true) ? 'Hide' : 'Show', - lock: (editor.getSelectedNode()?.locked ?? false) ? 'Unlock' : 'Lock' + visibility: (editor.getSelectedNode()?.visible ?? true) ? t.value.hide : t.value.show, + lock: (editor.getSelectedNode()?.locked ?? false) ? t.value.unlock : t.value.lock })) return { diff --git a/packages/vue/src/global.d.ts b/packages/vue/src/global.d.ts new file mode 100644 index 000000000..9ebba9189 --- /dev/null +++ b/packages/vue/src/global.d.ts @@ -0,0 +1,23 @@ +interface FontData { + family: string + fullName: string + postscriptName: string + style: string + blob(): Promise +} + +interface Window { + queryLocalFonts?(): Promise +} + +interface GestureEvent extends UIEvent { + scale: number + rotation: number + clientX: number + clientY: number +} + +declare module '*?raw' { + const content: string + export default content +} diff --git a/packages/vue/src/i18n/i18n.ts b/packages/vue/src/i18n/i18n.ts new file mode 100644 index 000000000..329df72f0 --- /dev/null +++ b/packages/vue/src/i18n/i18n.ts @@ -0,0 +1,24 @@ +import { createI18n } from '@nanostores/i18n' + +import { locale } from './locale' + +import type { Locale } from './locale' +import type { ComponentsJSON } from '@nanostores/i18n' + +const localeLoaders: Record, () => Promise<{ default: ComponentsJSON }>> = { + de: () => import('../locales/de.json'), + es: () => import('../locales/es.json'), + fr: () => import('../locales/fr.json'), + it: () => import('../locales/it.json'), + pl: () => import('../locales/pl.json'), + ru: () => import('../locales/ru.json') +} + +export const i18n = createI18n(locale, { + baseLocale: 'en', + async get(code) { + if (code === 'en') return {} + const mod = await localeLoaders[code]() + return mod.default + } +}) diff --git a/packages/vue/src/i18n/index.ts b/packages/vue/src/i18n/index.ts new file mode 100644 index 000000000..f87f43b0a --- /dev/null +++ b/packages/vue/src/i18n/index.ts @@ -0,0 +1,11 @@ +export { useI18n } from './useI18n' +export { locale, localeSetting, setLocale, AVAILABLE_LOCALES, LOCALE_LABELS } from './locale' +export type { Locale } from './locale' +export { + menuMessages, + commandMessages, + toolMessages, + panelMessages, + pageMessages, + dialogMessages +} from './messages' diff --git a/packages/vue/src/i18n/locale.ts b/packages/vue/src/i18n/locale.ts new file mode 100644 index 000000000..6bc485956 --- /dev/null +++ b/packages/vue/src/i18n/locale.ts @@ -0,0 +1,35 @@ +import { browser, localeFrom } from '@nanostores/i18n' +import { atom } from 'nanostores' + +export const AVAILABLE_LOCALES = ['en', 'de', 'es', 'fr', 'it', 'pl', 'ru'] as const +export type Locale = (typeof AVAILABLE_LOCALES)[number] + +export const LOCALE_LABELS: Record = { + en: 'English', + de: 'Deutsch', + es: 'Español', + fr: 'Français', + it: 'Italiano', + pl: 'Polski', + ru: 'Русский' +} + +const LOCALE_STORAGE_KEY = 'open-pencil-locale' + +export const localeSetting = atom(undefined) + +export const locale = localeFrom(localeSetting, browser({ available: AVAILABLE_LOCALES })) + +export function setLocale(code: Locale) { + localeSetting.set(code) + if (typeof localStorage !== 'undefined') { + localStorage.setItem(LOCALE_STORAGE_KEY, code) + } +} + +if (typeof localStorage !== 'undefined') { + const saved = localStorage.getItem(LOCALE_STORAGE_KEY) as Locale | null + if (saved && AVAILABLE_LOCALES.includes(saved)) { + localeSetting.set(saved) + } +} diff --git a/packages/vue/src/i18n/messages.ts b/packages/vue/src/i18n/messages.ts new file mode 100644 index 000000000..66f7aaa19 --- /dev/null +++ b/packages/vue/src/i18n/messages.ts @@ -0,0 +1,195 @@ +import { params } from '@nanostores/i18n' + +import { i18n } from './i18n' + +export const menuMessages = i18n('menu', { + file: 'File', + edit: 'Edit', + view: 'View', + object: 'Object', + arrange: 'Arrange', + text: 'Text', + + new: 'New', + open: 'Open…', + save: 'Save', + saveAs: 'Save as…', + exportSelection: 'Export selection…', + autosave: 'Auto-save to local file', + + copy: 'Copy', + paste: 'Paste', + + zoomIn: 'Zoom in', + zoomOut: 'Zoom out', + profiler: 'Performance profiler', + language: 'Language', + + moveToPage: 'Move to page', + createInstance: 'Create instance', + hide: 'Hide', + show: 'Show', + lock: 'Lock', + unlock: 'Unlock', + cut: 'Cut', + front: 'Front', + back: 'Back', + toggleUI: 'Toggle UI', + + bold: 'Bold', + italic: 'Italic', + underline: 'Underline' +}) + +export const commandMessages = i18n('commands', { + undo: 'Undo', + redo: 'Redo', + selectAll: 'Select all', + duplicate: 'Duplicate', + delete: 'Delete', + group: 'Group', + ungroup: 'Ungroup', + createComponent: 'Create component', + createComponentSet: 'Create component set', + createInstance: 'Create instance', + detachInstance: 'Detach instance', + goToMainComponent: 'Go to main component', + addAutoLayout: 'Add auto layout', + bringToFront: 'Bring to front', + sendToBack: 'Send to back', + toggleVisibility: 'Toggle visibility', + toggleLock: 'Toggle lock', + moveToPage: 'Move to page', + zoomTo100: 'Zoom to 100%', + zoomToFit: 'Zoom to fit', + zoomToSelection: 'Zoom to selection' +}) + +export const toolMessages = i18n('tools', { + move: 'Move', + frame: 'Frame', + section: 'Section', + rectangle: 'Rectangle', + ellipse: 'Ellipse', + line: 'Line', + polygon: 'Polygon', + star: 'Star', + pen: 'Pen', + text: 'Text', + hand: 'Hand' +}) + +export const panelMessages = i18n('panels', { + layers: 'Layers', + pages: 'Pages', + design: 'Design', + code: 'Code', + ai: 'AI', + assets: 'Assets', + + page: 'Page', + position: 'Position', + layout: 'Layout', + autoLayout: 'Auto layout', + alignment: 'Alignment', + appearance: 'Appearance', + fill: 'Fill', + stroke: 'Stroke', + effects: 'Effects', + export: 'Export', + typography: 'Typography', + variables: 'Variables', + constraints: 'Constraints', + + addFill: 'Add fill', + addStroke: 'Add stroke', + addEffect: 'Add effect', + addExport: 'Add export', + + dropShadow: 'Drop shadow', + innerShadow: 'Inner shadow', + layerBlur: 'Layer blur', + backgroundBlur: 'Background blur', + + noSelection: 'No selection', + noLocalVariables: 'No local variables', + openVariables: 'Open variables', + addPage: 'Add page', + toggleVisibility: 'Toggle visibility', + independentCornerRadii: 'Independent corner radii', + detachVariable: 'Detach variable', + applyVariable: 'Apply variable', + noVariablesFound: 'No variables found', + addAutoLayout: 'Add auto layout (Shift+A)', + removeAutoLayout: 'Remove auto layout', + alignLeft: 'Align left', + alignCenterHorizontally: 'Align center horizontally', + alignRight: 'Align right', + alignTop: 'Align top', + alignCenterVertically: 'Align center vertically', + alignBottom: 'Align bottom', + flipHorizontal: 'Flip horizontal', + flipVertical: 'Flip vertical', + rotate90: 'Rotate 90°', + mixedFillsHelp: 'Click + to replace mixed fills', + mixedStrokesHelp: 'Click + to replace mixed strokes', + mixedEffectsHelp: 'Click + to replace mixed effects', + strokeSides: 'Stroke sides', + mixed: 'Mixed', + layersCount: params('{count} layers'), + goToMainComponent: 'Go to Main Component', + detachInstance: 'Detach Instance', + + solid: 'Solid', + linearGradient: 'Linear', + radialGradient: 'Radial', + image: 'Image', + stops: 'Stops', + addStop: 'Add stop', + + alignCenter: 'Align center', + alignMiddle: 'Align middle' +}) + +export const pageMessages = i18n('pages', { + newPage: 'New page', + rename: 'Rename', + delete: 'Delete', + pageName: params('Page {number}') +}) + +export const dialogMessages = i18n('dialogs', { + cancel: 'Cancel', + apply: 'Apply', + close: 'Close', + ok: 'OK', + copy: 'Copy', + copied: 'Copied', + createCollection: 'Create collection', + localVariables: 'Local variables', + noVariableCollections: 'No variable collections', + selectLayerForJSX: 'Select a layer to see its JSX code', + connectAIProvider: 'Connect an AI provider to start chatting.', + connect: 'Connect', + getAPIKey: params('Get an {provider} API key →'), + oneKeyManyModels: 'One key for 100+ models from all providers.', + describeChange: 'Describe a change…', + describeCreateOrChange: 'Describe what you want to create or change.', + stopGenerating: 'Stop generating', + sendMessage: 'Send message', + baseURLPlaceholder: 'Base URL (e.g. http://localhost:11434/v1)', + modelIDPlaceholder: 'Model ID (e.g. llama-3.3-70b)', + yourName: 'Your name', + enterYourName: 'Enter your name', + shareThisFile: 'Share this file', + joinRoom: 'Join room', + join: 'Join', + roomLink: 'Room link', + joinCollaboration: 'Join collaboration', + orJoinRoom: 'or join a room', + pasteRoomLinkOrId: 'Paste room link or ID', + connected: 'Connected', + search: 'Search…', + noResults: 'No results', + share: 'Share' +}) diff --git a/packages/vue/src/i18n/useI18n.ts b/packages/vue/src/i18n/useI18n.ts new file mode 100644 index 000000000..7d238771b --- /dev/null +++ b/packages/vue/src/i18n/useI18n.ts @@ -0,0 +1,47 @@ +import { useStore } from '@nanostores/vue' + +import { locale, setLocale, AVAILABLE_LOCALES, LOCALE_LABELS } from './locale' +import { + menuMessages, + commandMessages, + toolMessages, + panelMessages, + pageMessages, + dialogMessages +} from './messages' + +import type { Locale } from './locale' +import type { Ref } from 'vue' + +/** + * Reactive i18n composable for OpenPencil Vue components. + * + * Returns reactive translation objects grouped by domain, plus locale + * controls. All values update automatically when the locale changes. + * + * @example + * ```vue + * + * + * + * ``` + */ +export function useI18n() { + return { + menu: useStore(menuMessages), + commands: useStore(commandMessages), + tools: useStore(toolMessages), + panels: useStore(panelMessages), + pages: useStore(pageMessages), + dialogs: useStore(dialogMessages), + locale: useStore(locale) as Ref, + availableLocales: AVAILABLE_LOCALES, + localeLabels: LOCALE_LABELS, + setLocale + } +} diff --git a/packages/vue/src/index.ts b/packages/vue/src/index.ts index 9f8b29863..ec02f2eac 100644 --- a/packages/vue/src/index.ts +++ b/packages/vue/src/index.ts @@ -86,3 +86,16 @@ export type { ScrubInputContext } from './ScrubInput' export { TypographyControlsRoot } from './TypographyControls' export { ToolbarRoot, ToolbarItem, useToolbar } from './Toolbar' export type { ToolbarContext } from './Toolbar' + +/** Internationalization. */ +export { useI18n } from './i18n' +export { locale, localeSetting, setLocale, AVAILABLE_LOCALES, LOCALE_LABELS } from './i18n' +export type { Locale } from './i18n' +export { + menuMessages, + commandMessages, + toolMessages, + panelMessages, + pageMessages, + dialogMessages +} from './i18n' diff --git a/packages/vue/src/locales/de.json b/packages/vue/src/locales/de.json new file mode 100644 index 000000000..dd55f7f98 --- /dev/null +++ b/packages/vue/src/locales/de.json @@ -0,0 +1,177 @@ +{ + "menu": { + "file": "Datei", + "edit": "Bearbeiten", + "view": "Ansicht", + "object": "Objekt", + "arrange": "Anordnen", + "text": "Text", + "new": "Neu", + "open": "Öffnen…", + "save": "Speichern", + "saveAs": "Speichern unter…", + "exportSelection": "Auswahl exportieren…", + "autosave": "Automatisch in Datei speichern", + "copy": "Kopieren", + "paste": "Einfügen", + "zoomIn": "Vergrößern", + "zoomOut": "Verkleinern", + "profiler": "Leistungsprofil", + "language": "Sprache", + "moveToPage": "Auf Seite verschieben", + "createInstance": "Instanz erstellen", + "hide": "Ausblenden", + "show": "Einblenden", + "lock": "Sperren", + "unlock": "Entsperren", + "cut": "Ausschneiden", + "front": "Nach vorn", + "back": "Nach hinten", + "toggleUI": "UI umschalten", + "bold": "Fett", + "italic": "Kursiv", + "underline": "Unterstrichen" + }, + "commands": { + "undo": "Rückgängig", + "redo": "Wiederholen", + "selectAll": "Alles auswählen", + "duplicate": "Duplizieren", + "delete": "Löschen", + "group": "Gruppieren", + "ungroup": "Gruppierung aufheben", + "createComponent": "Komponente erstellen", + "createComponentSet": "Komponentensatz erstellen", + "createInstance": "Instanz erstellen", + "detachInstance": "Instanz lösen", + "goToMainComponent": "Zur Hauptkomponente", + "addAutoLayout": "Auto-Layout hinzufügen", + "bringToFront": "In den Vordergrund", + "sendToBack": "In den Hintergrund", + "toggleVisibility": "Sichtbarkeit", + "toggleLock": "Sperren", + "moveToPage": "Auf Seite verschieben", + "zoomTo100": "Zoom 100 %", + "zoomToFit": "Einpassen", + "zoomToSelection": "Auswahl einpassen" + }, + "tools": { + "move": "Verschieben", + "frame": "Rahmen", + "section": "Abschnitt", + "rectangle": "Rechteck", + "ellipse": "Ellipse", + "line": "Linie", + "polygon": "Polygon", + "star": "Stern", + "pen": "Stift", + "text": "Text", + "hand": "Hand" + }, + "panels": { + "layers": "Ebenen", + "pages": "Seiten", + "design": "Design", + "code": "Code", + "ai": "KI", + "assets": "Elemente", + "page": "Seite", + "position": "Position", + "layout": "Layout", + "autoLayout": "Auto-Layout", + "alignment": "Ausrichtung", + "appearance": "Darstellung", + "fill": "Füllung", + "stroke": "Kontur", + "effects": "Effekte", + "export": "Export", + "typography": "Typografie", + "variables": "Variablen", + "constraints": "Einschränkungen", + "addFill": "Füllung hinzufügen", + "addStroke": "Kontur hinzufügen", + "addEffect": "Effekt hinzufügen", + "addExport": "Export hinzufügen", + "dropShadow": "Schlagschatten", + "innerShadow": "Innerer Schatten", + "layerBlur": "Ebenenunschärfe", + "backgroundBlur": "Hintergrundunschärfe", + "noSelection": "Keine Auswahl", + "noLocalVariables": "Keine lokalen Variablen", + "openVariables": "Variablen öffnen", + "addPage": "Seite hinzufügen", + "toggleVisibility": "Sichtbarkeit umschalten", + "independentCornerRadii": "Unabhängige Eckenradien", + "detachVariable": "Variable lösen", + "applyVariable": "Variable anwenden", + "noVariablesFound": "Keine Variablen gefunden", + "addAutoLayout": "Auto-Layout hinzufügen (Shift+A)", + "removeAutoLayout": "Auto-Layout entfernen", + "mixed": "Gemischt", + "layersCount": "{count} Ebenen", + "goToMainComponent": "Zur Hauptkomponente", + "detachInstance": "Instanz lösen", + "solid": "Vollton", + "linearGradient": "Linear", + "radialGradient": "Radial", + "image": "Bild", + "stops": "Stopps", + "addStop": "Stopp hinzufügen", + "alignLeft": "Links ausrichten", + "alignCenter": "Zentrieren", + "alignRight": "Rechts ausrichten", + "alignTop": "Oben ausrichten", + "alignMiddle": "Mittig ausrichten", + "alignBottom": "Unten ausrichten", + "alignCenterHorizontally": "Horizontal zentrieren", + "alignCenterVertically": "Vertikal zentrieren", + "flipHorizontal": "Horizontal spiegeln", + "flipVertical": "Vertikal spiegeln", + "rotate90": "90° drehen", + "mixedFillsHelp": "Klicke auf +, um gemischte Füllungen zu ersetzen", + "mixedStrokesHelp": "Klicke auf +, um gemischte Konturen zu ersetzen", + "mixedEffectsHelp": "Klicke auf +, um gemischte Effekte zu ersetzen", + "strokeSides": "Konturseiten" + }, + "pages": { + "newPage": "Neue Seite", + "rename": "Umbenennen", + "delete": "Löschen", + "pageName": "Seite {number}" + }, + "dialogs": { + "cancel": "Abbrechen", + "apply": "Anwenden", + "close": "Schließen", + "ok": "OK", + "copy": "Kopieren", + "copied": "Kopiert", + "createCollection": "Sammlung erstellen", + "localVariables": "Lokale Variablen", + "noVariableCollections": "Keine Variablensammlungen", + "selectLayerForJSX": "Wähle eine Ebene, um ihren JSX-Code zu sehen", + "connectAIProvider": "Verbinde einen KI-Anbieter, um den Chat zu starten.", + "connect": "Verbinden", + "getAPIKey": "Einen {provider}-API-Schlüssel erhalten →", + "oneKeyManyModels": "Ein Schlüssel für über 100 Modelle aller Anbieter.", + "describeChange": "Beschreibe eine Änderung…", + "describeCreateOrChange": "Beschreibe, was du erstellen oder ändern möchtest.", + "stopGenerating": "Generierung stoppen", + "sendMessage": "Nachricht senden", + "baseURLPlaceholder": "Base URL (z. B. http://localhost:11434/v1)", + "modelIDPlaceholder": "Model ID (z. B. llama-3.3-70b)", + "yourName": "Dein Name", + "enterYourName": "Deinen Namen eingeben", + "shareThisFile": "Diese Datei teilen", + "joinRoom": "Raum beitreten", + "join": "Beitreten", + "roomLink": "Raumlink", + "joinCollaboration": "Zur Zusammenarbeit beitreten", + "orJoinRoom": "oder einem Raum beitreten", + "pasteRoomLinkOrId": "Raumlink oder ID einfügen", + "connected": "Verbunden", + "search": "Suchen…", + "noResults": "Keine Ergebnisse", + "share": "Teilen" + } +} diff --git a/packages/vue/src/locales/es.json b/packages/vue/src/locales/es.json new file mode 100644 index 000000000..2de58b9ad --- /dev/null +++ b/packages/vue/src/locales/es.json @@ -0,0 +1,177 @@ +{ + "menu": { + "file": "Archivo", + "edit": "Editar", + "view": "Ver", + "object": "Objeto", + "arrange": "Organizar", + "text": "Texto", + "new": "Nuevo", + "open": "Abrir…", + "save": "Guardar", + "saveAs": "Guardar como…", + "exportSelection": "Exportar selección…", + "autosave": "Guardado automático en archivo", + "copy": "Copiar", + "paste": "Pegar", + "zoomIn": "Ampliar", + "zoomOut": "Reducir", + "profiler": "Perfil de rendimiento", + "language": "Idioma", + "moveToPage": "Mover a página", + "createInstance": "Crear instancia", + "hide": "Ocultar", + "show": "Mostrar", + "lock": "Bloquear", + "unlock": "Desbloquear", + "cut": "Cortar", + "front": "Al frente", + "back": "Al fondo", + "toggleUI": "Alternar interfaz", + "bold": "Negrita", + "italic": "Cursiva", + "underline": "Subrayado" + }, + "commands": { + "undo": "Deshacer", + "redo": "Rehacer", + "selectAll": "Seleccionar todo", + "duplicate": "Duplicar", + "delete": "Eliminar", + "group": "Agrupar", + "ungroup": "Desagrupar", + "createComponent": "Crear componente", + "createComponentSet": "Crear conjunto de componentes", + "createInstance": "Crear instancia", + "detachInstance": "Separar instancia", + "goToMainComponent": "Ir al componente principal", + "addAutoLayout": "Añadir auto-layout", + "bringToFront": "Traer al frente", + "sendToBack": "Enviar al fondo", + "toggleVisibility": "Visibilidad", + "toggleLock": "Bloqueo", + "moveToPage": "Mover a página", + "zoomTo100": "Zoom 100 %", + "zoomToFit": "Ajustar a pantalla", + "zoomToSelection": "Zoom a selección" + }, + "tools": { + "move": "Mover", + "frame": "Marco", + "section": "Sección", + "rectangle": "Rectángulo", + "ellipse": "Elipse", + "line": "Línea", + "polygon": "Polígono", + "star": "Estrella", + "pen": "Pluma", + "text": "Texto", + "hand": "Mano" + }, + "panels": { + "layers": "Capas", + "pages": "Páginas", + "design": "Diseño", + "code": "Código", + "ai": "IA", + "assets": "Recursos", + "page": "Página", + "position": "Posición", + "layout": "Diseño", + "autoLayout": "Auto-layout", + "alignment": "Alineación", + "appearance": "Apariencia", + "fill": "Relleno", + "stroke": "Trazo", + "effects": "Efectos", + "export": "Exportar", + "typography": "Tipografía", + "variables": "Variables", + "constraints": "Restricciones", + "addFill": "Añadir relleno", + "addStroke": "Añadir trazo", + "addEffect": "Añadir efecto", + "addExport": "Añadir exportación", + "dropShadow": "Sombra", + "innerShadow": "Sombra interior", + "layerBlur": "Desenfoque de capa", + "backgroundBlur": "Desenfoque de fondo", + "noSelection": "Sin selección", + "noLocalVariables": "Sin variables locales", + "openVariables": "Abrir variables", + "addPage": "Añadir página", + "toggleVisibility": "Alternar visibilidad", + "independentCornerRadii": "Radios de esquina independientes", + "detachVariable": "Desvincular variable", + "applyVariable": "Aplicar variable", + "noVariablesFound": "No se encontraron variables", + "addAutoLayout": "Añadir auto-layout (Shift+A)", + "removeAutoLayout": "Quitar auto-layout", + "mixed": "Mixto", + "layersCount": "{count} capas", + "goToMainComponent": "Ir al componente principal", + "detachInstance": "Separar instancia", + "solid": "Sólido", + "linearGradient": "Lineal", + "radialGradient": "Radial", + "image": "Imagen", + "stops": "Paradas", + "addStop": "Añadir parada", + "alignLeft": "Alinear a la izquierda", + "alignCenter": "Centrar", + "alignRight": "Alinear a la derecha", + "alignTop": "Alinear arriba", + "alignMiddle": "Centrar verticalmente", + "alignBottom": "Alinear abajo", + "alignCenterHorizontally": "Centrar horizontalmente", + "alignCenterVertically": "Centrar verticalmente", + "flipHorizontal": "Voltear horizontalmente", + "flipVertical": "Voltear verticalmente", + "rotate90": "Girar 90°", + "mixedFillsHelp": "Haz clic en + para reemplazar rellenos mixtos", + "mixedStrokesHelp": "Haz clic en + para reemplazar trazos mixtos", + "mixedEffectsHelp": "Haz clic en + para reemplazar efectos mixtos", + "strokeSides": "Lados del trazo" + }, + "pages": { + "newPage": "Nueva página", + "rename": "Renombrar", + "delete": "Eliminar", + "pageName": "Página {number}" + }, + "dialogs": { + "cancel": "Cancelar", + "apply": "Aplicar", + "close": "Cerrar", + "ok": "Aceptar", + "copy": "Copiar", + "copied": "Copiado", + "createCollection": "Crear colección", + "localVariables": "Variables locales", + "noVariableCollections": "No hay colecciones de variables", + "selectLayerForJSX": "Selecciona una capa para ver su código JSX", + "connectAIProvider": "Conecta un proveedor de IA para empezar a chatear.", + "connect": "Conectar", + "getAPIKey": "Obtener una clave API de {provider} →", + "oneKeyManyModels": "Una clave para más de 100 modelos de todos los proveedores.", + "describeChange": "Describe un cambio…", + "describeCreateOrChange": "Describe lo que quieres crear o cambiar.", + "stopGenerating": "Detener generación", + "sendMessage": "Enviar mensaje", + "baseURLPlaceholder": "Base URL (p. ej. http://localhost:11434/v1)", + "modelIDPlaceholder": "Model ID (p. ej. llama-3.3-70b)", + "yourName": "Tu nombre", + "enterYourName": "Introduce tu nombre", + "shareThisFile": "Compartir este archivo", + "joinRoom": "Unirse a la sala", + "join": "Unirse", + "roomLink": "Enlace de la sala", + "joinCollaboration": "Unirse a la colaboración", + "orJoinRoom": "o unirse a una sala", + "pasteRoomLinkOrId": "Pega el enlace o ID de la sala", + "connected": "Conectado", + "search": "Buscar…", + "noResults": "Sin resultados", + "share": "Compartir" + } +} diff --git a/packages/vue/src/locales/fr.json b/packages/vue/src/locales/fr.json new file mode 100644 index 000000000..78a37165f --- /dev/null +++ b/packages/vue/src/locales/fr.json @@ -0,0 +1,177 @@ +{ + "menu": { + "file": "Fichier", + "edit": "Édition", + "view": "Affichage", + "object": "Objet", + "arrange": "Disposition", + "text": "Texte", + "new": "Nouveau", + "open": "Ouvrir…", + "save": "Enregistrer", + "saveAs": "Enregistrer sous…", + "exportSelection": "Exporter la sélection…", + "autosave": "Enregistrement automatique", + "copy": "Copier", + "paste": "Coller", + "zoomIn": "Zoom avant", + "zoomOut": "Zoom arrière", + "profiler": "Profileur de performance", + "language": "Langue", + "moveToPage": "Déplacer vers la page", + "createInstance": "Créer une instance", + "hide": "Masquer", + "show": "Afficher", + "lock": "Verrouiller", + "unlock": "Déverrouiller", + "cut": "Couper", + "front": "Premier plan", + "back": "Arrière-plan", + "toggleUI": "Basculer l’interface", + "bold": "Gras", + "italic": "Italique", + "underline": "Souligné" + }, + "commands": { + "undo": "Annuler", + "redo": "Rétablir", + "selectAll": "Tout sélectionner", + "duplicate": "Dupliquer", + "delete": "Supprimer", + "group": "Grouper", + "ungroup": "Dégrouper", + "createComponent": "Créer un composant", + "createComponentSet": "Créer un jeu de composants", + "createInstance": "Créer une instance", + "detachInstance": "Détacher l'instance", + "goToMainComponent": "Aller au composant principal", + "addAutoLayout": "Ajouter un auto-layout", + "bringToFront": "Mettre au premier plan", + "sendToBack": "Mettre en arrière-plan", + "toggleVisibility": "Visibilité", + "toggleLock": "Verrouillage", + "moveToPage": "Déplacer vers la page", + "zoomTo100": "Zoom 100 %", + "zoomToFit": "Ajuster à l'écran", + "zoomToSelection": "Zoom sur la sélection" + }, + "tools": { + "move": "Déplacer", + "frame": "Cadre", + "section": "Section", + "rectangle": "Rectangle", + "ellipse": "Ellipse", + "line": "Ligne", + "polygon": "Polygone", + "star": "Étoile", + "pen": "Plume", + "text": "Texte", + "hand": "Main" + }, + "panels": { + "layers": "Calques", + "pages": "Pages", + "design": "Design", + "code": "Code", + "ai": "IA", + "assets": "Ressources", + "page": "Page", + "position": "Position", + "layout": "Disposition", + "autoLayout": "Auto-layout", + "alignment": "Alignement", + "appearance": "Apparence", + "fill": "Remplissage", + "stroke": "Contour", + "effects": "Effets", + "export": "Export", + "typography": "Typographie", + "variables": "Variables", + "constraints": "Contraintes", + "addFill": "Ajouter un remplissage", + "addStroke": "Ajouter un contour", + "addEffect": "Ajouter un effet", + "addExport": "Ajouter un export", + "dropShadow": "Ombre portée", + "innerShadow": "Ombre interne", + "layerBlur": "Flou de calque", + "backgroundBlur": "Flou d'arrière-plan", + "noSelection": "Aucune sélection", + "noLocalVariables": "Aucune variable locale", + "openVariables": "Ouvrir les variables", + "addPage": "Ajouter une page", + "toggleVisibility": "Basculer la visibilité", + "independentCornerRadii": "Rayons de coin indépendants", + "detachVariable": "Détacher la variable", + "applyVariable": "Appliquer la variable", + "noVariablesFound": "Aucune variable trouvée", + "addAutoLayout": "Ajouter un auto-layout (Shift+A)", + "removeAutoLayout": "Retirer l'auto-layout", + "mixed": "Mixte", + "layersCount": "{count} calques", + "goToMainComponent": "Aller au composant principal", + "detachInstance": "Détacher l'instance", + "solid": "Uni", + "linearGradient": "Linéaire", + "radialGradient": "Radial", + "image": "Image", + "stops": "Arrêts", + "addStop": "Ajouter un arrêt", + "alignLeft": "Aligner à gauche", + "alignCenter": "Centrer", + "alignRight": "Aligner à droite", + "alignTop": "Aligner en haut", + "alignMiddle": "Centrer verticalement", + "alignBottom": "Aligner en bas", + "alignCenterHorizontally": "Centrer horizontalement", + "alignCenterVertically": "Centrer verticalement", + "flipHorizontal": "Retourner horizontalement", + "flipVertical": "Retourner verticalement", + "rotate90": "Pivoter de 90°", + "mixedFillsHelp": "Cliquez sur + pour remplacer les remplissages mixtes", + "mixedStrokesHelp": "Cliquez sur + pour remplacer les contours mixtes", + "mixedEffectsHelp": "Cliquez sur + pour remplacer les effets mixtes", + "strokeSides": "Côtés du contour" + }, + "pages": { + "newPage": "Nouvelle page", + "rename": "Renommer", + "delete": "Supprimer", + "pageName": "Page {number}" + }, + "dialogs": { + "cancel": "Annuler", + "apply": "Appliquer", + "close": "Fermer", + "ok": "OK", + "copy": "Copier", + "copied": "Copié", + "createCollection": "Créer une collection", + "localVariables": "Variables locales", + "noVariableCollections": "Aucune collection de variables", + "selectLayerForJSX": "Sélectionnez un calque pour voir son code JSX", + "connectAIProvider": "Connectez un fournisseur d’IA pour commencer à discuter.", + "connect": "Connecter", + "getAPIKey": "Obtenir une clé API {provider} →", + "oneKeyManyModels": "Une clé pour plus de 100 modèles de tous les fournisseurs.", + "describeChange": "Décrivez un changement…", + "describeCreateOrChange": "Décrivez ce que vous voulez créer ou modifier.", + "stopGenerating": "Arrêter la génération", + "sendMessage": "Envoyer le message", + "baseURLPlaceholder": "Base URL (ex. http://localhost:11434/v1)", + "modelIDPlaceholder": "Model ID (ex. llama-3.3-70b)", + "yourName": "Votre nom", + "enterYourName": "Entrez votre nom", + "shareThisFile": "Partager ce fichier", + "joinRoom": "Rejoindre la salle", + "join": "Rejoindre", + "roomLink": "Lien de la salle", + "joinCollaboration": "Rejoindre la collaboration", + "orJoinRoom": "ou rejoindre une salle", + "pasteRoomLinkOrId": "Collez le lien ou l’ID de la salle", + "connected": "Connecté", + "search": "Rechercher…", + "noResults": "Aucun résultat", + "share": "Partager" + } +} diff --git a/packages/vue/src/locales/it.json b/packages/vue/src/locales/it.json new file mode 100644 index 000000000..e00f86ff4 --- /dev/null +++ b/packages/vue/src/locales/it.json @@ -0,0 +1,177 @@ +{ + "menu": { + "file": "File", + "edit": "Modifica", + "view": "Visualizza", + "object": "Oggetto", + "arrange": "Disponi", + "text": "Testo", + "new": "Nuovo", + "open": "Apri…", + "save": "Salva", + "saveAs": "Salva con nome…", + "exportSelection": "Esporta selezione…", + "autosave": "Salvataggio automatico su file", + "copy": "Copia", + "paste": "Incolla", + "zoomIn": "Ingrandisci", + "zoomOut": "Riduci", + "profiler": "Profilo prestazioni", + "language": "Lingua", + "moveToPage": "Sposta nella pagina", + "createInstance": "Crea istanza", + "hide": "Nascondi", + "show": "Mostra", + "lock": "Blocca", + "unlock": "Sblocca", + "cut": "Taglia", + "front": "Davanti", + "back": "Dietro", + "toggleUI": "Mostra/nascondi interfaccia", + "bold": "Grassetto", + "italic": "Corsivo", + "underline": "Sottolineato" + }, + "commands": { + "undo": "Annulla", + "redo": "Ripeti", + "selectAll": "Seleziona tutto", + "duplicate": "Duplica", + "delete": "Elimina", + "group": "Raggruppa", + "ungroup": "Separa", + "createComponent": "Crea componente", + "createComponentSet": "Crea set di componenti", + "createInstance": "Crea istanza", + "detachInstance": "Scollega istanza", + "goToMainComponent": "Vai al componente principale", + "addAutoLayout": "Aggiungi auto-layout", + "bringToFront": "Porta in primo piano", + "sendToBack": "Porta in secondo piano", + "toggleVisibility": "Visibilità", + "toggleLock": "Blocco", + "moveToPage": "Sposta nella pagina", + "zoomTo100": "Zoom 100%", + "zoomToFit": "Adatta allo schermo", + "zoomToSelection": "Zoom sulla selezione" + }, + "tools": { + "move": "Sposta", + "frame": "Cornice", + "section": "Sezione", + "rectangle": "Rettangolo", + "ellipse": "Ellisse", + "line": "Linea", + "polygon": "Poligono", + "star": "Stella", + "pen": "Penna", + "text": "Testo", + "hand": "Mano" + }, + "panels": { + "layers": "Livelli", + "pages": "Pagine", + "design": "Design", + "code": "Codice", + "ai": "AI", + "assets": "Risorse", + "page": "Pagina", + "position": "Posizione", + "layout": "Layout", + "autoLayout": "Auto-layout", + "alignment": "Allineamento", + "appearance": "Aspetto", + "fill": "Riempimento", + "stroke": "Contorno", + "effects": "Effetti", + "export": "Esporta", + "typography": "Tipografia", + "variables": "Variabili", + "constraints": "Vincoli", + "addFill": "Aggiungi riempimento", + "addStroke": "Aggiungi contorno", + "addEffect": "Aggiungi effetto", + "addExport": "Aggiungi esportazione", + "dropShadow": "Ombra esterna", + "innerShadow": "Ombra interna", + "layerBlur": "Sfocatura livello", + "backgroundBlur": "Sfocatura sfondo", + "noSelection": "Nessuna selezione", + "noLocalVariables": "Nessuna variabile locale", + "openVariables": "Apri variabili", + "addPage": "Aggiungi pagina", + "toggleVisibility": "Attiva/disattiva visibilità", + "independentCornerRadii": "Raggi angolo indipendenti", + "detachVariable": "Scollega variabile", + "applyVariable": "Applica variabile", + "noVariablesFound": "Nessuna variabile trovata", + "addAutoLayout": "Aggiungi auto-layout (Shift+A)", + "removeAutoLayout": "Rimuovi auto-layout", + "mixed": "Misto", + "layersCount": "{count} livelli", + "goToMainComponent": "Vai al componente principale", + "detachInstance": "Scollega istanza", + "solid": "Pieno", + "linearGradient": "Lineare", + "radialGradient": "Radiale", + "image": "Immagine", + "stops": "Punti", + "addStop": "Aggiungi punto", + "alignLeft": "Allinea a sinistra", + "alignCenter": "Centra", + "alignRight": "Allinea a destra", + "alignTop": "Allinea in alto", + "alignMiddle": "Centra verticalmente", + "alignBottom": "Allinea in basso", + "alignCenterHorizontally": "Centra orizzontalmente", + "alignCenterVertically": "Centra verticalmente", + "flipHorizontal": "Rifletti orizzontalmente", + "flipVertical": "Rifletti verticalmente", + "rotate90": "Ruota di 90°", + "mixedFillsHelp": "Fai clic su + per sostituire i riempimenti misti", + "mixedStrokesHelp": "Fai clic su + per sostituire i contorni misti", + "mixedEffectsHelp": "Fai clic su + per sostituire gli effetti misti", + "strokeSides": "Lati del contorno" + }, + "pages": { + "newPage": "Nuova pagina", + "rename": "Rinomina", + "delete": "Elimina", + "pageName": "Pagina {number}" + }, + "dialogs": { + "cancel": "Annulla", + "apply": "Applica", + "close": "Chiudi", + "ok": "OK", + "copy": "Copia", + "copied": "Copiato", + "createCollection": "Crea raccolta", + "localVariables": "Variabili locali", + "noVariableCollections": "Nessuna raccolta di variabili", + "selectLayerForJSX": "Seleziona un livello per vedere il suo codice JSX", + "connectAIProvider": "Collega un provider AI per iniziare a chattare.", + "connect": "Connetti", + "getAPIKey": "Ottieni una chiave API {provider} →", + "oneKeyManyModels": "Una chiave per oltre 100 modelli di tutti i provider.", + "describeChange": "Descrivi una modifica…", + "describeCreateOrChange": "Descrivi cosa vuoi creare o cambiare.", + "stopGenerating": "Interrompi generazione", + "sendMessage": "Invia messaggio", + "baseURLPlaceholder": "Base URL (es. http://localhost:11434/v1)", + "modelIDPlaceholder": "Model ID (es. llama-3.3-70b)", + "yourName": "Il tuo nome", + "enterYourName": "Inserisci il tuo nome", + "shareThisFile": "Condividi questo file", + "joinRoom": "Entra nella stanza", + "join": "Entra", + "roomLink": "Link della stanza", + "joinCollaboration": "Partecipa alla collaborazione", + "orJoinRoom": "o entra in una stanza", + "pasteRoomLinkOrId": "Incolla il link o l'ID della stanza", + "connected": "Connesso", + "search": "Cerca…", + "noResults": "Nessun risultato", + "share": "Condividi" + } +} diff --git a/packages/vue/src/locales/pl.json b/packages/vue/src/locales/pl.json new file mode 100644 index 000000000..ec9bebf7d --- /dev/null +++ b/packages/vue/src/locales/pl.json @@ -0,0 +1,177 @@ +{ + "menu": { + "file": "Plik", + "edit": "Edycja", + "view": "Widok", + "object": "Obiekt", + "arrange": "Rozmieszczenie", + "text": "Tekst", + "new": "Nowy", + "open": "Otwórz…", + "save": "Zapisz", + "saveAs": "Zapisz jako…", + "exportSelection": "Eksportuj zaznaczenie…", + "autosave": "Automatyczny zapis do pliku", + "copy": "Kopiuj", + "paste": "Wklej", + "zoomIn": "Powiększ", + "zoomOut": "Pomniejsz", + "profiler": "Profiler wydajności", + "language": "Język", + "moveToPage": "Przenieś na stronę", + "createInstance": "Utwórz instancję", + "hide": "Ukryj", + "show": "Pokaż", + "lock": "Zablokuj", + "unlock": "Odblokuj", + "cut": "Wytnij", + "front": "Do przodu", + "back": "Do tyłu", + "toggleUI": "Przełącz interfejs", + "bold": "Pogrubienie", + "italic": "Kursywa", + "underline": "Podkreślenie" + }, + "commands": { + "undo": "Cofnij", + "redo": "Ponów", + "selectAll": "Zaznacz wszystko", + "duplicate": "Duplikuj", + "delete": "Usuń", + "group": "Grupuj", + "ungroup": "Rozgrupuj", + "createComponent": "Utwórz komponent", + "createComponentSet": "Utwórz zestaw komponentów", + "createInstance": "Utwórz instancję", + "detachInstance": "Odłącz instancję", + "goToMainComponent": "Przejdź do komponentu", + "addAutoLayout": "Dodaj auto-layout", + "bringToFront": "Przenieś na wierzch", + "sendToBack": "Przenieś na spód", + "toggleVisibility": "Widoczność", + "toggleLock": "Blokada", + "moveToPage": "Przenieś na stronę", + "zoomTo100": "Zoom 100%", + "zoomToFit": "Dopasuj do ekranu", + "zoomToSelection": "Zoom do zaznaczenia" + }, + "tools": { + "move": "Przesuń", + "frame": "Ramka", + "section": "Sekcja", + "rectangle": "Prostokąt", + "ellipse": "Elipsa", + "line": "Linia", + "polygon": "Wielokąt", + "star": "Gwiazda", + "pen": "Pióro", + "text": "Tekst", + "hand": "Dłoń" + }, + "panels": { + "layers": "Warstwy", + "pages": "Strony", + "design": "Design", + "code": "Kod", + "ai": "AI", + "assets": "Zasoby", + "page": "Strona", + "position": "Pozycja", + "layout": "Układ", + "autoLayout": "Auto-layout", + "alignment": "Wyrównanie", + "appearance": "Wygląd", + "fill": "Wypełnienie", + "stroke": "Obrys", + "effects": "Efekty", + "export": "Eksport", + "typography": "Typografia", + "variables": "Zmienne", + "constraints": "Ograniczenia", + "addFill": "Dodaj wypełnienie", + "addStroke": "Dodaj obrys", + "addEffect": "Dodaj efekt", + "addExport": "Dodaj eksport", + "dropShadow": "Cień", + "innerShadow": "Cień wewnętrzny", + "layerBlur": "Rozmycie warstwy", + "backgroundBlur": "Rozmycie tła", + "noSelection": "Brak zaznaczenia", + "noLocalVariables": "Brak lokalnych zmiennych", + "openVariables": "Otwórz zmienne", + "addPage": "Dodaj stronę", + "toggleVisibility": "Przełącz widoczność", + "independentCornerRadii": "Niezależne promienie rogów", + "detachVariable": "Odłącz zmienną", + "applyVariable": "Zastosuj zmienną", + "noVariablesFound": "Nie znaleziono zmiennych", + "addAutoLayout": "Dodaj auto-layout (Shift+A)", + "removeAutoLayout": "Usuń auto-layout", + "mixed": "Mieszane", + "layersCount": "{count} warstw", + "goToMainComponent": "Przejdź do głównego komponentu", + "detachInstance": "Odłącz instancję", + "solid": "Jednolity", + "linearGradient": "Liniowy", + "radialGradient": "Promieniowy", + "image": "Obraz", + "stops": "Punkty", + "addStop": "Dodaj punkt", + "alignLeft": "Wyrównaj do lewej", + "alignCenter": "Wyśrodkuj", + "alignRight": "Wyrównaj do prawej", + "alignTop": "Wyrównaj do góry", + "alignMiddle": "Wyśrodkuj w pionie", + "alignBottom": "Wyrównaj do dołu", + "alignCenterHorizontally": "Wyśrodkuj poziomo", + "alignCenterVertically": "Wyśrodkuj pionowo", + "flipHorizontal": "Odbij poziomo", + "flipVertical": "Odbij pionowo", + "rotate90": "Obróć o 90°", + "mixedFillsHelp": "Kliknij +, aby zastąpić mieszane wypełnienia", + "mixedStrokesHelp": "Kliknij +, aby zastąpić mieszane obrysy", + "mixedEffectsHelp": "Kliknij +, aby zastąpić mieszane efekty", + "strokeSides": "Boki obrysu" + }, + "pages": { + "newPage": "Nowa strona", + "rename": "Zmień nazwę", + "delete": "Usuń", + "pageName": "Strona {number}" + }, + "dialogs": { + "cancel": "Anuluj", + "apply": "Zastosuj", + "close": "Zamknij", + "ok": "OK", + "copy": "Kopiuj", + "copied": "Skopiowano", + "createCollection": "Utwórz kolekcję", + "localVariables": "Zmienne lokalne", + "noVariableCollections": "Brak kolekcji zmiennych", + "selectLayerForJSX": "Wybierz warstwę, aby zobaczyć jej kod JSX", + "connectAIProvider": "Połącz dostawcę AI, aby rozpocząć czat.", + "connect": "Połącz", + "getAPIKey": "Pobierz klucz API {provider} →", + "oneKeyManyModels": "Jeden klucz do ponad 100 modeli od wszystkich dostawców.", + "describeChange": "Opisz zmianę…", + "describeCreateOrChange": "Opisz, co chcesz utworzyć lub zmienić.", + "stopGenerating": "Zatrzymaj generowanie", + "sendMessage": "Wyślij wiadomość", + "baseURLPlaceholder": "Base URL (np. http://localhost:11434/v1)", + "modelIDPlaceholder": "Model ID (np. llama-3.3-70b)", + "yourName": "Twoje imię", + "enterYourName": "Wpisz swoje imię", + "shareThisFile": "Udostępnij ten plik", + "joinRoom": "Dołącz do pokoju", + "join": "Dołącz", + "roomLink": "Link do pokoju", + "joinCollaboration": "Dołącz do współpracy", + "orJoinRoom": "lub dołącz do pokoju", + "pasteRoomLinkOrId": "Wklej link lub ID pokoju", + "connected": "Połączono", + "search": "Szukaj…", + "noResults": "Brak wyników", + "share": "Udostępnij" + } +} diff --git a/packages/vue/src/locales/ru.json b/packages/vue/src/locales/ru.json new file mode 100644 index 000000000..90dcb3b69 --- /dev/null +++ b/packages/vue/src/locales/ru.json @@ -0,0 +1,181 @@ +{ + "menu": { + "file": "Файл", + "edit": "Правка", + "view": "Вид", + "object": "Объект", + "arrange": "Порядок", + "text": "Текст", + "new": "Создать", + "open": "Открыть…", + "save": "Сохранить", + "saveAs": "Сохранить как…", + "exportSelection": "Экспорт выделения…", + "autosave": "Автосохранение в файл", + "copy": "Копировать", + "paste": "Вставить", + "zoomIn": "Увеличить", + "zoomOut": "Уменьшить", + "profiler": "Профилирование", + "language": "Язык", + "moveToPage": "Переместить на страницу", + "createInstance": "Создать экземпляр", + "hide": "Скрыть", + "show": "Показать", + "lock": "Заблокировать", + "unlock": "Разблокировать", + "cut": "Вырезать", + "front": "Вперёд", + "back": "Назад", + "toggleUI": "Переключить интерфейс", + "bold": "Жирный", + "italic": "Курсив", + "underline": "Подчёркнутый" + }, + "commands": { + "undo": "Отменить", + "redo": "Повторить", + "selectAll": "Выделить всё", + "duplicate": "Дублировать", + "delete": "Удалить", + "group": "Сгруппировать", + "ungroup": "Разгруппировать", + "createComponent": "Создать компонент", + "createComponentSet": "Создать набор компонентов", + "createInstance": "Создать экземпляр", + "detachInstance": "Отвязать экземпляр", + "goToMainComponent": "Перейти к компоненту", + "addAutoLayout": "Добавить авто-раскладку", + "bringToFront": "На передний план", + "sendToBack": "На задний план", + "toggleVisibility": "Видимость", + "toggleLock": "Блокировка", + "moveToPage": "Переместить на страницу", + "zoomTo100": "Масштаб 100%", + "zoomToFit": "Вписать в экран", + "zoomToSelection": "Масштаб по выделению" + }, + "tools": { + "move": "Перемещение", + "frame": "Фрейм", + "section": "Секция", + "rectangle": "Прямоугольник", + "ellipse": "Эллипс", + "line": "Линия", + "polygon": "Многоугольник", + "star": "Звезда", + "pen": "Перо", + "text": "Текст", + "hand": "Рука" + }, + "panels": { + "layers": "Слои", + "pages": "Страницы", + "design": "Дизайн", + "code": "Код", + "ai": "AI", + "assets": "Ассеты", + "page": "Страница", + "position": "Позиция", + "layout": "Раскладка", + "autoLayout": "Автораскладка", + "alignment": "Выравнивание", + "appearance": "Внешний вид", + "fill": "Заливка", + "stroke": "Обводка", + "effects": "Эффекты", + "export": "Экспорт", + "typography": "Типографика", + "variables": "Переменные", + "constraints": "Ограничения", + "addFill": "Добавить заливку", + "addStroke": "Добавить обводку", + "addEffect": "Добавить эффект", + "addExport": "Добавить экспорт", + "dropShadow": "Тень", + "innerShadow": "Внутренняя тень", + "layerBlur": "Размытие слоя", + "backgroundBlur": "Размытие фона", + "noSelection": "Ничего не выделено", + "noLocalVariables": "Нет локальных переменных", + "openVariables": "Открыть переменные", + "addPage": "Добавить страницу", + "toggleVisibility": "Переключить видимость", + "independentCornerRadii": "Независимые радиусы углов", + "detachVariable": "Отвязать переменную", + "applyVariable": "Применить переменную", + "noVariablesFound": "Переменные не найдены", + "addAutoLayout": "Добавить автораскладку (Shift+A)", + "removeAutoLayout": "Убрать автораскладку", + "alignLeft": "Выровнять по левому краю", + "alignCenterHorizontally": "Выровнять по центру по горизонтали", + "alignRight": "Выровнять по правому краю", + "alignTop": "Выровнять по верхнему краю", + "alignCenterVertically": "Выровнять по центру по вертикали", + "alignBottom": "Выровнять по нижнему краю", + "flipHorizontal": "Отразить по горизонтали", + "flipVertical": "Отразить по вертикали", + "rotate90": "Повернуть на 90°", + "mixedFillsHelp": "Нажмите +, чтобы заменить смешанные заливки", + "mixedStrokesHelp": "Нажмите +, чтобы заменить смешанные обводки", + "mixedEffectsHelp": "Нажмите +, чтобы заменить смешанные эффекты", + "strokeSides": "Стороны обводки", + "mixed": "Смешанное", + "layersCount": "{count} слоёв", + "goToMainComponent": "Перейти к главному компоненту", + "detachInstance": "Отвязать экземпляр", + "solid": "Сплошной", + "linearGradient": "Линейный", + "radialGradient": "Радиальный", + "image": "Изображение", + "stops": "Точки", + "addStop": "Добавить точку", + "alignLeft": "По левому краю", + "alignCenter": "По центру", + "alignRight": "По правому краю", + "alignTop": "По верхнему краю", + "alignMiddle": "По середине", + "alignBottom": "По нижнему краю" + }, + "pages": { + "newPage": "Новая страница", + "rename": "Переименовать", + "delete": "Удалить", + "pageName": "Страница {number}" + }, + "dialogs": { + "cancel": "Отмена", + "apply": "Применить", + "close": "Закрыть", + "ok": "ОК", + "copy": "Копировать", + "copied": "Скопировано", + "createCollection": "Создать коллекцию", + "localVariables": "Локальные переменные", + "noVariableCollections": "Нет коллекций переменных", + "selectLayerForJSX": "Выберите слой, чтобы увидеть его JSX-код", + "connectAIProvider": "Подключите AI-провайдера, чтобы начать чат.", + "connect": "Подключить", + "getAPIKey": "Получить API-ключ {provider} →", + "oneKeyManyModels": "Один ключ для 100+ моделей от разных провайдеров.", + "describeChange": "Опишите изменение…", + "describeCreateOrChange": "Опишите, что вы хотите создать или изменить.", + "stopGenerating": "Остановить генерацию", + "sendMessage": "Отправить сообщение", + "baseURLPlaceholder": "Base URL (например, http://localhost:11434/v1)", + "modelIDPlaceholder": "Model ID (например, llama-3.3-70b)", + "yourName": "Ваше имя", + "enterYourName": "Введите ваше имя", + "shareThisFile": "Поделиться файлом", + "joinRoom": "Войти в комнату", + "join": "Войти", + "roomLink": "Ссылка на комнату", + "joinCollaboration": "Присоединиться к совместной работе", + "orJoinRoom": "или войти в комнату", + "pasteRoomLinkOrId": "Вставьте ссылку на комнату или ID", + "connected": "Подключено", + "search": "Поиск…", + "noResults": "Ничего не найдено", + "share": "Поделиться" + } +} diff --git a/packages/vue/src/shared/useInlineRename.ts b/packages/vue/src/shared/useInlineRename.ts index be8dc13b8..703954598 100644 --- a/packages/vue/src/shared/useInlineRename.ts +++ b/packages/vue/src/shared/useInlineRename.ts @@ -1,8 +1,19 @@ import { onClickOutside } from '@vueuse/core' import { nextTick, ref, type Ref } from 'vue' -export function useInlineRename(onCommit: (id: T, newName: string) => void) { - const editingId = ref(null) +export interface InlineRenameState { + editingId: Ref + start: (id: T, currentName: string) => void + focusInput: (input: HTMLInputElement | null) => Promise + commit: (id: T, input: HTMLInputElement) => void + cancel: () => void + onKeydown: (e: KeyboardEvent) => void +} + +export function useInlineRename( + onCommit: (id: T, newName: string) => void +): InlineRenameState { + const editingId: Ref = ref(null) const inputRef: Ref = ref(null) let originalName = '' let cleanupOutsideClick: (() => void) | undefined diff --git a/packages/vue/tsconfig.json b/packages/vue/tsconfig.json index 7e8f5cfe4..7a083d056 100644 --- a/packages/vue/tsconfig.json +++ b/packages/vue/tsconfig.json @@ -1,6 +1,7 @@ { "compilerOptions": { - "target": "ESNext", + "target": "ES2022", + "lib": ["ESNext", "DOM", "DOM.Iterable"], "module": "ESNext", "moduleResolution": "bundler", "strict": true, @@ -8,10 +9,18 @@ "declaration": true, "declarationDir": "./dist", "outDir": "./dist", - "rootDir": "./src", + "rootDir": "..", "skipLibCheck": true, - "esModuleInterop": true + "esModuleInterop": true, + "resolveJsonModule": true, + "baseUrl": ".", + "paths": { + "@open-pencil/vue": ["./src/index.ts"], + "@open-pencil/vue/*": ["./src/*"], + "@open-pencil/core": ["../core/src/index.ts"], + "@open-pencil/core/*": ["../core/src/*"] + } }, - "include": ["src/**/*.ts", "src/**/*.vue"], + "include": ["src/**/*.ts", "src/**/*.vue", "src/**/*.d.ts", "../core/src/global.d.ts"], "exclude": ["node_modules", "dist", "example"] } diff --git a/src/components/AppMenu.vue b/src/components/AppMenu.vue index 8ab8fa489..2f6e5d621 100644 --- a/src/components/AppMenu.vue +++ b/src/components/AppMenu.vue @@ -18,7 +18,7 @@ import IconChevronRight from '~icons/lucide/chevron-right' import { computed } from 'vue' -import { useInlineRename } from '@open-pencil/vue' +import { useI18n, useInlineRename } from '@open-pencil/vue' import { useMenuUI } from '@/components/ui/menu' import { IS_TAURI } from '@/constants' import { useAppMenu } from '@/composables/use-app-menu' @@ -46,6 +46,7 @@ function commitRename(input: HTMLInputElement) { const isMac = navigator.platform.includes('Mac') const mod = isMac ? '⌘' : 'Ctrl+' +const { menu: t } = useI18n() interface MenuAction { separator?: false @@ -91,7 +92,7 @@ const subMenuCls = useMenuUI({ content: 'min-w-44' }) @dblclick="startRename" >{{ store.state.documentName }} - + diff --git a/src/components/CollabPanel.vue b/src/components/CollabPanel.vue index 0c133e965..c51aa772f 100644 --- a/src/components/CollabPanel.vue +++ b/src/components/CollabPanel.vue @@ -10,12 +10,14 @@ import { usePopoverUI } from '@/components/ui/popover' import { DEFAULT_COLLAB_STATE, useCollabInjected } from '@/composables/use-collab' import { toast } from '@/utils/toast' import { initials } from '@/utils/text' +import { useI18n } from '@open-pencil/vue' const route = useRoute() const router = useRouter() const cls = usePopoverUI({ content: 'z-50 w-72 p-3' }) const collab = useCollabInjected() const { copy, copied } = useClipboard({ copiedDuring: 2000 }) +const { dialogs } = useI18n() const joinInput = ref('') const nameDraft = ref(collab?.state.value.localName ?? '') @@ -120,7 +122,7 @@ function onDisconnect() { " > - {{ state.connected ? 'Connected' : isJoining ? 'Join room' : 'Share' }} + {{ state.connected ? dialogs.connected : isJoining ? dialogs.joinRoom : dialogs.share }} @@ -134,7 +136,7 @@ function onDisconnect() { >