Finish i18n pass for menus and tool labels
Localize remaining menu state labels like Hide/Show and Lock/Unlock, wire toolbar tool/action labels through the i18n stores, and localize the app menu UI toggle tooltip. Replace template-string JSON imports with an explicit locale loader map so published package builds have stable locale asset references.
This commit is contained in:
parent
eb2891233a
commit
ea4676d991
|
|
@ -124,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 {
|
||||
|
|
|
|||
|
|
@ -5,10 +5,20 @@ import { locale } from './locale'
|
|||
import type { Locale } from './locale'
|
||||
import type { ComponentsJSON } from '@nanostores/i18n'
|
||||
|
||||
const localeLoaders: Record<Exclude<Locale, 'en'>, () => 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, 'en'>(locale, {
|
||||
baseLocale: 'en',
|
||||
async get(code) {
|
||||
const mod = (await import(`../locales/${code}.json`)) as { default: ComponentsJSON }
|
||||
if (code === 'en') return {}
|
||||
const mod = await localeLoaders[code]()
|
||||
return mod.default
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -26,6 +26,14 @@ export const menuMessages = i18n('menu', {
|
|||
|
||||
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',
|
||||
|
|
|
|||
|
|
@ -19,6 +19,14 @@
|
|||
"profiler": "Leistungsprofil",
|
||||
"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"
|
||||
|
|
|
|||
|
|
@ -19,6 +19,14 @@
|
|||
"profiler": "Perfil de rendimiento",
|
||||
"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"
|
||||
|
|
|
|||
|
|
@ -19,6 +19,14 @@
|
|||
"profiler": "Profileur de performance",
|
||||
"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é"
|
||||
|
|
|
|||
|
|
@ -19,6 +19,14 @@
|
|||
"profiler": "Profilo prestazioni",
|
||||
"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"
|
||||
|
|
|
|||
|
|
@ -19,6 +19,14 @@
|
|||
"profiler": "Profiler wydajności",
|
||||
"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"
|
||||
|
|
|
|||
|
|
@ -19,6 +19,14 @@
|
|||
"profiler": "Профилирование",
|
||||
"moveToPage": "Переместить на страницу",
|
||||
"createInstance": "Создать экземпляр",
|
||||
"hide": "Скрыть",
|
||||
"show": "Показать",
|
||||
"lock": "Заблокировать",
|
||||
"unlock": "Разблокировать",
|
||||
"cut": "Вырезать",
|
||||
"front": "Вперёд",
|
||||
"back": "Назад",
|
||||
"toggleUi": "Переключить интерфейс",
|
||||
"bold": "Жирный",
|
||||
"italic": "Курсив",
|
||||
"underline": "Подчёркнутый"
|
||||
|
|
|
|||
|
|
@ -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 }}</span
|
||||
>
|
||||
<Tip label="Toggle UI (⌘\)">
|
||||
<Tip :label="`${t.toggleUi} (${mod}\\)`">
|
||||
<button
|
||||
data-test-id="app-toggle-ui"
|
||||
class="flex size-6 shrink-0 cursor-pointer items-center justify-center rounded text-muted transition-colors hover:bg-hover hover:text-surface"
|
||||
|
|
|
|||
|
|
@ -1,7 +1,13 @@
|
|||
<script setup lang="ts">
|
||||
import { TreeRoot, TreeItem, ContextMenuRoot, ContextMenuTrigger, ContextMenuPortal } from 'reka-ui'
|
||||
|
||||
import { LayerTreeRoot, LayerTreeItem, useInlineRename, useLayerDrag } from '@open-pencil/vue'
|
||||
import {
|
||||
LayerTreeRoot,
|
||||
LayerTreeItem,
|
||||
useI18n,
|
||||
useInlineRename,
|
||||
useLayerDrag
|
||||
} from '@open-pencil/vue'
|
||||
import { useEditorStore } from '@/stores/editor'
|
||||
import { nodeIcon, COMPONENT_TYPES } from '@/utils/layer-icons'
|
||||
import CanvasMenu from './CanvasMenu.vue'
|
||||
|
|
@ -10,6 +16,7 @@ import Tip from './ui/Tip.vue'
|
|||
const INDENT = 16
|
||||
const store = useEditorStore()
|
||||
const rename = useInlineRename((id, name) => store.renameNode(id, name))
|
||||
const { menu: t } = useI18n()
|
||||
const { draggingId, instruction, instructionTargetId } = useLayerDrag(store, INDENT)
|
||||
|
||||
function onLayerRightClick(e: MouseEvent) {
|
||||
|
|
@ -142,7 +149,7 @@ function onLayerRightClick(e: MouseEvent) {
|
|||
!node.locked && node.visible ? 'opacity-0 group-hover/row:opacity-100' : ''
|
||||
"
|
||||
>
|
||||
<Tip :label="node.locked ? 'Unlock' : 'Lock'">
|
||||
<Tip :label="node.locked ? t.unlock : t.lock">
|
||||
<span
|
||||
class="flex size-4 items-center justify-center rounded hover:bg-white/15"
|
||||
@pointerdown.stop
|
||||
|
|
@ -160,7 +167,7 @@ function onLayerRightClick(e: MouseEvent) {
|
|||
/>
|
||||
</span>
|
||||
</Tip>
|
||||
<Tip :label="node.visible ? 'Hide' : 'Show'">
|
||||
<Tip :label="node.visible ? t.hide : t.show">
|
||||
<span
|
||||
class="flex size-4 items-center justify-center rounded hover:bg-white/15"
|
||||
@pointerdown.stop
|
||||
|
|
|
|||
|
|
@ -31,9 +31,12 @@ import {
|
|||
ToolbarRoot,
|
||||
ToolbarItem,
|
||||
useEditorCommands,
|
||||
useI18n,
|
||||
useToolbarState,
|
||||
useViewportKind
|
||||
} from '@open-pencil/vue'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import type { Component } from 'vue'
|
||||
import type { Tool } from '@open-pencil/vue'
|
||||
|
||||
|
|
@ -41,20 +44,21 @@ const store = useEditorStore()
|
|||
const { isMobile } = useViewportKind()
|
||||
const { getCommand } = useEditorCommands()
|
||||
const { showActionToast } = useActionToast()
|
||||
const { menu: t, tools: toolTexts } = useI18n()
|
||||
|
||||
const toolLabels: Record<Tool, string> = {
|
||||
SELECT: 'Move',
|
||||
FRAME: 'Frame',
|
||||
SECTION: 'Section',
|
||||
RECTANGLE: 'Rectangle',
|
||||
ELLIPSE: 'Ellipse',
|
||||
LINE: 'Line',
|
||||
POLYGON: 'Polygon',
|
||||
STAR: 'Star',
|
||||
PEN: 'Pen',
|
||||
TEXT: 'Text',
|
||||
HAND: 'Hand'
|
||||
}
|
||||
const toolLabels = computed<Record<Tool, string>>(() => ({
|
||||
SELECT: toolTexts.value.move,
|
||||
FRAME: toolTexts.value.frame,
|
||||
SECTION: toolTexts.value.section,
|
||||
RECTANGLE: toolTexts.value.rectangle,
|
||||
ELLIPSE: toolTexts.value.ellipse,
|
||||
LINE: toolTexts.value.line,
|
||||
POLYGON: toolTexts.value.polygon,
|
||||
STAR: toolTexts.value.star,
|
||||
PEN: toolTexts.value.pen,
|
||||
TEXT: toolTexts.value.text,
|
||||
HAND: toolTexts.value.hand
|
||||
}))
|
||||
|
||||
const toolShortcuts: Record<Tool, string> = {
|
||||
SELECT: 'V',
|
||||
|
|
@ -78,29 +82,49 @@ interface ActionItem {
|
|||
|
||||
const flyoutMenuCls = useMenuUI({ content: 'min-w-32' })
|
||||
|
||||
const editActions: ActionItem[] = [
|
||||
{ icon: IconCopy, label: 'Copy', action: () => store.mobileCopy() },
|
||||
{ icon: IconClipboard, label: 'Paste', action: () => store.mobilePaste() },
|
||||
{ icon: IconScissors, label: 'Cut', action: () => store.mobileCut() },
|
||||
{ icon: IconCopyPlus, label: 'Duplicate', action: () => getCommand('selection.duplicate').run() },
|
||||
{ icon: IconTrash2, label: 'Delete', action: () => getCommand('selection.delete').run() }
|
||||
]
|
||||
const editActions = computed<ActionItem[]>(() => [
|
||||
{ icon: IconCopy, label: t.value.copy, action: () => store.mobileCopy() },
|
||||
{ icon: IconClipboard, label: t.value.paste, action: () => store.mobilePaste() },
|
||||
{ icon: IconScissors, label: t.value.cut, action: () => store.mobileCut() },
|
||||
{
|
||||
icon: IconCopyPlus,
|
||||
label: getCommand('selection.duplicate').label,
|
||||
action: () => getCommand('selection.duplicate').run()
|
||||
},
|
||||
{
|
||||
icon: IconTrash2,
|
||||
label: getCommand('selection.delete').label,
|
||||
action: () => getCommand('selection.delete').run()
|
||||
}
|
||||
])
|
||||
|
||||
const arrangeActions: ActionItem[] = [
|
||||
const arrangeActions = computed<ActionItem[]>(() => [
|
||||
{
|
||||
icon: IconArrowUpToLine,
|
||||
label: 'Front',
|
||||
label: t.value.front,
|
||||
action: () => getCommand('selection.bringToFront').run()
|
||||
},
|
||||
{
|
||||
icon: IconArrowDownToLine,
|
||||
label: 'Back',
|
||||
label: t.value.back,
|
||||
action: () => getCommand('selection.sendToBack').run()
|
||||
},
|
||||
{ icon: IconGroup, label: 'Group', action: () => getCommand('selection.group').run() },
|
||||
{ icon: IconUngroup, label: 'Ungroup', action: () => getCommand('selection.ungroup').run() },
|
||||
{ icon: IconLock, label: 'Lock', action: () => getCommand('selection.toggleLock').run() }
|
||||
]
|
||||
{
|
||||
icon: IconGroup,
|
||||
label: getCommand('selection.group').label,
|
||||
action: () => getCommand('selection.group').run()
|
||||
},
|
||||
{
|
||||
icon: IconUngroup,
|
||||
label: getCommand('selection.ungroup').label,
|
||||
action: () => getCommand('selection.ungroup').run()
|
||||
},
|
||||
{
|
||||
icon: IconLock,
|
||||
label: t.value.lock,
|
||||
action: () => getCommand('selection.toggleLock').run()
|
||||
}
|
||||
])
|
||||
|
||||
const {
|
||||
mobileCategory,
|
||||
|
|
|
|||
Loading…
Reference in a new issue