diff --git a/.agents/06-design-system.md b/.agents/06-design-system.md index 265660a96..2870a002a 100644 --- a/.agents/06-design-system.md +++ b/.agents/06-design-system.md @@ -28,6 +28,7 @@ Two shadow-root gotchas the ahead-of-time CSS builder MUST compensate for (both - **A surface keeping its own component CSS** (inspect, a11y) sources every color from `@antfu/design`'s semantic shortcuts via `--at-apply` (expanded by `transformerDirectives`) rather than hardcoding a palette, so it tracks the shared theme and the `.dark` class. - **Plain `.ts`/vanilla views** MUST opt `.ts` into UnoCSS extraction (`content.pipeline.include` for Vite, or `content.filesystem` globs for the `@unocss/postcss` setup Next uses), since UnoCSS only scans framework files by default. - Before making new components or inline DOM elements, always check whether an `@antfu/design` component can be reused. +- **hub-ui text goes through `t()`.** Every user-facing string in `packages/hub-ui/src/client` (templates, tooltips, `aria-label`s, command titles, confirm copy) MUST be a key in `client/i18n/locales/en.json` rendered with `t()` / `tAround()` from `client/i18n`, never a literal. A new key lands in every locale file in the same change; the i18n test enforces the lockstep. Brand names and `DF` diagnostics stay untranslated. ## Storybook diff --git a/docs/content/1.guide/18.hub-initiate.md b/docs/content/1.guide/18.hub-initiate.md index b0247794b..f5d2a86f5 100644 --- a/docs/content/1.guide/18.hub-initiate.md +++ b/docs/content/1.guide/18.hub-initiate.md @@ -59,6 +59,9 @@ interface DevframeHubUi { - `'normal'` (default): shows immediately. - `'passive'`: hidden until `Shift+Alt+D`, then persisted per-origin (later browser sessions start shown). - `'hidden'`: hidden until `Shift+Alt+D`, that browser session only. +- **`locale`** sets the default UI language, one of the tags in `HUB_UI_LOCALES` (`en`, `zh-CN`, `zh-TW`, `ja`, `ko`, `es`, `fr`, `de`, `pt-BR`, `ru`). Without it the UI follows the browser language and falls back to English. A visitor's own pick in Settings → Appearance wins and persists per browser. The hub UI writes the language it resolved back into `ConnectionMeta.configs.ui.locale` on its window, so a same-origin frame that inherits the connection (see [Client](/guide/client)) reads the effective language at boot. + +To add a language, add its tag and native name to `packages/hub-ui/src/locales.ts`, translate a copy of `packages/hub-ui/src/client/i18n/locales/en.json` under that tag, and register the file in the `messages` map in `packages/hub-ui/src/client/i18n/index.ts`. The i18n test fails on a missing key or a changed `{slot}`. ## Renderer modules diff --git a/packages/devframe/src/client/connection-storage.ts b/packages/devframe/src/client/connection-storage.ts index 8bb16f2b1..25bcace2a 100644 --- a/packages/devframe/src/client/connection-storage.ts +++ b/packages/devframe/src/client/connection-storage.ts @@ -44,6 +44,11 @@ export function readStoredAuthToken(userAuthToken?: string): string | undefined return readFromWindows(CONNECTION_AUTH_TOKEN_KEY) } +/** + * Publish the connection on this window for same-origin frames to inherit. + * Call again after mutating `connection.connectionMeta` so the legacy meta + * copy below follows. + */ export function storeConnection(connection: DevframeConnection): void { ;(globalThis as any)[DEVFRAME_CONNECTION_KEY] = connection // Keep the established metadata/auth globals in sync for viewers that still diff --git a/packages/devframe/src/client/index.ts b/packages/devframe/src/client/index.ts index 1add93bc5..f1f73dce4 100644 --- a/packages/devframe/src/client/index.ts +++ b/packages/devframe/src/client/index.ts @@ -1,6 +1,7 @@ import { getDevframeRpcClient } from './rpc' export * from './connection' +export { storeConnection } from './connection-storage' export * from './otp' export * from './rpc' export type { DevframeServiceClientHandle, DevframeServicesClient } from './rpc-services' diff --git a/packages/hub-ui/.storybook/preview.ts b/packages/hub-ui/.storybook/preview.ts index 45cacd0f2..319b26d1e 100644 --- a/packages/hub-ui/.storybook/preview.ts +++ b/packages/hub-ui/.storybook/preview.ts @@ -1,4 +1,6 @@ import type { Decorator, Preview } from '@storybook/vue3-vite' +import { setLocalePreference } from '../src/client/i18n' +import { HUB_UI_LOCALES } from '../src/locales' // Reset first so uno utilities win over its resets, matching the production // `[reset, userStyle, unoCss, ...]` order; otherwise stories lose the reset. import '@unocss/reset/tailwind.css' @@ -18,6 +20,7 @@ function applyTheme(theme: string): void { const withTheme: Decorator = (story, context) => { applyTheme(context.globals.theme ?? 'dark') + setLocalePreference(context.globals.locale ?? 'en') return { components: { story }, template: '' } } @@ -46,6 +49,16 @@ const preview: Preview = { dynamicTitle: true, }, }, + locale: { + description: 'UI language', + defaultValue: 'en', + toolbar: { + title: 'Language', + icon: 'globe', + items: Object.entries(HUB_UI_LOCALES).map(([value, title]) => ({ value, title })), + dynamicTitle: true, + }, + }, }, decorators: [withTheme], } diff --git a/packages/hub-ui/playground/main.ts b/packages/hub-ui/playground/main.ts index b303c5b62..bbff6d81d 100644 --- a/packages/hub-ui/playground/main.ts +++ b/packages/hub-ui/playground/main.ts @@ -4,6 +4,7 @@ import { useLocalStorage } from '@vueuse/core' import { watchEffect } from 'vue' import { isDark } from '../src/client/state/color-mode' import { DEFAULT_DOCK_PANEL_STORE } from '../src/client/state/docks' +import { setupLocale } from '../src/client/state/locale' /** * The base `hub-plugin.ts` mounts the playground's hub instance at. Kept as @@ -34,6 +35,7 @@ watchEffect(() => { async function main(): Promise { const rpc = await getDevframeRpcClient({ baseURL: HUB_BASE, simpleAuth: false }) + setupLocale(rpc) const { createDocksContext } = await import('../src/client/state/context') if (mode === 'embedded') { diff --git a/packages/hub-ui/src/client/components/command-palette/CommandPalette.vue b/packages/hub-ui/src/client/components/command-palette/CommandPalette.vue index f77bc3e57..2b1ab5b28 100644 --- a/packages/hub-ui/src/client/components/command-palette/CommandPalette.vue +++ b/packages/hub-ui/src/client/components/command-palette/CommandPalette.vue @@ -4,6 +4,7 @@ import type { DocksContext } from '@devframes/hub/client' import type { PaletteCrumb, PaletteFlatItem } from '../../state/palette' import Fuse from 'fuse.js' import { computed, nextTick, ref, useTemplateRef, watch } from 'vue' +import { t, tAround } from '../../i18n' import { flattenPaletteCommands, paletteActionKeepsOpen, paletteScopeTrail, paletteTrailScopeId, reconcilePaletteTrail, resolvePaletteSelection } from '../../state/palette' import BrandWordmark from '../icons/BrandWordmark.vue' import CommandPaletteItem from './CommandPaletteItem.vue' @@ -13,6 +14,7 @@ const props = defineProps<{ }>() const commandsCtx = computed(() => props.context.commands) +const noResults = computed(() => tAround('palette.noResults', 'query')) const show = computed({ get: () => commandsCtx.value.paletteOpen, set: (v) => { commandsCtx.value.paletteOpen = v }, @@ -311,7 +313,7 @@ function getKeybindings(id: string) { ref="searchInput" v-model="search" class="flex-1 bg-transparent py-3 outline-none text-sm color-base" - placeholder="Type a command..." + :placeholder="t('palette.placeholder')" @keydown="onKeyDown" > @@ -334,10 +336,10 @@ function getKeybindings(id: string) {
- No results for "{{ search }}" + {{ noResults[0] }}{{ search }}{{ noResults[1] }}
- No commands available + {{ t('palette.empty') }}
@@ -346,15 +348,15 @@ function getKeybindings(id: string) {
↓↑ - navigate + {{ t('palette.navigate') }}
esc - {{ breadcrumb.length > 0 || dynamicItems ? 'back' : 'close' }} + {{ breadcrumb.length > 0 || dynamicItems ? t('palette.back') : t('palette.close') }}
↵ - select + {{ t('palette.select') }}
diff --git a/packages/hub-ui/src/client/components/command-palette/CommandPaletteItem.vue b/packages/hub-ui/src/client/components/command-palette/CommandPaletteItem.vue index f39a253b8..d6594d887 100644 --- a/packages/hub-ui/src/client/components/command-palette/CommandPaletteItem.vue +++ b/packages/hub-ui/src/client/components/command-palette/CommandPaletteItem.vue @@ -1,6 +1,7 @@ @@ -9,10 +10,12 @@ import { isDark } from '../../state/color-mode' fixed/absolute-positioned children (dock anchor, floating layers) are unaffected while every descendant still matches `.dark …` selectors, inside the dock's shadow root and the light-DOM standalone page alike. The native - `color-scheme` inherits down for scrollbars and form controls. + `color-scheme` inherits down for scrollbars and form controls, as does + `lang` for hyphenation, fonts and screen readers. -->
diff --git a/packages/hub-ui/src/client/components/dock/Dock.vue b/packages/hub-ui/src/client/components/dock/Dock.vue index a11a8c2b4..43453e8a6 100644 --- a/packages/hub-ui/src/client/components/dock/Dock.vue +++ b/packages/hub-ui/src/client/components/dock/Dock.vue @@ -5,6 +5,7 @@ import type { DockLayout } from './dock-layout' import { useEventListener, useScreenSafeArea, whenever } from '@vueuse/core' import { computed, onMounted, reactive, ref, useTemplateRef, watch } from 'vue' import { BUILTIN_ENTRY_CLIENT_AUTH_NOTICE } from '../../constants' +import { t } from '../../i18n' import { docksSplitGroupsWithCapacity, getEntryGroup, resolveNextRecentDockId, resolveRecentDockEntry } from '../../state/dock-settings' import { sharedStateToRef } from '../../state/docks' import { setDocksOverflowPanel, useDocksGroupPanel } from '../../state/floating-tooltip' @@ -325,7 +326,7 @@ onMounted(() => { :class="context.panel.isVertical ? 'rotate-270' : 'rotate-0'" />
- Unauthorized + {{ t('dock.unauthorized') }}
diff --git a/packages/hub-ui/src/client/components/dock/DockContextMenu.ts b/packages/hub-ui/src/client/components/dock/DockContextMenu.ts index cdf2ae494..fb692cde9 100644 --- a/packages/hub-ui/src/client/components/dock/DockContextMenu.ts +++ b/packages/hub-ui/src/client/components/dock/DockContextMenu.ts @@ -1,6 +1,7 @@ import type { DevframeDockEntry } from '@devframes/hub' import type { DocksContext } from '@devframes/hub/client' import { h } from 'vue' +import { t } from '../../i18n' import { setDockContextMenu } from '../../state/floating-tooltip' import { isDockPopupSupported, requestDockPopupOpen, useIsDockPopupOpen } from '../../state/popup' @@ -68,19 +69,19 @@ export function openDockContextMenu(options: { const isEdgeMode = context.panel.store.mode === 'edge' const items: DockMenuItem[] = [ { - label: 'Hide', + label: t('common.hide'), icon: 'i-ph-eye-slash-duotone', action: () => hideDock(context, entry), visible: canHide(context, entry), }, { - label: 'Refresh', + label: t('common.refresh'), icon: 'i-ph-arrow-clockwise-duotone', action: () => refreshDock(context, entry), visible: canRefresh(entry), }, { - label: isEdgeMode ? 'Float Mode' : 'Edge Mode', + label: isEdgeMode ? t('command.floatMode') : t('command.edgeMode'), icon: isEdgeMode ? 'i-ph-arrows-out-duotone' : 'i-ph-square-half-bottom-duotone', action: () => { if (isEdgeMode) { @@ -114,7 +115,7 @@ export function openDockContextMenu(options: { visible: context.clientType === 'embedded', }, { - label: 'Popup', + label: t('dock.popup'), icon: 'i-ph-arrow-square-out-duotone', action: () => { setDockContextMenu(null) diff --git a/packages/hub-ui/src/client/components/dock/DockEdge.vue b/packages/hub-ui/src/client/components/dock/DockEdge.vue index 55c6490fa..bd8e87a7d 100644 --- a/packages/hub-ui/src/client/components/dock/DockEdge.vue +++ b/packages/hub-ui/src/client/components/dock/DockEdge.vue @@ -6,6 +6,7 @@ import type { DockEdge as DockEdgePosition, DockLayout } from './dock-layout' import { useEventListener } from '@vueuse/core' import { computed, h, onMounted, ref, useTemplateRef, watch } from 'vue' import { BUILTIN_ENTRY_SETTINGS } from '../../constants' +import { t } from '../../i18n' import { docksSplitGroupsBySize, getEntryGroup } from '../../state/dock-settings' import { setEdgePositionDropdown, setFloatingTooltip, useDocksGroupPanel, useDocksOverflowPanel, useEdgePositionDropdown } from '../../state/floating-tooltip' import { useSettings } from '../../state/settings-defaults' @@ -512,9 +513,9 @@ const dragPreviewStyle = computed(() => { >
- No tools yet + {{ t('dock.noTools') }}
diff --git a/packages/hub-ui/src/client/components/dock/DockGroupSidebar.vue b/packages/hub-ui/src/client/components/dock/DockGroupSidebar.vue index b23ee4a40..e19d2503d 100644 --- a/packages/hub-ui/src/client/components/dock/DockGroupSidebar.vue +++ b/packages/hub-ui/src/client/components/dock/DockGroupSidebar.vue @@ -3,6 +3,7 @@ import type { DevframeDockEntry, DevframeViewGroup } from '@devframes/hub' import type { DocksContext } from '@devframes/hub/client' import { watchDebounced } from '@vueuse/core' import { computed, h, ref, useTemplateRef, watch } from 'vue' +import { t } from '../../i18n' import { deriveSidebarCapacity, docksSplitGroupsWithCapacity, getGroupMembersGrouped } from '../../state/dock-settings' import { setDocksSidebarOverflowPanel, setFloatingTooltip, useDocksSidebarOverflowPanel } from '../../state/floating-tooltip' import { useSettings } from '../../state/settings-defaults' @@ -199,10 +200,10 @@ const moreButtonActive = computed(() => selectedInOverflow.value || isOverflowPa