Skip to content

Commit 2f960be

Browse files
authored
feat(hub): localize dock, command and launcher titles with titleLocales (#417)
* feat(hub): localize dock, command and launcher titles with titleLocales A registrant adds translations next to its plain title: `titleLocales: { 'zh-CN': '构建', ja: 'ビルド' }`. The plain title stays the default, so a reader that ignores the map keeps working. resolveTitle() and matchLocale() from @devframes/hub pick the closest tag; the reference hub UI resolves every entry and command against its own language setting, and the example hosts resolve against the browser language. refs #398 * fix(hub-ui): import matchLocale from its new home in the language setting * fix(hub): keep the latest dock selection while a page script loads A dock switch that waits on a page script no longer overrides a newer switch that committed meanwhile, so clicking a dock while the previous one still loads its script keeps the click. * test(e2e): start the Next hub server without pnpm exec pnpm 12.6 starts an exec child in its own process group, so Playwright's teardown killed only the shell and the orphaned Next server held the stdout pipe open: every e2e run hung until the 15-minute job limit.
1 parent 495fc4e commit 2f960be

33 files changed

Lines changed: 236 additions & 44 deletions

File tree

‎docs/content/1.guide/16.hub.md‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,23 @@ ctx.commands.register({
4646

4747
`args` takes positional [Standard Schema](https://standardschema.dev/) schemas (a single `v.object(...)` unwraps into the input); omit for zero-arg. `safety` defaults to `'action'`; `when` clauses are unenforced for agent calls.
4848

49+
## Localized titles
50+
51+
A dock entry, a command, and a launcher each carry a plain `title` and an optional `titleLocales` map of translations by BCP 47 tag. The plain `title` stays the default, so a hub UI that ignores the map still shows it. The reference hub UI picks the translation closest to its own language setting (`zh-HK` falls back to `zh-TW`, `pt-PT` to `pt-BR`, `en-GB` to `en`):
52+
53+
```ts
54+
ctx.docks.register({
55+
type: 'iframe',
56+
id: 'app:build',
57+
title: 'Build',
58+
titleLocales: { 'zh-CN': '构建', 'ja': 'ビルド' },
59+
icon: 'ph:hammer-duotone',
60+
url: '/__build/',
61+
})
62+
```
63+
64+
A devframe localizes the dock a hub synthesizes for it through `dock: { titleLocales }` in its definition. A hub UI provider resolves a title with `resolveTitle(entry, locale)` from `@devframes/hub/client`.
65+
4966
## Nested commands
5067

5168
A command's `children` nest arbitrarily deep. The palette drills into each level, and every command in the tree is bindable at any depth: a shortcut assigned to a leaf several levels down fires as directly as one on a top-level command, and each appears as its own row under **Settings → Shortcuts**, indented by nesting level.

‎docs/content/1.guide/22.build-your-own-hub-ui.md‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,9 @@ the [Hub API reference](/references/hub-api#dock-entry-types).
4747

4848
Honor `when` / `visibility`, `category` grouping (order from
4949
`DEFAULT_CATEGORIES_ORDER`, `@devframes/hub/constants`), and the
50-
`hub:docks:activate` broadcast.
50+
`hub:docks:activate` broadcast. Show titles through
51+
`resolveTitle(entry, locale)` so a registrant's `titleLocales` translations
52+
reach the user.
5153

5254
An `iframe` entry serving a [remote assets package](/guide/client-assets) can
5355
report it unreachable: its fallback page posts a `RemoteAssetsErrorMessage`

‎docs/content/8.references/6.hub-api.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@ The origin-locked `postMessage` protocol on `devframe:frame-nav`: [Shared-iframe
147147

148148
## Dock entry types
149149

150-
The built-in variants of the open dock union (`DevframeDockEntryRegistry`, `@devframes/hub/types`) a hub UI provider renders: [Build Your Own Hub UI](/guide/build-your-own-hub-ui). Each entry's `icon` takes any of the [icon values](/references/node-api#icon-values).
150+
The built-in variants of the open dock union (`DevframeDockEntryRegistry`, `@devframes/hub/types`) a hub UI provider renders: [Build Your Own Hub UI](/guide/build-your-own-hub-ui). Each entry's `icon` takes any of the [icon values](/references/node-api#icon-values). Each entry's `title` has an optional `titleLocales` map of translations, resolved with `resolveTitle(entry, locale)` from `@devframes/hub/client`; commands and launchers carry the same pair.
151151

152152
| Type | The hub UI provider renders |
153153
|---|---|

‎examples/custom-hub-next/src/client/app/page.tsx‎

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import type {
1111
import type { DevframeJsonRenderSpec } from '@devframes/json-render'
1212
import type { DevframeJsonRenderDockEntry } from '@devframes/json-render/hub'
1313
import type { FormEvent } from 'react'
14-
import { connectDevframe, createDevframeClientRuntime, FRAME_NAV_CHANNEL } from '@devframes/hub/client'
14+
import { connectDevframe, createDevframeClientRuntime, FRAME_NAV_CHANNEL, resolveTitle } from '@devframes/hub/client'
1515
import { useEffect, useMemo, useRef, useState } from 'react'
1616
import { createReactJsonRenderDockRenderer } from '../json-render/react-renderer'
1717
import { dockIconSvg } from './icons'
@@ -246,7 +246,7 @@ function DockIcon({ entry }: { entry: DevframeDockEntry }) {
246246
const svg = useDockIconSvg(entry.icon)
247247
if (svg)
248248
return <span className="h-5 w-5 shrink-0 text-lg" dangerouslySetInnerHTML={{ __html: svg }} />
249-
const initial = (entry.title?.[0] ?? '?').toUpperCase()
249+
const initial = (resolveTitle(entry, navigator.language)[0] ?? '?').toUpperCase()
250250
return <span className="grid h-5 w-5 shrink-0 place-items-center rounded bg-active text-[0.7rem] font-bold">{initial}</span>
251251
}
252252

@@ -537,7 +537,7 @@ export default function Page() {
537537
let el = pool.get(key)
538538
if (!el) {
539539
el = document.createElement('iframe')
540-
el.title = selectedDock.title
540+
el.title = resolveTitle(selectedDock, navigator.language)
541541
el.className = 'absolute inset-0 block h-full w-full border-0 bg-base'
542542
el.src = selectedDock.url
543543
stage.appendChild(el)
@@ -652,10 +652,10 @@ export default function Page() {
652652
void ctx?.docks.switchEntry(dock.id)
653653
}}
654654
className={`relative inline-flex items-center gap-1.5 max-w-52 px-2 py-1 rounded-md border border-transparent text-sm op-fade select-none cursor-pointer transition hover:op100 hover:bg-active w-full! max-w-none! gap-2.5!${dock.id === selectedDockId ? ' op100! bg-active border-base! color-base' : ''}`}
655-
title={dock.title}
655+
title={resolveTitle(dock, navigator.language)}
656656
>
657657
<DockIcon entry={dock} />
658-
<span className="truncate">{dock.title}</span>
658+
<span className="truncate">{resolveTitle(dock, navigator.language)}</span>
659659
{dock.badge && <span className="ml-auto shrink-0 rounded bg-active px1 py0.5 text-[0.6rem] font-mono color-base">{dock.badge}</span>}
660660
</button>
661661
</li>
@@ -709,7 +709,7 @@ export default function Page() {
709709
? <li className="rounded-lg border border-base bg-base border-dashed px2.5 py1.5 text-xs font-mono op-mute">Waiting for snapshot…</li>
710710
: commands.map(command => (
711711
<li key={command.id} className={rowClass}>
712-
{command.title}
712+
{resolveTitle(command, navigator.language)}
713713
{' '}
714714
<code className="op-fade">{command.id}</code>
715715
</li>

‎examples/custom-hub-vite/src/client/main.ts‎

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import type {
77
} from '@devframes/hub/types'
88
import type { DevframeJsonRenderSpec } from '@devframes/json-render'
99
import type { DevframeJsonRenderDockEntry } from '@devframes/json-render/hub'
10-
import { connectDevframe, createDevframeClientRuntime, FRAME_NAV_CHANNEL } from '@devframes/hub/client'
10+
import { connectDevframe, createDevframeClientRuntime, FRAME_NAV_CHANNEL, resolveTitle } from '@devframes/hub/client'
1111
import { dockIconSvg } from './icons'
1212
import 'virtual:uno.css'
1313
import '@antfu/design/styles.css'
@@ -85,15 +85,17 @@ function iconName(icon: DevframeDockEntry['icon']): string | undefined {
8585
// real SVG once `paintDockIcons` resolves it), the title, and an optional badge.
8686
function dockButton(entry: DevframeDockEntry, selectedId: string | null): string {
8787
const active = entry.id === selectedId
88-
const initial = (entry.title?.[0] ?? '?').toUpperCase()
88+
// `titleLocales` carries a registrant's translations; the browser language picks one.
89+
const title = resolveTitle(entry, navigator.language)
90+
const initial = (title[0] ?? '?').toUpperCase()
8991
const badge = entry.badge
9092
? `<span class="ml-auto shrink-0 rounded bg-active px1 py0.5 text-[0.6rem] font-mono color-base">${entry.badge}</span>`
9193
: ''
9294
return `<li>
93-
<button type="button" data-dock-id="${entry.id}" title="${entry.title}"
95+
<button type="button" data-dock-id="${entry.id}" title="${title}"
9496
class="relative flex w-full items-center gap-2.5 px-2 py-1 rounded-md border border-transparent text-sm select-none cursor-pointer transition hover:op100 hover:bg-active${active ? ' op100 bg-active border-base! color-base' : ' op-fade'}">
9597
<span class="grid h-5 w-5 shrink-0 place-items-center rounded bg-active text-[0.7rem] font-bold" data-dock-icon="${entry.id}">${initial}</span>
96-
<span class="truncate">${entry.title}</span>${badge}
98+
<span class="truncate">${title}</span>${badge}
9799
</button>
98100
</li>`
99101
}
@@ -379,7 +381,7 @@ function wireDockRail(host: Awaited<ReturnType<typeof createDevframeClientRuntim
379381
let frame = iframes.get(key)
380382
if (!frame) {
381383
frame = document.createElement('iframe')
382-
frame.title = entry.title
384+
frame.title = resolveTitle(entry, navigator.language)
383385
frame.className = 'absolute inset-0 block h-full w-full border-0 bg-base'
384386
frame.hidden = true
385387
frame.src = entry.url
@@ -508,7 +510,7 @@ async function wireDrawer(rpc: Awaited<ReturnType<typeof connectDevframe>>): Pro
508510
// Commands - read straight from `devframe:commands` shared state.
509511
const commands = await rpc.sharedState.get<DevframeCommandEntry[]>('devframe:commands', { initialValue: [] })
510512
const renderCommands = (): void => renderList(el.commands, commands.value() ?? [], c =>
511-
`<li class="rounded-lg border border-base bg-base px2.5 py1.5 text-xs font-mono">${c.title} <code class="op-fade">${c.id}</code></li>`)
513+
`<li class="rounded-lg border border-base bg-base px2.5 py1.5 text-xs font-mono">${resolveTitle(c, navigator.language)} <code class="op-fade">${c.id}</code></li>`)
512514
commands.on('updated', renderCommands)
513515
renderCommands()
514516

‎packages/devframe/src/types/devframe.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,8 @@ export interface DevframeCliOptions {
267267
export interface DevframeDockDefaults {
268268
/** Dock entry title. Defaults to the definition's `name`. */
269269
title?: string
270+
/** Translations of `title` by BCP 47 tag; see `@devframes/hub`'s `resolveTitle()`. */
271+
titleLocales?: Record<string, string>
270272
/** Dock entry icon. Defaults to the definition's `icon`. */
271273
icon?: string | { light: string, dark: string }
272274
/**

‎packages/hub-ui/playground/seed.ts‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ const playgroundGroup: DevframeViewGroup = {
3737
type: 'group',
3838
id: PLAYGROUND_GROUP_ID,
3939
title: 'Playground Tools',
40+
// Exercises `titleLocales`: switch the language in Settings → Appearance.
41+
titleLocales: { 'zh-CN': '演练场工具', 'ja': 'プレイグラウンドツール' },
4042
icon: 'ph:flask-duotone',
4143
category: 'app',
4244
// No `defaultChildId`, so clicking reveals the member popover instead of
@@ -72,6 +74,7 @@ export async function seedPlayground(ctx: DevframeHubContext): Promise<void> {
7274
ctx.commands.register({
7375
id: 'playground:say-hello',
7476
title: 'Playground · Say Hello',
77+
titleLocales: { 'zh-CN': '演练场 · 打个招呼', 'ja': 'プレイグラウンド · あいさつ' },
7578
icon: 'ph:hand-waving-duotone',
7679
category: 'playground',
7780
handler: () => 'Hello from the hub-ui playground!',

‎packages/hub-ui/src/client/components/views-builtin/SettingsAppearance.vue‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22
import type { DocksContext } from '@devframes/hub/client'
33
import FormSelect from '@antfu/design/components/Form/FormSelect.vue'
44
import { computed } from 'vue'
5-
import { HUB_UI_LOCALES, matchLocale } from '../../../locales'
6-
import { localePreference, setLocalePreference, t } from '../../i18n'
5+
import { HUB_UI_LOCALES } from '../../../locales'
6+
import { localePreference, matchLocale, setLocalePreference, t } from '../../i18n'
77
import { useBranding } from '../../state/branding'
88
import { colorSchemePreference, setColorSchemePreference } from '../../state/color-mode'
99
import { isDockPopupSupported, requestDockPopupOpen, useIsDockPopupOpen } from '../../state/popup'

‎packages/hub-ui/src/client/components/views/ViewLauncher.vue‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { HUB_EVENTS } from '@devframes/hub/constants'
77
import { computed, ref, watch } from 'vue'
88
import { TERMINALS_DOCK_ID } from '../../constants'
99
import { t } from '../../i18n'
10+
import { localizeTitle } from '../../state/locale'
1011
import DockIcon from '../dock/DockIcon.vue'
1112
1213
const props = defineProps<{
@@ -92,7 +93,7 @@ const canLaunch = computed(() => status.value === 'idle' || status.value === 'er
9293
<div class="flex flex-col gap-4 items-center justify-center h-full relative">
9394
<DockIcon :icon="entry.launcher.icon || entry.icon" class="w-10 h-10" />
9495
<h1 class="text-2xl font-bold">
95-
{{ entry.launcher.title }}
96+
{{ localizeTitle(entry.launcher).title }}
9697
</h1>
9798
<p>{{ entry.launcher.description }}</p>
9899

‎packages/hub-ui/src/client/i18n/i18n.test.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { afterEach, beforeAll, describe, expect, it } from 'vitest'
2-
import { HUB_UI_LOCALES, matchLocale } from '../../locales'
3-
import { loadLocale, locale, setHostLocale, setLocalePreference, t, tAround } from './index'
2+
import { HUB_UI_LOCALES } from '../../locales'
3+
import { loadLocale, locale, matchLocale, setHostLocale, setLocalePreference, t, tAround } from './index'
44
import en from './locales/en.json'
55

66
const files = import.meta.glob<Record<string, string>>('./locales/*.json', { import: 'default' })

0 commit comments

Comments
 (0)