Merge origin/main into capsule-windows-shadow-inset

This commit is contained in:
weikeyi committed 2026-05-05 17:37:50 +08:00
commit 351f956b1e
9 files changed
+547 -17

No files matched your search

+18 -12
View File
@@ -15,9 +15,14 @@ interface AudioBarsProps {
function AudioBars({ level }: AudioBarsProps) {
const envelope = [0.55, 0.85, 1.0, 0.85, 0.55];
const base = 4;
const max = 18;
const base = 2;
const max = 24;
const voice = Math.min(1, Math.max(0, level));
const silenceGate = 0.012;
const responseCeiling = 0.34;
const gatedVoice = Math.min(1, Math.max(0, (voice - silenceGate) / (responseCeiling - silenceGate)));
const easedVoice = gatedVoice * gatedVoice * (3 - 2 * gatedVoice);
const visualVoice = Math.pow(easedVoice, 0.42);
return (
<div
style={{
@@ -35,10 +40,11 @@ function AudioBars({ level }: AudioBarsProps) {
style={{
display: 'inline-block',
width: 3,
height: base + (max - base) * voice * env,
height: base + (max - base) * visualVoice * env,
borderRadius: 999,
background: 'var(--ol-blue)',
opacity: 0.82,
transformOrigin: 'center',
transition: 'height 0.08s var(--ol-motion-quick)',
}}
/>
@@ -110,6 +116,8 @@ interface CircleButtonProps {
function CircleButton({ variant, enabled, onClick }: CircleButtonProps) {
const { t } = useTranslation();
const isCancel = variant === 'cancel';
const os = detectOS();
const useBackdrop = os !== 'win' && isCancel;
return (
<button
onClick={enabled ? onClick : undefined}
@@ -120,8 +128,8 @@ function CircleButton({ variant, enabled, onClick }: CircleButtonProps) {
height: 28,
borderRadius: 999,
background: isCancel ? 'rgba(255, 255, 255, 0.55)' : 'rgba(255, 255, 255, 0.92)',
backdropFilter: isCancel ? 'blur(12px) saturate(160%)' : 'none',
WebkitBackdropFilter: isCancel ? 'blur(12px) saturate(160%)' : 'none',
backdropFilter: useBackdrop ? 'blur(12px) saturate(160%)' : 'none',
WebkitBackdropFilter: useBackdrop ? 'blur(12px) saturate(160%)' : 'none',
color: 'var(--ol-ink)',
border: '0.8px solid rgba(0, 0, 0, 0.08)',
display: 'inline-flex',
@@ -129,6 +137,7 @@ function CircleButton({ variant, enabled, onClick }: CircleButtonProps) {
justifyContent: 'center',
cursor: enabled ? 'default' : 'not-allowed',
opacity: enabled ? 1 : 0.42,
visibility: 'visible',
flexShrink: 0,
padding: 0,
boxShadow: '0 1px 2px rgba(0, 0, 0, 0.06)',
@@ -222,9 +231,7 @@ function Pill({ os, state, level, insertedChars, message, onCancel, onConfirm }:
const ambient = state === 'recording' ? Math.min(1, Math.max(0, level)) : 0;
const scale = os === 'win' ? 1 : 1 + ambient * 0.018;
const shadowAlpha = 0.20 + ambient * 0.10;
const dropShadow = os === 'win'
? `drop-shadow(0 12px 24px rgba(0, 0, 0, ${(0.15 + ambient * 0.06).toFixed(3)}))`
: 'none';
const useBackdrop = os !== 'win';
return (
<div
@@ -239,11 +246,11 @@ function Pill({ os, state, level, insertedChars, message, onCancel, onConfirm }:
boxSizing: metrics.boxSizing,
borderRadius: 999,
background: 'rgba(255, 255, 255, 0.62)',
backdropFilter: 'blur(28px) saturate(180%)',
WebkitBackdropFilter: 'blur(28px) saturate(180%)',
backdropFilter: useBackdrop ? 'blur(28px) saturate(180%)' : 'none',
WebkitBackdropFilter: useBackdrop ? 'blur(28px) saturate(180%)' : 'none',
border: '1px solid rgba(255, 255, 255, 0.55)',
boxShadow: os === 'win'
? '0 0 0 0.5px rgba(0, 0, 0, 0.08), inset 0 0.5px 0 rgba(255, 255, 255, 0.55)'
? `0 10px 24px -14px rgba(0, 0, 0, ${(0.24 + ambient * 0.06).toFixed(3)}), 0 0 0 0.5px rgba(0, 0, 0, 0.08), inset 0 0.5px 0 rgba(255, 255, 255, 0.55)`
: `0 18px 50px -10px rgba(0, 0, 0, ${shadowAlpha.toFixed(3)}), 0 0 0 0.5px rgba(0, 0, 0, 0.08), inset 0 0.5px 0 rgba(255, 255, 255, 0.55)`,
color: 'var(--ol-ink)',
fontFamily: 'var(--ol-font-sans)',
@@ -251,7 +258,6 @@ function Pill({ os, state, level, insertedChars, message, onCancel, onConfirm }:
transformOrigin: 'center',
transition: 'transform 0.08s var(--ol-motion-quick), box-shadow 0.08s var(--ol-motion-quick)',
willChange: 'transform, box-shadow',
filter: dropShadow,
}}
>
<CircleButton variant="cancel" enabled={enabled} onClick={onCancel} />
@@ -333,6 +333,7 @@ function LanguagePicker() {
>
<option value={FOLLOW_SYSTEM}>{t('settings.language.followSystem')}</option>
<option value="zh-CN">{t('settings.language.zh')}</option>
<option value="zh-TW">{t('settings.language.zhTW')}</option>
<option value="en">{t('settings.language.en')}</option>
</select>
);
+1
View File
@@ -391,6 +391,7 @@ export const en: typeof zhCN = {
labelDesc: 'Choose "Follow system" to match the OS language at launch.',
followSystem: 'Follow system',
zh: '简体中文',
zhTW: '繁體中文',
en: 'English',
restartHint: 'Some native menus (system tray, etc.) may require an app restart to fully switch.',
},
+8 -3
View File
@@ -12,8 +12,9 @@ import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import { en } from './en';
import { zhCN } from './zh-CN';
import { zhTW } from './zh-TW';
export const SUPPORTED_LOCALES = ['zh-CN', 'en'] as const;
export const SUPPORTED_LOCALES = ['zh-CN', 'zh-TW', 'en'] as const;
export type SupportedLocale = (typeof SUPPORTED_LOCALES)[number];
export const LOCALE_STORAGE_KEY = 'ol.locale';
@@ -22,14 +23,17 @@ const FOLLOW_SYSTEM_VALUE = 'system';
function detectSystemLocale(): SupportedLocale {
if (typeof navigator === 'undefined') return 'zh-CN';
const nav = (navigator.language || '').toLowerCase();
if (nav.startsWith('zh')) return 'zh-CN';
if (nav.startsWith('zh')) {
if (nav.includes('hant') || nav.includes('tw') || nav.includes('hk') || nav.includes('mo')) return 'zh-TW';
return 'zh-CN';
}
return 'en';
}
function getStoredLocale(): SupportedLocale | null {
if (typeof window === 'undefined') return null;
const raw = window.localStorage.getItem(LOCALE_STORAGE_KEY);
return raw === 'zh-CN' || raw === 'en' ? raw : null;
return raw === 'zh-CN' || raw === 'zh-TW' || raw === 'en' ? raw : null;
}
const initialLng: SupportedLocale = getStoredLocale() ?? detectSystemLocale();
@@ -37,6 +41,7 @@ const initialLng: SupportedLocale = getStoredLocale() ?? detectSystemLocale();
void i18n.use(initReactI18next).init({
resources: {
'zh-CN': { translation: zhCN },
'zh-TW': { translation: zhTW },
en: { translation: en },
},
lng: initialLng,
+1
View File
@@ -389,6 +389,7 @@ export const zhCN = {
labelDesc: '选择「跟随系统」时按操作系统当前语言显示。',
followSystem: '跟随系统',
zh: '简体中文',
zhTW: '繁體中文',
en: 'English',
restartHint: '部分原生菜单(系统托盘等)可能需要重启 App 才会切换。',
},
+515
View File
@@ -0,0 +1,515 @@
import type { zhCN } from './zh-CN';
// 繁體中文資源 — 與產品當前文案保持一致。
// 新增 key 時,必須同步更新 en.ts,避免切換到 English 後出現中文殘留。
export const zhTW: typeof zhCN = {
app: {
name: 'OpenLess',
tagline: '自然說話,完美書寫',
},
common: {
loading: '加載中…',
refresh: '刷新',
clear: '清空',
copy: '複製',
delete: '刪除',
later: '稍後',
cancel: '取消',
close: '關閉',
show: '顯示',
hide: '隱藏',
saved: '已保存',
saving: '保存中',
copied: '已複製',
operationFailed: '操作失敗',
add: '添加',
durationSeconds: '{{value}} 秒',
durationMinutes: '{{value}} 分鐘',
},
capsule: {
thinking: '正在思考中',
cancelled: '已取消',
error: '出錯了',
inserted: '已插入 {{count}}',
translating: '正在翻譯',
},
qa: {
thinking: '思考中…',
error: '出錯了,請稍後再試。',
errorRetry: '重試',
errorRetryHint: '再按 {{recordHotkey}} 重新提問。',
pinTooltip: '固定(不自動關閉)',
unpinTooltip: '取消固定',
closeTooltip: '關閉',
selectionPreview: '基於選中文本:',
emptyTitle: '按 {{recordHotkey}} 開始提問',
emptyDesc: '在任意 app 選中一段文字後,按一次 {{recordHotkey}} 開始錄音,再按一次結束並提交。回答會顯示在這裏,可以連續多輪追問。',
recordingHint: '錄音中…再按一次 {{recordHotkey}} 結束並提問',
statusIdle: '按 {{recordHotkey}} 提問',
statusRecording: '錄音中',
statusThinking: '思考中',
statusError: '出錯了',
},
nav: {
overview: '概覽',
history: '歷史',
vocab: '詞彙表',
style: '風格',
translation: '翻譯',
selectionAsk: '劃詞追問',
},
shell: {
shortcutLabel: '錄音快捷鍵',
shortcutHint: '開始 / 停止',
betaTag: 'BETA',
betaNote: '所有數據都只保存在本機。',
footer: {
account: '賬戶',
feedback: '反饋',
settings: '設置',
help: '幫助',
version: '版本 {{version}}',
helpPopover: {
tagline: '本地驅動的語音輸入層',
releaseNotes: '查看發佈日誌 ↗',
docs: '幫助中心 ↗',
},
},
providerPrompt: {
title: '設置語音提供商',
body: '還沒有配置 ASR 或 LLM 提供商,語音輸入和潤色暫時無法正常工作。',
later: '稍後',
openSettings: '去設置',
},
hotkeyModePrompt: {
title: '檢查錄音方式',
body: '本版本默認改爲“切換式說話”。如果你之前改過快捷鍵觸發方式,請到“錄音”裏手動確認一次。本次更新同時調整了快捷鍵方式的讀取邏輯;如果你更習慣按住說話,可以重新切回“按住說話”。',
later: '稍後提醒',
openSettings: '去錄音設置',
},
},
onboarding: {
welcome: '歡迎使用 OpenLess',
intro: '本地說出,本地落字。開始前需要兩個系統權限。',
accessibilityTitle: '輔助功能',
hotkeyTitle: '全局快捷鍵',
accessibilityDesc: '用於監聽全局快捷鍵(默認 {{trigger}})並把識別結果寫入光標位置。',
hotkeyDesc: '用於確認全局快捷鍵監聽可用。',
micTitle: '麥克風',
micDesc: '用於捕獲你的語音輸入。',
actionNotApplicable: '無需授權',
actionGranted: '已授權',
actionOpenSystem: '打開系統設置',
actionGrant: '授權',
actionRequestMic: '彈出授權',
accessibilityHint: '授權後必須**完全退出 OpenLess** 再重新打開(macOS TCC 規則)。',
footerHint: '授權全部完成後此引導自動關閉。如果一直不消失,從菜單欄 OpenLess → 退出,重新打開 App。',
},
overview: {
kicker: 'DASHBOARD',
title: '今日概覽',
desc: '本地說出,本地落字。下面是你今日的口述節奏與系統狀態。',
pressPrefix: '按',
pressSuffix: '開始錄音',
asrKind: 'ASR 語音',
llmKind: 'LLM 模型',
asrName: '火山引擎',
asrSubname: 'bigmodel',
llmName: 'OpenAI 兼容',
llmConfigured: '已配置 active LLM',
llmNotConfigured: '未配置',
statusConfigured: '已配置',
statusNotConfigured: '未配置',
metricChars: '今日字數',
metricSegments: '{{count}} 段',
metricDuration: '今日總時長',
metricAvg: '平均段落',
metricAvgTrend: '今日均值',
metricNoData: '暫無數據',
metricTotal: '累計記錄',
metricTotalTrend: '本機存檔 (上限 200)',
weekTitle: '近 7 天',
weekUnit: '條數 / 天',
recentTitle: '最近識別',
recentAll: '全部記錄 →',
recentEmpty: '還沒有記錄。按 {{trigger}} 開始第一次錄音。',
weekDays: ['日', '一', '二', '三', '四', '五', '六'],
},
history: {
kicker: 'HISTORY',
title: '歷史記錄',
desc: '最近的識別結果只保存在本機。左側爲時間線,右側爲原文與潤色對比。',
filterAll: '全部',
summary: '共 {{total}} 條 · 顯示 {{shown}}',
empty: '還沒有歷史記錄。按 {{trigger}} 錄一段試試。',
rawLabel: '原文',
rawEmpty: '(空)',
selectHint: '左側選一條查看詳情。',
insertedTo: '插入到',
chars: '{{count}} 字',
vocabHits: '{{count}} 個熱詞',
inserted: '已插入',
pasteSent: '已嘗試粘貼',
copiedFallback: '已複製(需 {{shortcut}})',
insertFailed: '插入失敗',
confirmClear: '確定清空全部 {{count}} 條記錄?此操作不可恢復。',
},
vocab: {
kicker: 'VOCABULARY',
title: '詞彙表',
desc: '告訴模型識別前可能出現的詞——生詞、新詞或專業詞彙。同時進入 ASR 熱詞與後期模型上下文。',
placeholder: '輸入詞語,按 Enter 或點添加…',
tip: '支持中英混合 · 數字開頭按字面識別 · 命中次數自動計數',
loadFailed: '加載失敗:{{err}}',
empty: '還沒有詞條。在上面輸入一個生詞或專業術語,讓模型在聽寫時優先匹配。',
tipDisabled: '點擊禁用此詞條',
tipEnabled: '點擊啓用此詞條',
removeAria: '刪除',
presets: {
title: '場景預設',
tip: '可多選後批量啓用;支持編輯和新建,已爲後續導入導出預留本地結構。',
create: '新建預設',
apply: '啓用所選',
save: '保存預設',
edit: '編輯 {{name}}',
newPreset: '新預設',
namePlaceholder: '預設名稱',
wordsPlaceholder: '詞條(用逗號或換行分隔)',
},
},
style: {
kicker: 'STYLE',
title: '輸出風格',
desc: '選擇默認風格用於全局錄音。每張卡可單獨啓停;啓停的風格不會出現在歷史記錄的「重新潤色」切換中。',
masterToggle: '整體啓用',
currentDefault: '當前默認',
ariaSetDefault: '設爲默認',
modes: {
raw: { name: '原文', desc: '只補標點和必要分句,不改寫不擴寫。', sample: '保留原始口語;嗯、那個等口癖會被去除,但不會重組語句。' },
light: { name: '輕度潤色', desc: '去口癖、補標點,整理爲可發送的自然文字。', sample: '讓轉寫聽起來不像念稿——保留語氣和表達習慣,但行文流暢。' },
structured: { name: '清晰結構', desc: '多個主題或步驟時,自動組織爲分點列表。', sample: '1. 主題一\na. 要點\nb. 要點\n2. 主題二\na. 要點\nb. 要點' },
formal: { name: '正式表達', desc: '工作溝通和郵件場景,更專業更完整。', sample: '郵件場景自動識別問候 / 落款;不引入空泛客套。' },
},
},
translation: {
kicker: 'TRANSLATION',
title: '翻譯',
desc: '把口述的內容自動翻譯成目標語言後再插入。目標語言、工作語言、觸發方式都在這裏配置。',
statusEnabled: '已啓用',
statusDisabled: '未啓用',
working: {
title: '工作語言',
desc: '勾選你日常會用到的語言(多選)。這組語言會作爲前提注入 LLM 的 system prompt 頭部,影響潤色與翻譯的判斷(專名拼寫、語氣、行文習慣)。',
},
target: {
title: '翻譯目標語言',
desc: '選了某個語言後,錄音過程中任意時刻按一下 Shift,停止後就會把轉寫翻譯成該語言再插入到光標位置。選「不啓用」則 Shift 沒有任何效果,走普通潤色管線。',
disabled: '不啓用(Shift 按下不觸發翻譯)',
},
howto: {
title: '使用方法',
step1: '在另一個 app 的輸入框裏聚焦光標(備忘錄、郵件、聊天窗口都行)。',
step2: '按一下"錄音快捷鍵"(當前是 {{trigger}}),開始錄音。',
step3: '在錄音過程中任意時刻按一下 Shift——按一下即可,不需要按住,可以在開口前、說到一半、快說完時按。',
step4: '再按一下"錄音快捷鍵"停止錄音。',
step5: '系統會把轉寫交給大模型翻譯成上面選的目標語言,然後插入到一開始那個輸入框光標位置。',
indicatorTitle: '怎麼知道翻譯模式生效了',
indicatorDesc: '一旦按下 Shift,屏幕底部錄音膠囊的上方會立刻懸浮一個藍色"● 正在翻譯"小藥丸——它會一直顯示到本次插入完成,讓你確認這次輸出會走翻譯管線。',
fallbackTitle: '安全兜底',
fallbackDesc: '翻譯模式選「不啓用」時 Shift 是沒作用的;翻譯過程中如果大模型調用失敗,會回退到把原始中文轉寫直接插入,不會丟字。詳見 issue #4。',
},
},
selectionAsk: {
kicker: 'SELECTION ASK',
title: '劃詞追問',
desc: '選中任意 app 裏的一段文字,按 {{hotkey}} 彈出浮窗,再按 {{recordHotkey}} 錄音提問。支持多輪追問,浮窗一直保留直到你手動關。',
statusEnabled: '已啓用',
statusDisabled: '未啓用',
hotkey: {
title: '彈出浮窗的快捷鍵',
desc: '只決定「打開 / 關閉」浮窗。浮窗裏錄音 / 提問統一用 {{recordHotkey}}(與你的主聽寫鍵複用)。選「不啓用」則關閉整個功能。',
optionDisabled: '不啓用',
chordWarning: '',
},
history: {
title: '保存歷史',
desc: '勾上則把每次追問的「選中文本 + 你的語音問題 + AI 答案」寫入本地存檔(不上雲)。默認關,關閉時浮窗一關問答即遺忘,更注重隱私。',
},
howto: {
title: '使用方法',
step1: '按「{{hotkey}}」在任意時刻打開浮窗(不需要先選文字)。',
step2: '在任意 app(瀏覽器、Mail、IDE、PDF reader…)裏選中一段文字。',
step3: '按一下 **{{recordHotkey}}**——開始錄音;再按一下 {{recordHotkey}},停止並提交,AI 答案顯示在浮窗裏。',
step4: '同一個浮窗裏可繼續多輪追問:再按 {{recordHotkey}} 錄音 → 再按 {{recordHotkey}} 提交。可以重新選文字讓下一輪帶新選區,也可以不選直接對話。',
step5: '按 Esc 或浮窗右上角 ✕ 關閉,關閉即清空所有多輪歷史。再按「{{hotkey}}」就是一段新的對話。',
windowTitle: '浮窗位置 + 拖動 + 釘住',
windowDesc: '浮窗第一次打開在屏幕底部錄音膠囊正上方;標題欄可拖動,移到任意位置後下一次打開會保留位置(同一次啓動期間)。右上角 📌 釘住時即使重新提問也保留窗口;不釘住按 Esc 即關。',
privacyTitle: '隱私契約',
privacyDesc: '選中的文本只在內存裏活到浮窗關閉,**絕不**寫入歷史存檔(保存歷史開關只控制問答 metadata);超過 4000 字符會截首+尾各 2000 後再上送大模型,避免泄露太多。LLM 調用走你已配的 ARK / DeepSeek 等 OpenAI 兼容 endpoint。',
},
},
settings: {
kicker: 'SETTINGS',
title: '設置',
desc: '錄音方式、模型與語音提供商、快捷鍵、權限與關於信息——全部在這裏。',
sections: {
recording: '錄音',
providers: '提供商',
shortcuts: '快捷鍵',
permissions: '權限',
language: '語言',
about: '關於',
},
recording: {
title: '錄音',
desc: '定義全局錄音的快捷鍵與觸發方式。',
hotkeyLabel: '錄音快捷鍵',
hotkeyDescAcc: '按下即開始捕獲語音,全局生效。需要授予輔助功能權限。',
hotkeyDescNoAcc: '按下即開始捕獲語音,全局生效。無需額外輔助功能授權。',
modeLabel: '錄音方式',
modeDesc: '切換式 = 按一次開始、再按一次結束;按住說話 = 按住開始、鬆開結束。',
modeToggle: '切換式',
modeHold: '按住說話',
migrationNoticeTitle: '默認已改爲切換式說話',
migrationNoticeDesc: '如果你之前改過快捷鍵觸發方式,請在這裏手動確認一次。本次更新調整了快捷鍵方式的默認值與讀取邏輯;如果你更習慣按住說話,可以重新切回“按住說話”。',
capsuleLabel: '錄音膠囊',
capsuleDesc: '錄音 / 轉寫時在屏幕底部顯示半透明膠囊。',
restoreClipboardLabel: '插入後恢復剪貼板',
restoreClipboardDesc: '僅 Windows / Linux:粘貼成功後恢復你原來的剪貼板內容(默認開)。關掉就把聽寫文本留在剪貼板,模擬粘貼沒真正落地時可以手動 Ctrl+V 找回。詳見 issue #111。',
allowNonTsfFallbackLabel: '允許非 TSF 兜底',
allowNonTsfFallbackDesc: '僅 Windows:TSF 直接上屏失敗後,允許改用 Unicode SendInput、快捷鍵粘貼或 WM_PASTE。關閉後可驗證是否真實使用 TSF 輸入。',
startupAtBoot: '開機自啓',
startupAtBootDesc: '登錄後自動啓動 OpenLess。macOS 寫 LaunchAgent,Linux 寫 ~/.config/autostart,Windows 寫 HKCU\\Run(不需要管理員)。詳見 issue #194。',
startupAtBootError: '開機自啓切換失敗:{{message}}',
},
providers: {
llmTitle: 'LLM 模型(潤色)',
llmDesc: 'OpenAI 兼容協議,支持多家供應商切換。',
providerLabel: '供應商',
llmProviderDesc: '選擇後將自動填入 Base URL 默認值。',
asrProviderDesc: '切換後將自動選用對應憑據。',
asrTitle: 'ASR 語音(轉寫)',
asrDesc: '用於將口述實時轉寫爲文本。',
presets: {
ark: 'ARK(火山方舟)',
deepseek: 'DeepSeek',
siliconflow: '硅基流動',
openai: 'OpenAI',
custom: '自定義',
asrVolcengine: '火山引擎 bigasr',
asrSiliconflow: '硅基流動 SenseVoice',
asrZhipu: '智譜 GLM-ASR',
asrGroq: 'Groq Whisper-large-v3',
asrWhisper: 'OpenAI Whisper(兼容)',
},
volcengineAppKeyLabel: 'APP ID',
volcengineAccessKeyLabel: 'Access Token',
volcengineResourceIdLabel: 'Resource ID',
volcengineMappingNote: 'Secret Key 當前無需填寫。Resource ID 默認使用 volc.bigasr.sauc.duration。',
fillDefault: '填入默認值',
readFailed: '讀取失敗',
apiKeyLabel: 'API 密鑰',
baseUrlLabel: '接口地址',
modelLabel: '模型',
appIdLabel: 'App ID(應用 ID)',
accessKeyLabel: 'Access Key',
resourceIdLabel: '資源 ID',
toolsLabel: '連接檢查',
toolsDesc: '先保存上方配置,再驗證當前模型連通性或拉取模型;失敗時仍可手動填寫模型 ID。',
validate: '驗證',
validating: '驗證中…',
fetchModels: '拉取模型',
loadingModels: '拉取模型中…',
modelMissing: '未配置模型,請先填寫模型 ID。',
modelsEmpty: '鑑權成功,但沒有返回可用模型。',
modelsLoaded: '已拉取 {{count}} 個模型。',
selectModel: '選擇一個模型寫入上方字段',
modelSaved: '已保存模型 {{model}}。',
validateSuccess: '連接檢查通過。',
providerHttpStatus: '供應商接口返回 {{status}},請檢查 API Key 權限或 Endpoint。',
endpointMustUseHttps: 'Endpoint 必須使用 HTTPS(本地 localhost/127.0.0.1 測試除外)。',
endpointInvalid: 'Endpoint 格式不合法。',
responseTooLarge: '供應商響應過大,已停止驗證以保證安全。',
asrInvalidJson: 'ASR 響應不是有效 JSON。',
asrMissingTextField: 'ASR 響應缺少 text 字段。',
apiKeyMissing: 'API Key 爲空。',
endpointMissing: 'Endpoint 爲空。',
requestTimeout: '請求超時,請稍後重試。',
},
shortcuts: {
title: '快捷鍵速查',
descAcc: '所有快捷鍵全局生效,需要在權限設置中開啓輔助功能。',
descNoAcc: '所有快捷鍵全局生效。若無響應,請在權限頁查看全局快捷鍵監聽狀態。',
startStop: '開始 / 停止錄音',
cancel: '取消本次錄音',
confirm: '膠囊確認插入',
switchStyle: '切換上一次風格',
openApp: '打開 OpenLess',
confirmHint: '點擊右側 ✓',
notSupported: '暫未支持',
},
permissions: {
title: '權限',
descAcc: 'OpenLess 需要以下系統權限才能正常工作。授權後通常需要完全退出 App 重啓一次才生效。',
descNoAcc: 'OpenLess 需要麥克風可用,並依賴全局快捷鍵監聽狀態判斷 native hook 是否正常工作。',
micLabel: '麥克風',
micDesc: '用於捕獲你的語音輸入。',
accLabel: '輔助功能',
accDesc: '用於監聽全局快捷鍵並將識別結果寫入光標位置。',
hotkeyLabel: '全局快捷鍵',
hotkeyDescWithAdapter: '當前適配器:{{adapter}}。用於判斷快捷鍵監聽是否已經安裝。',
hotkeyDescPlain: '用於判斷快捷鍵監聽是否已經安裝。',
networkLabel: '網絡',
networkDesc: '雲端 ASR / LLM 調用所必需。本地模式可關閉。',
networkOk: '可用',
checking: '檢查中…',
granted: '已授權',
notApplicable: '無需授權',
denied: '未授權',
indeterminate: '未確定',
openSystem: '打開系統設置',
grant: '授權',
hotkeyInstalled: '已安裝',
hotkeyStarting: '安裝中…',
hotkeyFailed: '監聽失敗',
windowsImeLabel: 'Windows 輸入法後端',
windowsImeDesc: '用於在語音會話期間臨時切換到 OpenLess TSF 輸入法,避免剪貼板插入限制。',
windowsImeInstalled: '已安裝',
windowsImeUnavailable: '不可用',
windowsIme: {
installed: '已安裝。語音輸入時會臨時切換到 OpenLess 輸入法。',
notInstalled: '未安裝。OpenLess 正在使用剪貼板 / WM_PASTE 兜底。',
registrationBroken: '註冊已損壞。請重新安裝 OpenLess 輸入法。',
notWindows: '僅 Windows 可用。',
},
},
language: {
title: '界面語言',
desc: '切換 UI 顯示語言。當前會話即時生效,下次啓動自動沿用。',
label: '語言',
labelDesc: '選擇「跟隨系統」時按操作系統當前語言顯示。',
followSystem: '跟隨系統',
zh: '簡體中文',
zhTW: '繁體中文',
en: 'English',
restartHint: '部分原生菜單(系統托盤等)可能需要重啓 App 纔會切換。',
},
about: {
tagline: '自然說話,完美書寫',
checkUpdate: '檢查更新',
checkUpdateBtn: '檢查',
checkingUpdate: '檢查中…',
upToDate: '當前已是最新版本。',
updateError: '檢查或更新失敗,請稍後重試。',
openReleases: '打開 Releases',
source: '源碼',
docs: '文檔',
feedback: '反饋',
qq: '社區 QQ 羣',
qqDesc: '使用 QQ 搜索羣號加入,或掃碼進羣。',
copyQq: '複製羣號',
privacy: '隱私',
privacyDesc: '所有識別結果僅保存在本機。雲端 API 僅用於實時轉寫與潤色,不會保留你的錄音。',
localFirst: '本地優先',
updateDialog: {
available: {
title: '發現新版本',
desc: '發現 OpenLess {{version}},是否現在更新?',
},
downloading: {
title: '正在下載更新',
desc: '正在下載 OpenLess {{version}},請保持應用打開。',
},
downloaded: {
title: '更新已準備好',
desc: 'OpenLess {{version}} 已安裝完成。是否現在自動重啓以應用更新?',
},
installing: {
title: '正在安裝更新',
desc: '正在安裝 OpenLess {{version}},請保持應用打開。',
},
install: '現在更新',
downloadingLabel: '下載中…',
installingLabel: '安裝中…',
later: '稍後手動重啓',
restartNow: '現在重啓',
progress: '{{progress}}% · {{downloaded}} / {{total}}',
progressUnknown: '已下載 {{downloaded}}',
},
},
},
modal: {
sections: {
account: '賬戶',
settings: '設置',
personalize: '個性化',
about: '關於',
helpCenter: '幫助中心',
releaseNotes: '版本說明',
},
account: {
localUser: '本地用戶',
localUserDesc: '未登錄 · 所有數據保存在本機',
loginSync: '登錄 / 同步',
footer: 'OpenLess 默認完全本地運行。登錄後可在多設備間同步詞彙表與風格預設,識別仍在本機或你配置的 Provider 上完成。',
},
personalize: {
appearance: '外觀',
appearanceDesc: '跟隨系統 / 淺色 / 深色',
appearanceSystem: '跟隨系統',
appearanceLight: '淺色',
appearanceDark: '深色',
language: '界面語言',
font: '字體大小',
fontDesc: '整體縮放界面字號,立即生效。',
fontSmall: '小',
fontMedium: '中',
fontLarge: '大',
blur: '毛玻璃強度',
blurDesc: '影響窗口內層 backdrop-filter 強度(macOS 系統磨砂層無法運行時調)。',
startupOpen: '啓動時打開',
startupOverview: '概覽',
startupLast: '上次位置',
startupAtBoot: '開機自啓',
},
about: {
tagline: '自然說話,完美書寫',
checkUpdate: '檢查更新',
checkUpdateBtn: '檢查',
docs: '文檔',
docsBtn: 'openless.app/docs ↗',
feedback: '反饋渠道',
feedbackBtn: 'GitHub Issues ↗',
privacy: '隱私',
privacyDesc: '所有識別結果只保存在本機,雲端 API 僅用於實時調用。',
localFirst: '本地優先',
},
},
windowChrome: {
minimize: '最小化',
maximize: '最大化',
close: '關閉',
},
hotkey: {
triggers: {
rightOption: '右 Option',
leftOption: '左 Option',
rightControl: '右 Control',
leftControl: '左 Control',
rightCommand: '右 Command',
fn: 'Fn (地球鍵)',
rightAlt: '右 Alt',
},
fallback: '全局快捷鍵',
modeHoldSuffix: '(按住說話)',
modeToggleSuffix: '(開始 / 停止)',
usageHold: '按住 {{trigger}} 說話,鬆開結束。',
usageToggle: '按 {{trigger}} 開始錄音,再按一次結束。',
adapter: {
macEventTap: 'macOS Event Tap',
windowsLowLevel: 'Windows 低層鍵盤 hook',
rdev: 'rdev 監聽器',
},
},
};
@@ -13,7 +13,7 @@ function assertEqual<T>(actual: T, expected: T, name: string) {
const winMetrics = getCapsulePillMetrics('win');
assertEqual(winMetrics.width, 196, 'windows capsule widens pill');
assertEqual(winMetrics.height, 52, 'windows capsule increases pill height');
assertEqual(winMetrics.textWidth, 118, 'windows capsule widens text slot');
assertEqual(winMetrics.textWidth, 104, 'windows capsule keeps side controls clear');
assertEqual(winMetrics.boxSizing, 'border-box', 'windows capsule pill width is an outer border-box metric');
const winHost = getCapsuleHostMetrics('win', false);
+1 -1
View File
@@ -26,7 +26,7 @@ export interface CapsuleMessageLayout {
export function getCapsulePillMetrics(os: OS): CapsulePillMetrics {
if (os === 'win') {
// Windows metrics describe the visible outer footprint of the pill.
return { width: 196, height: 52, textWidth: 118, boxSizing: 'border-box' };
return { width: 196, height: 52, textWidth: 104, boxSizing: 'border-box' };
}
return { width: 176, height: 42, textWidth: 84, boxSizing: 'content-box' };
+1
View File
@@ -1096,6 +1096,7 @@ function LanguageSection() {
>
<option value={FOLLOW_SYSTEM}>{t('settings.language.followSystem')}</option>
<option value="zh-CN">{t('settings.language.zh')}</option>
<option value="zh-TW">{t('settings.language.zhTW')}</option>
<option value="en">{t('settings.language.en')}</option>
</select>
</SettingRow>