Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ curl -fsS http://127.0.0.1:8765/healthz

```bash
curl -fsSL \
https://github.com/mcocdaa/TermFlow/releases/download/v0.1.0/install-termflow-node.sh \
https://github.com/mcocdaa/TermFlow/releases/download/v0.2.0/install-termflow-node.sh \
| bash

termflow login \
Expand Down
2 changes: 1 addition & 1 deletion README_CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ curl -fsS http://127.0.0.1:8765/healthz

```bash
curl -fsSL \
https://github.com/mcocdaa/TermFlow/releases/download/v0.1.0/install-termflow-node.sh \
https://github.com/mcocdaa/TermFlow/releases/download/v0.2.0/install-termflow-node.sh \
| bash

termflow login \
Expand Down
171 changes: 0 additions & 171 deletions README_EN.md

This file was deleted.

26 changes: 24 additions & 2 deletions packages/client-core/src/http/apiError.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
export type ApiErrorKind = 'offline' | 'authentication' | 'validation' | 'rate_limit' | 'server' | 'aborted' | 'http_capability_denied'

const safeMessages: Record<ApiErrorKind, string> = {
const safeMessagesZh: Record<ApiErrorKind, string> = {
offline: '无法连接服务,请检查网络后重试。',
authentication: '会话已过期,请重新登录。',
validation: '提交的内容不符合要求。',
Expand All @@ -10,6 +10,28 @@ const safeMessages: Record<ApiErrorKind, string> = {
http_capability_denied: '客户端网络权限配置无效,请升级或重新安装 TermFlow。',
}

const safeMessagesEn: Record<ApiErrorKind, string> = {
offline: 'Unable to connect to the server. Please check your network and try again.',
authentication: 'Session expired. Please sign in again.',
validation: 'The submitted content is invalid.',
rate_limit: 'Too many requests. Please try again later.',
server: 'Service is temporarily unavailable. Please try again later.',
aborted: 'Request canceled.',
http_capability_denied: 'Client network capability configuration is invalid. Please update or reinstall TermFlow.',
}

function isEnglishLocale(): boolean {
try {
return typeof localStorage !== 'undefined' && typeof localStorage.getItem === 'function' && localStorage.getItem('termflow_lang') === 'en'
} catch {
return false
}
}

export function safeMessageFor(kind: ApiErrorKind): string {
return isEnglishLocale() ? safeMessagesEn[kind] : safeMessagesZh[kind]
}

export interface ApiErrorDetails {
status?: number
code?: string
Expand All @@ -26,7 +48,7 @@ export class ApiError extends Error {
readonly dpopNonce?: string

constructor(public readonly kind: ApiErrorKind, details: ApiErrorDetails = {}) {
super(safeMessages[kind])
super(safeMessageFor(kind))
this.name = 'ApiError'
if (details.status !== undefined) this.status = details.status
if (details.code !== undefined) this.code = details.code
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<span>{{ t('agent.chat.backOverview') }}</span>
</RouterLink>
<div>
<p class="eyebrow">Agent Chat</p>
<p class="eyebrow">{{ t('agent.chat.eyebrow') }}</p>
<h1 data-agent-chat-title>{{ title }}</h1>
<p class="agent-chat-header__binding" data-agent-chat-binding>{{ bindingLabel }}</p>
</div>
Expand Down
4 changes: 2 additions & 2 deletions packages/client-ui/src/components/agent/AgentPartsList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
<AgentCollapsibleOutput :content="pretty(part.input)" />
</template>
<p v-if="part.input_omitted?.length" class="agent-tool-activity__label" data-agent-tool-defaults>
{{ t('agent.chat.toolDefaultsOmitted', { items: part.input_omitted.join('、') }) }}
{{ t('agent.chat.toolDefaultsOmitted', { items: part.input_omitted.join(locale === 'en' ? ', ' : '、') }) }}
</p>
<template v-if="part.output">
<p class="agent-tool-activity__label">{{ t('agent.chat.toolOutput') }}</p>
Expand Down Expand Up @@ -63,7 +63,7 @@ import { Sparkles, Wrench } from '@lucide/vue'
import { useI18n } from '../../i18n'
import AgentCollapsibleOutput from './AgentCollapsibleOutput.vue'

const { t } = useI18n()
const { t, locale } = useI18n()

export interface AgentConversationPart {
type: string
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<section ref="panel" role="dialog" aria-modal="true" :aria-label="t('agent.reauth.title')" class="dialog-panel agent-reauth-dialog" @keydown="onKeydown">
<header>
<div>
<p class="eyebrow">Security Check</p>
<p class="eyebrow">{{ t('settings.totp.securityCheckEyebrow') }}</p>
<h2>{{ t('agent.reauth.title') }}</h2>
</div>
<button type="button" class="icon-button icon-only" :aria-label="t('common.close')" data-action="close-reauth-dialog" @click="cancel">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { computed, ref } from 'vue'
import { Bot } from '@lucide/vue'
import type { AgentSetupResponse } from '@termflow/client-contracts'
import { useI18n } from '../../i18n'
const { t } = useI18n()
const { t, locale } = useI18n()
const props = defineProps<{ open: boolean; pendingCount: number; readiness: AgentSetupResponse['state'] | null }>()
const emit = defineEmits<{ toggle: [] }>()
const button = ref<HTMLButtonElement | null>(null)
Expand All @@ -19,6 +19,6 @@ const accessibleLabel = computed(() => [
'Agent',
readinessLabel.value,
props.pendingCount ? t('agent.conversations.pendingBadgeAria', { count: props.pendingCount }) : null,
].filter(Boolean).join(','))
].filter(Boolean).join(locale.value === 'en' ? ', ' : ','))
defineExpose({ focus: () => button.value?.focus() })
</script>
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
>
<h2 id="delete-computer-title">{{ t('computers.deleteDialog.title') }}</h2>
<div id="delete-computer-description">
<p>{{ t('computers.deleteDialog.desc1') }}<strong>{{ computer.display_name }}</strong>。</p>
<p>{{ t('computers.deleteDialog.desc1') }}<strong>{{ computer.display_name }}</strong>{{ locale === 'en' ? '.' : '。' }}</p>
<p>{{ t('computers.deleteDialog.desc2') }}</p>
</div>
<p v-if="error" class="form-error" role="alert">{{ error }}</p>
Expand Down Expand Up @@ -41,7 +41,7 @@
import { nextTick, onBeforeUnmount, onMounted, ref } from 'vue'
import { useI18n } from '../../i18n'
import type { ComputerSummary } from '../../types'
const { t } = useI18n()
const { t, locale } = useI18n()

const props = defineProps<{ computer: ComputerSummary; pending: boolean; error: string; cleanupJobId?: string }>()
const emit = defineEmits<{ confirm: [installationId: string]; cancel: [] }>()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<template>
<article class="computer-card" :data-computer-id="computer.installation_id">
<header>
<div><p class="eyebrow">Computer</p><h2>{{ computer.display_name }}</h2><p v-if="metadata" class="muted">{{ metadata }}</p></div>
<div><p class="eyebrow">{{ t('dashboard.computerEyebrow') }}</p><h2>{{ computer.display_name }}</h2><p v-if="metadata" class="muted">{{ metadata }}</p></div>
<StatusPill :online="computer.online" />
</header>
<div v-if="computer.terms.length" class="term-list"><TermRow v-for="term in computer.terms" :key="term.instance_id" :term="term" @request-delete="$emit('request-delete', $event)" /></div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
>
<h2 id="delete-term-title">{{ t('dashboard.deleteDialog.title') }}</h2>
<div id="delete-term-description">
<p>{{ t('dashboard.deleteDialog.desc1') }}<strong>{{ term.name }}</strong>({{ term.instance_id }})。</p>
<p>{{ t('dashboard.deleteDialog.desc1') }}<strong>{{ term.name }}</strong>{{ locale === 'en' ? ` (${term.instance_id}).` : `(${term.instance_id})。` }}</p>
<p>{{ t('dashboard.deleteDialog.desc2') }}</p>
<p>{{ t('dashboard.deleteDialog.desc3') }}</p>
<code class="delete-term-activation">termflow activate {{ term.instance_id }}</code>
Expand Down Expand Up @@ -43,7 +43,7 @@
import { nextTick, onBeforeUnmount, onMounted, ref } from 'vue'
import { useI18n } from '../../i18n'
import type { TermSummary } from '../../types'
const { t } = useI18n()
const { t, locale } = useI18n()

const props = defineProps<{ term: TermSummary; pending: boolean; error: string; cleanupJobId?: string }>()
const emit = defineEmits<{ confirm: [instanceId: string]; cancel: [] }>()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<template>
<section class="settings-panel" aria-labelledby="clients-heading">
<div class="settings-panel-heading"><div><p class="eyebrow">Access</p><h2 id="clients-heading">{{ t('settings.clients.heading') }}</h2></div><div class="page-heading-actions"><span class="status-chip">{{ clients.length }}</span><button data-action="authorize-new-client" class="text-button" type="button" :title="t('settings.clients.authorizeNewTitle')" @click="approvalOpen = true">{{ t('settings.clients.authorizeNew') }}</button></div></div>
<div class="settings-panel-heading"><div><p class="eyebrow">{{ t('settings.clients.eyebrow') }}</p><h2 id="clients-heading">{{ t('settings.clients.heading') }}</h2></div><div class="page-heading-actions"><span class="status-chip">{{ clients.length }}</span><button data-action="authorize-new-client" class="text-button" type="button" :title="t('settings.clients.authorizeNewTitle')" @click="approvalOpen = true">{{ t('settings.clients.authorizeNew') }}</button></div></div>
<p v-if="clients.length === 0" class="empty-state">{{ t('settings.clients.empty') }}</p>
<ul v-else class="authorized-client-list">
<li v-for="client in clients" :key="client.client_id">
Expand All @@ -10,7 +10,7 @@
</ul>
<div v-if="selected" class="dialog-backdrop" @click.self="cancel">
<section ref="revokePanel" class="dialog-panel revoke-client-dialog" role="alertdialog" aria-modal="true" aria-labelledby="revoke-client-title" @keydown="trapFocus">
<header><div><p class="eyebrow">Access</p><h2 id="revoke-client-title">{{ t('settings.clients.revokeTitle') }}</h2></div><button class="icon-button icon-only" type="button" :aria-label="t('common.close')" @click="cancel">×</button></header>
<header><div><p class="eyebrow">{{ t('settings.clients.eyebrow') }}</p><h2 id="revoke-client-title">{{ t('settings.clients.revokeTitle') }}</h2></div><button class="icon-button icon-only" type="button" :aria-label="t('common.close')" @click="cancel">×</button></header>
<p>{{ t('settings.clients.revokeDesc', { name: selected.display_name }) }}</p>
<form class="security-form" @submit.prevent="removeSelected">
<label for="revoke-admin-token">{{ t('settings.totp.adminToken') }}</label><input id="revoke-admin-token" ref="revokeInput" v-model="adminToken" type="password" autocomplete="off" required />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<template>
<div class="dialog-backdrop" role="presentation">
<section data-action="device-approval-dialog" class="dialog-panel device-approval-dialog" role="dialog" aria-modal="true" aria-labelledby="device-approval-title">
<header><div><p class="eyebrow">Client authorization</p><h2 id="device-approval-title">{{ t('settings.clients.authorizeNew') }}</h2></div><button class="icon-button icon-only" type="button" :aria-label="t('common.close')" :title="t('common.close')" @click="emit('closed')">×</button></header>
<header><div><p class="eyebrow">{{ t('settings.deviceApproval.eyebrow') }}</p><h2 id="device-approval-title">{{ t('settings.clients.authorizeNew') }}</h2></div><button class="icon-button icon-only" type="button" :aria-label="t('common.close')" :title="t('common.close')" @click="emit('closed')">×</button></header>
<form v-if="preview === null" data-action="lookup-device-approval" class="security-form" @submit.prevent="lookup()">
<label for="device-approval-code">{{ t('settings.deviceApproval.codeLabel') }}</label>
<input id="device-approval-code" v-model="userCode" inputmode="text" autocomplete="off" placeholder="ABCD-EFGH" required />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ describe('ServerConnectionPanel', () => {
global: { plugins: [createClientUi(runtime)] },
})

expect(wrapper.get('.eyebrow').text()).toBe('Server')
expect(wrapper.get('.eyebrow').text()).toBe('服务连接')
expect(wrapper.get('#server-heading').text()).toBe('中继服务器')
expect(wrapper.get('[data-server-label]').text()).toContain('服务网址')
const field = wrapper.get('[data-server-field]')
Expand Down
Loading
Loading