diff --git a/.agents/skills/i18n/SKILL.md b/.agents/skills/i18n/SKILL.md new file mode 100644 index 0000000..b7613fb --- /dev/null +++ b/.agents/skills/i18n/SKILL.md @@ -0,0 +1,48 @@ +--- +name: i18n +description: Implement and review Cursor BYOK desktop localization, including t() usage, locale catalogs, system-language selection, translation completeness, and language settings under apps/desktop. +--- + +# Desktop i18n + +Keep localization organized as: + +```text +apps/desktop/ +├── plugins/static-i18n-plugin.ts +└── src/i18n/ + ├── I18nRoot.tsx + ├── runtime.ts + ├── store.ts + ├── generated/catalog.json + └── locales/ + ├── zh-CN.json + └── en-US.json +``` + +## Author messages + +- Use the global `t()` function with a static Simplified Chinese string literal: `t("保存")`. +- Do not import or locally declare `t`. +- Keep calls inside render paths when the result must update after a language change. Do not translate module-level UI constants. +- Use named placeholders with a static object: `t("第 {page} 页", { page })`. +- Preserve every placeholder name exactly in all translations. +- Wrap all user-visible labels, descriptions, tooltips, empty states, validation messages, and accessibility labels. Do not wrap protocol tokens, model IDs, URLs, or product names that should remain unchanged. + +## Locale behavior + +- `system` is the default preference. +- Resolve supported operating-system languages to their locale and fall back to `en-US` for every unsupported language. +- Persist only explicit locale selections. Removing the preference restores system-language behavior. +- Apply changes immediately and update the document `lang` attribute. + +## Update catalogs + +From `apps/desktop`: + +1. Run `npm run i18n:scan` after adding or changing `t()` sources. +2. Translate every empty entry in `src/i18n/locales/en-US.json`. +3. Never edit `src/i18n/generated/catalog.json` or `zh-CN.json` manually; scanning owns them. +4. Run `npm run check`. A normal build must fail on a missing translation or mismatched placeholder. + +When adding another locale, add it to the plugin's supported locales, runtime locale type and messages, locale resolver, settings options, and provide a complete locale JSON file. diff --git a/apps/desktop/plugins/static-i18n-plugin.ts b/apps/desktop/plugins/static-i18n-plugin.ts index 81d2248..0623dc5 100644 --- a/apps/desktop/plugins/static-i18n-plugin.ts +++ b/apps/desktop/plugins/static-i18n-plugin.ts @@ -295,7 +295,8 @@ function mergeLocaleMessages( function validateTranslations( sourceRoot: string, - entries: Record + entries: Record, + requireComplete: boolean ): void { for (const locale of SUPPORTED_LOCALES) { if (locale === SOURCE_LOCALE) continue @@ -303,7 +304,14 @@ function validateTranslations( const messages = readJSONFile(localePath) for (const [id, entry] of Object.entries(entries)) { const translation = messages[id] - if (!translation) continue + if (!translation) { + if (requireComplete) { + throw new Error( + `[static-i18n] ${localePath}:${id} 缺少译文:${entry.source}` + ) + } + continue + } const actual = placeholderNames(translation) assertUniquePlaceholders(actual, `${localePath}:${id}`, translation) const missing = entry.placeholders.filter((name) => !actual.includes(name)) @@ -446,8 +454,8 @@ export function staticI18nPlugin(): Plugin { }, buildStart() { const catalog = collectCatalog(sourceRoot) - validateTranslations(sourceRoot, catalog.entries) if (shouldScan) syncCatalogFiles(sourceRoot, catalog) + validateTranslations(sourceRoot, catalog.entries, !shouldScan) }, transform(code, id) { if (!isSourceFile(id) || isExcludedFile(sourceRoot, id)) return null diff --git a/apps/desktop/src/api.ts b/apps/desktop/src/api.ts index 3853a58..486886e 100644 --- a/apps/desktop/src/api.ts +++ b/apps/desktop/src/api.ts @@ -1,4 +1,5 @@ import type { AdRuntime } from "./components/ads/types"; +import type { Locale } from "./i18n/runtime"; export type ProviderType = "openai-chat" | "openai-responses" | "anthropic"; @@ -221,9 +222,14 @@ async function request(path: string, init?: RequestInit): Promise { } export const api = { - ads: (disabledAdIds: Iterable) => { + ads: (disabledAdIds: Iterable, locale: Locale) => { const value = [...disabledAdIds].join(","); - return request("/ads", value ? { headers: { "disable-ad-ids": value } } : undefined); + return request("/ads", { + headers: { + "accept-language": locale, + ...(value ? { "disable-ad-ids": value } : {}), + }, + }); }, dismissAd: (id: string, reason: string) => request(`/ads/${encodeURIComponent(id)}/dismissals`, { method: "POST", body: JSON.stringify({ reason }) }), providers: () => request("/providers"), diff --git a/apps/desktop/src/components/overview/OverviewTimeRangeFilter.tsx b/apps/desktop/src/components/overview/OverviewTimeRangeFilter.tsx index 6f72d32..6cc03a4 100644 --- a/apps/desktop/src/components/overview/OverviewTimeRangeFilter.tsx +++ b/apps/desktop/src/components/overview/OverviewTimeRangeFilter.tsx @@ -11,14 +11,6 @@ import styles from "./OverviewTimeRangeFilter.module.scss"; export type OverviewRangePreset = "ten-minutes" | "hour" | "today" | "week" | "month" | "custom"; -const presets: Array<{ value: Exclude; label: string }> = [ - { value: "hour", label: "近1小时" }, - { value: "today", label: "近1自然日" }, - { value: "ten-minutes", label: "近10分钟" }, - { value: "week", label: "近一周" }, - { value: "month", label: "近一个月" }, -]; - export function OverviewTimeRangeFilter({ value, customOpen, customStart, customEnd, modelOptions, providerOptions, selectedModels, selectedProviders, busy, onSelect, onCustomOpenChange, onCustomStartChange, onCustomEndChange, onSelectedModelsChange, onSelectedProvidersChange, onCustomApply, onRefresh }: { value: OverviewRangePreset; customOpen: boolean; @@ -38,6 +30,13 @@ export function OverviewTimeRangeFilter({ value, customOpen, customStart, custom onCustomApply: () => void; onRefresh: () => void; }) { + const presets: Array<{ value: Exclude; label: string }> = [ + { value: "hour", label: t("近1小时") }, + { value: "today", label: t("近1自然日") }, + { value: "ten-minutes", label: t("近10分钟") }, + { value: "week", label: t("近一周") }, + { value: "month", label: t("近一个月") }, + ]; const customButton = useRef(null); const popover = useRef(null); const popoverId = useId(); diff --git a/apps/desktop/src/components/ui/icons.ts b/apps/desktop/src/components/ui/icons.ts index 4b8bef7..fed1d0b 100644 --- a/apps/desktop/src/components/ui/icons.ts +++ b/apps/desktop/src/components/ui/icons.ts @@ -6,7 +6,7 @@ const icon = (body: string, width = 24, height = 24): IconifyIcon => ({ body, wi export const flatColorAreaChartIcon = icon('', 48, 48); export const flatColorSalesPerformanceIcon = icon('', 48, 48); -export const flatColorLinkIcon = icon('', 48, 48); +export const flatColorAboutIcon = icon('', 48, 48); export const flatColorOrganizationIcon = icon('', 48, 48); export const flatColorBarChartIcon = icon('', 48, 48); export const flatColorComboChartIcon = icon('', 48, 48); diff --git a/apps/desktop/src/i18n/I18nRoot.tsx b/apps/desktop/src/i18n/I18nRoot.tsx new file mode 100644 index 0000000..d290fec --- /dev/null +++ b/apps/desktop/src/i18n/I18nRoot.tsx @@ -0,0 +1,7 @@ +import { App } from "../App"; +import { useI18n } from "./store"; + +export function I18nRoot() { + useI18n(); + return ; +} diff --git a/apps/desktop/src/i18n/generated/catalog.json b/apps/desktop/src/i18n/generated/catalog.json index 29f12be..4180be5 100644 --- a/apps/desktop/src/i18n/generated/catalog.json +++ b/apps/desktop/src/i18n/generated/catalog.json @@ -87,7 +87,7 @@ }, { "file": "pages/SettingsPage.tsx", - "line": 145, + "line": 148, "column": 82 } ] @@ -111,7 +111,7 @@ "refs": [ { "file": "pages/SettingsPage.tsx", - "line": 231, + "line": 254, "column": 14 } ] @@ -163,7 +163,7 @@ "refs": [ { "file": "pages/SettingsPage.tsx", - "line": 222, + "line": 245, "column": 16 } ] @@ -312,7 +312,7 @@ "refs": [ { "file": "pages/SettingsPage.tsx", - "line": 205, + "line": 228, "column": 26 } ] @@ -595,7 +595,7 @@ "refs": [ { "file": "pages/SettingsPage.tsx", - "line": 209, + "line": 232, "column": 60 } ] @@ -621,17 +621,17 @@ "refs": [ { "file": "pages/SettingsPage.tsx", - "line": 52, + "line": 55, "column": 42 }, { "file": "pages/SettingsPage.tsx", - "line": 150, + "line": 153, "column": 22 }, { "file": "pages/SettingsPage.tsx", - "line": 177, + "line": 180, "column": 58 } ] @@ -707,6 +707,18 @@ } ] }, + "217cfe7db1e3d10a": { + "source": "跟随系统", + "kind": "text", + "placeholders": [], + "refs": [ + { + "file": "pages/SettingsPage.tsx", + "line": 206, + "column": 43 + } + ] + }, "22c6b4eb4caee6ae": { "source": "代理设置已保存", "kind": "text", @@ -714,7 +726,7 @@ "refs": [ { "file": "pages/SettingsPage.tsx", - "line": 107, + "line": 110, "column": 15 } ] @@ -747,6 +759,18 @@ } ] }, + "29fbbef32a6eb58b": { + "source": "不再显示此广告", + "kind": "text", + "placeholders": [], + "refs": [ + { + "file": "layouts/AppLayout.tsx", + "line": 216, + "column": 14 + } + ] + }, "2a2773134a829016": { "source": "按历史 LLM 调用记录汇总,进行中的调用不计入。", "kind": "text", @@ -766,7 +790,7 @@ "refs": [ { "file": "components/overview/OverviewTimeRangeFilter.tsx", - "line": 115, + "line": 114, "column": 105 }, { @@ -777,7 +801,12 @@ { "file": "components/ui/Modal.tsx", "line": 7, - "column": 92 + "column": 96 + }, + { + "file": "layouts/AppLayout.tsx", + "line": 217, + "column": 19 }, { "file": "pages/CursorSettingsPage.tsx", @@ -786,12 +815,12 @@ }, { "file": "pages/SettingsPage.tsx", - "line": 141, + "line": 144, "column": 81 }, { "file": "pages/SettingsPage.tsx", - "line": 224, + "line": 247, "column": 21 } ] @@ -908,6 +937,18 @@ } ] }, + "36f33adaf0942634": { + "source": "确认", + "kind": "text", + "placeholders": [], + "refs": [ + { + "file": "layouts/AppLayout.tsx", + "line": 218, + "column": 20 + } + ] + }, "378bb0eec39fa8a2": { "source": "最后一页", "kind": "text", @@ -1012,7 +1053,7 @@ "refs": [ { "file": "pages/SettingsPage.tsx", - "line": 216, + "line": 239, "column": 14 } ] @@ -1067,6 +1108,23 @@ } ] }, + "3d13868593ae4eeb": { + "source": "界面语言", + "kind": "text", + "placeholders": [], + "refs": [ + { + "file": "pages/SettingsPage.tsx", + "line": 198, + "column": 22 + }, + { + "file": "pages/SettingsPage.tsx", + "line": 204, + "column": 26 + } + ] + }, "3da0bf1610ff5db5": { "source": "推荐内容", "kind": "text", @@ -1074,7 +1132,7 @@ "refs": [ { "file": "components/ads/AdMenu.tsx", - "line": 15, + "line": 18, "column": 56 } ] @@ -1091,6 +1149,18 @@ } ] }, + "4125fc7ba333524c": { + "source": "默认亮色", + "kind": "text", + "placeholders": [], + "refs": [ + { + "file": "pages/SettingsPage.tsx", + "line": 223, + "column": 52 + } + ] + }, "42655ed8e4108ae2": { "source": "输入(非缓存)", "kind": "text", @@ -1149,6 +1219,18 @@ } ] }, + "4510b63fdcf7451f": { + "source": "Cursor 助手 v0.1.0", + "kind": "text", + "placeholders": [], + "refs": [ + { + "file": "layouts/AppHeader.tsx", + "line": 20, + "column": 18 + } + ] + }, "470049252e54de6a": { "source": "成功占比:{rate}", "kind": "template", @@ -1215,6 +1297,18 @@ } ] }, + "493e097fac6bd217": { + "source": "上游配置", + "kind": "text", + "placeholders": [], + "refs": [ + { + "file": "layouts/AppLayout.tsx", + "line": 71, + "column": 48 + } + ] + }, "497c85690c4cc0fc": { "source": "暂无数据", "kind": "text", @@ -1239,8 +1333,13 @@ "refs": [ { "file": "components/ads/AdMenu.tsx", - "line": 25, - "column": 97 + "line": 28, + "column": 99 + }, + { + "file": "layouts/AppLayout.tsx", + "line": 197, + "column": 84 } ] }, @@ -1251,7 +1350,7 @@ "refs": [ { "file": "components/overview/OverviewTimeRangeFilter.tsx", - "line": 90, + "line": 89, "column": 9 }, { @@ -1266,6 +1365,18 @@ } ] }, + "51d04bc3d286f018": { + "source": "近1自然日", + "kind": "text", + "placeholders": [], + "refs": [ + { + "file": "components/overview/OverviewTimeRangeFilter.tsx", + "line": 35, + "column": 30 + } + ] + }, "5228358a6db59fe7": { "source": "如:现在、2026-08-23 18:00", "kind": "text", @@ -1273,7 +1384,7 @@ "refs": [ { "file": "components/overview/OverviewTimeRangeFilter.tsx", - "line": 111, + "line": 110, "column": 70 } ] @@ -1285,7 +1396,7 @@ "refs": [ { "file": "pages/SettingsPage.tsx", - "line": 139, + "line": 142, "column": 26 } ] @@ -1384,6 +1495,18 @@ } ] }, + "5f9acfb945229062": { + "source": "你确认不想再看到此广告吗?", + "kind": "text", + "placeholders": [], + "refs": [ + { + "file": "layouts/AppLayout.tsx", + "line": 222, + "column": 11 + } + ] + }, "5fd2ec5a6e9b654c": { "source": "合计:{cost}", "kind": "template", @@ -1479,7 +1602,7 @@ "refs": [ { "file": "components/overview/OverviewTimeRangeFilter.tsx", - "line": 116, + "line": 115, "column": 109 } ] @@ -1491,7 +1614,7 @@ "refs": [ { "file": "api.ts", - "line": 255, + "line": 265, "column": 43 } ] @@ -1575,6 +1698,18 @@ } ] }, + "68ea5dd4d7af20e6": { + "source": "系统设置", + "kind": "text", + "placeholders": [], + "refs": [ + { + "file": "layouts/AppLayout.tsx", + "line": 74, + "column": 47 + } + ] + }, "6a9906c79f26c0ba": { "source": "开始时间", "kind": "text", @@ -1582,7 +1717,7 @@ "refs": [ { "file": "components/overview/OverviewTimeRangeFilter.tsx", - "line": 110, + "line": 109, "column": 21 } ] @@ -1693,7 +1828,7 @@ "refs": [ { "file": "pages/SettingsPage.tsx", - "line": 129, + "line": 132, "column": 16 } ] @@ -1717,7 +1852,7 @@ "refs": [ { "file": "pages/SettingsPage.tsx", - "line": 192, + "line": 215, "column": 26 } ] @@ -1741,7 +1876,7 @@ "refs": [ { "file": "api.ts", - "line": 250, + "line": 260, "column": 43 } ] @@ -1765,7 +1900,7 @@ "refs": [ { "file": "pages/SettingsPage.tsx", - "line": 225, + "line": 248, "column": 22 } ] @@ -1808,7 +1943,7 @@ "refs": [ { "file": "pages/SettingsPage.tsx", - "line": 82, + "line": 85, "column": 15 } ] @@ -1868,7 +2003,7 @@ "refs": [ { "file": "api.ts", - "line": 213, + "line": 214, "column": 21 } ] @@ -2016,6 +2151,18 @@ } ] }, + "8df48894086d6fbd": { + "source": "原因(可选)", + "kind": "text", + "placeholders": [], + "refs": [ + { + "file": "layouts/AppLayout.tsx", + "line": 224, + "column": 16 + } + ] + }, "90800c48a1dd0655": { "source": "{label} 必须是 JSON 对象", "kind": "template", @@ -2047,6 +2194,18 @@ } ] }, + "91aaf184cfc17ffd": { + "source": "数据概览", + "kind": "text", + "placeholders": [], + "refs": [ + { + "file": "layouts/AppLayout.tsx", + "line": 69, + "column": 39 + } + ] + }, "91ee087c3d4f3220": { "source": "支持完整 HTTP(S) 地址或以 / 开头、与上游地址组合的相对路径。", "kind": "text", @@ -2193,11 +2352,35 @@ "refs": [ { "file": "components/overview/OverviewTimeRangeFilter.tsx", - "line": 110, + "line": 109, "column": 70 } ] }, + "9f6fee1aba17a565": { + "source": "语言", + "kind": "text", + "placeholders": [], + "refs": [ + { + "file": "pages/SettingsPage.tsx", + "line": 195, + "column": 26 + } + ] + }, + "9fb48101d237ff96": { + "source": "近一周", + "kind": "text", + "placeholders": [], + "refs": [ + { + "file": "components/overview/OverviewTimeRangeFilter.tsx", + "line": 37, + "column": 29 + } + ] + }, "a026f37e613cf48b": { "source": "输出 Token", "kind": "text", @@ -2217,7 +2400,7 @@ "refs": [ { "file": "pages/SettingsPage.tsx", - "line": 45, + "line": 48, "column": 34 } ] @@ -2259,11 +2442,11 @@ { "file": "components/ui/Modal.tsx", "line": 7, - "column": 115 + "column": 119 }, { "file": "pages/SettingsPage.tsx", - "line": 142, + "line": 145, "column": 133 } ] @@ -2372,7 +2555,7 @@ }, { "file": "components/overview/OverviewTimeRangeFilter.tsx", - "line": 113, + "line": 112, "column": 61 }, { @@ -2487,22 +2670,22 @@ "refs": [ { "file": "components/overview/OverviewTimeRangeFilter.tsx", - "line": 92, + "line": 91, "column": 28 }, { "file": "components/overview/OverviewTimeRangeFilter.tsx", - "line": 92, + "line": 91, "column": 89 }, { "file": "layouts/AppLayout.tsx", - "line": 140, + "line": 237, "column": 64 }, { "file": "layouts/AppLayout.tsx", - "line": 140, + "line": 237, "column": 125 } ] @@ -2593,7 +2776,7 @@ "refs": [ { "file": "pages/SettingsPage.tsx", - "line": 208, + "line": 231, "column": 22 } ] @@ -2610,6 +2793,18 @@ } ] }, + "b8c9b486c83b5778": { + "source": "不再显示广告", + "kind": "text", + "placeholders": [], + "refs": [ + { + "file": "components/ads/AdMenu.tsx", + "line": 44, + "column": 24 + } + ] + }, "b9670c85a4ab939e": { "source": "路由", "kind": "text", @@ -2678,6 +2873,30 @@ } ] }, + "bb7efdcb6af6e805": { + "source": "默认暗色", + "kind": "text", + "placeholders": [], + "refs": [ + { + "file": "pages/SettingsPage.tsx", + "line": 223, + "column": 40 + } + ] + }, + "bda62ce1d5e4ace9": { + "source": "可以告诉我们原因", + "kind": "text", + "placeholders": [], + "refs": [ + { + "file": "layouts/AppLayout.tsx", + "line": 229, + "column": 24 + } + ] + }, "bf57afd709694b55": { "source": "概览时间范围", "kind": "text", @@ -2685,7 +2904,7 @@ "refs": [ { "file": "components/overview/OverviewTimeRangeFilter.tsx", - "line": 74, + "line": 73, "column": 51 } ] @@ -2815,7 +3034,7 @@ "refs": [ { "file": "components/overview/OverviewTimeRangeFilter.tsx", - "line": 112, + "line": 111, "column": 61 } ] @@ -2892,12 +3111,12 @@ "refs": [ { "file": "pages/SettingsPage.tsx", - "line": 127, + "line": 130, "column": 22 }, { "file": "pages/SettingsPage.tsx", - "line": 133, + "line": 136, "column": 20 } ] @@ -2909,7 +3128,7 @@ "refs": [ { "file": "pages/SettingsPage.tsx", - "line": 164, + "line": 167, "column": 21 } ] @@ -2926,6 +3145,18 @@ } ] }, + "d0bfccc77315d887": { + "source": "近一个月", + "kind": "text", + "placeholders": [], + "refs": [ + { + "file": "components/overview/OverviewTimeRangeFilter.tsx", + "line": 38, + "column": 30 + } + ] + }, "d15a909c3490a7e0": { "source": "端点类型", "kind": "text", @@ -3049,7 +3280,7 @@ "refs": [ { "file": "layouts/AppLayout.tsx", - "line": 114, + "line": 180, "column": 54 } ] @@ -3066,6 +3297,18 @@ } ] }, + "da7ae985487c38e6": { + "source": "近1小时", + "kind": "text", + "placeholders": [], + "refs": [ + { + "file": "components/overview/OverviewTimeRangeFilter.tsx", + "line": 34, + "column": 29 + } + ] + }, "db0a1393f05aa4bd": { "source": "默认继承上游,可为当前模型单独修改。", "kind": "text", @@ -3121,7 +3364,7 @@ "refs": [ { "file": "pages/SettingsPage.tsx", - "line": 239, + "line": 262, "column": 30 } ] @@ -3227,6 +3470,18 @@ } ] }, + "e25bf3f419bb68f0": { + "source": "调用详细", + "kind": "text", + "placeholders": [], + "refs": [ + { + "file": "layouts/AppLayout.tsx", + "line": 70, + "column": 44 + } + ] + }, "e3fee05f688708b4": { "source": "LLM 调用", "kind": "text", @@ -3239,6 +3494,18 @@ } ] }, + "e5043c7a2b408271": { + "source": "近10分钟", + "kind": "text", + "placeholders": [], + "refs": [ + { + "file": "components/overview/OverviewTimeRangeFilter.tsx", + "line": 36, + "column": 36 + } + ] + }, "e59ae97924d62f01": { "source": "第一页", "kind": "text", @@ -3263,7 +3530,7 @@ "refs": [ { "file": "pages/SettingsPage.tsx", - "line": 58, + "line": 61, "column": 17 } ] @@ -3316,7 +3583,7 @@ "refs": [ { "file": "pages/SettingsPage.tsx", - "line": 124, + "line": 127, "column": 26 } ] @@ -3391,6 +3658,11 @@ "kind": "text", "placeholders": [], "refs": [ + { + "file": "layouts/AppLayout.tsx", + "line": 75, + "column": 53 + }, { "file": "pages/CursorSettingsPage.tsx", "line": 122, @@ -3417,7 +3689,7 @@ "refs": [ { "file": "pages/SettingsPage.tsx", - "line": 183, + "line": 186, "column": 16 } ] @@ -3509,7 +3781,7 @@ "refs": [ { "file": "components/overview/OverviewTimeRangeFilter.tsx", - "line": 111, + "line": 110, "column": 21 } ] @@ -3526,6 +3798,20 @@ } ] }, + "f78413c36d36f090": { + "source": "默认跟随操作系统;不支持的系统语言使用英文。当前:{language}", + "kind": "template", + "placeholders": [ + "language" + ], + "refs": [ + { + "file": "pages/SettingsPage.tsx", + "line": 199, + "column": 21 + } + ] + }, "f85537d1fd2ef6f6": { "source": "自定义概览筛选", "kind": "text", @@ -3533,7 +3819,7 @@ "refs": [ { "file": "components/overview/OverviewTimeRangeFilter.tsx", - "line": 100, + "line": 99, "column": 19 } ] @@ -3595,7 +3881,7 @@ "refs": [ { "file": "pages/SettingsPage.tsx", - "line": 151, + "line": 154, "column": 21 } ] @@ -3692,6 +3978,18 @@ } ] }, + "fec45092945f8790": { + "source": "使用教程", + "kind": "text", + "placeholders": [], + "refs": [ + { + "file": "layouts/AppLayout.tsx", + "line": 72, + "column": 48 + } + ] + }, "ff509c9ba052a21c": { "source": "保存中…", "kind": "text", @@ -3704,7 +4002,7 @@ }, { "file": "pages/SettingsPage.tsx", - "line": 142, + "line": 145, "column": 121 } ] @@ -3728,17 +4026,17 @@ "refs": [ { "file": "pages/SettingsPage.tsx", - "line": 53, + "line": 56, "column": 46 }, { "file": "pages/SettingsPage.tsx", - "line": 163, + "line": 166, "column": 22 }, { "file": "pages/SettingsPage.tsx", - "line": 178, + "line": 181, "column": 58 } ] diff --git a/apps/desktop/src/i18n/locales/en-US.json b/apps/desktop/src/i18n/locales/en-US.json index 4d538b6..c009e65 100644 --- a/apps/desktop/src/i18n/locales/en-US.json +++ b/apps/desktop/src/i18n/locales/en-US.json @@ -1,264 +1,287 @@ { - "00929f23850e4ff0": "", - "01f3e69a5a9b2c9b": "", - "025b70bde54ed1ef": "", - "028a4de61bff743d": "", - "03ff62ab4b818492": "", - "051836569928a9f9": "", - "0580e0a99a6f1afc": "", - "05912a17829faacc": "", - "076832c1b2de22c3": "", - "07879e064ae16542": "", - "07ec86e0f1d44f91": "", - "08d742566ffc9fa3": "", - "099008ea7a42ebd1": "", - "09ebc2643631ba25": "", - "0a5f9a892960a00a": "", - "0b96da34f6fbdd3b": "", - "0bd20614caf56647": "", - "0c2fee621d406ea7": "", - "0c70665b6eb65f1a": "", - "0c72229b7db0e1a9": "", - "0d5e2bdb15579fc4": "", - "0e41f8e3d59ec47b": "", - "0ec1e85b0c3cfa65": "", - "124be3f86f197802": "", - "12ae77e6202d063e": "", - "12b86e89701b0241": "", - "13a9ac7a68c5fd96": "", - "13b61c5f697b6700": "", - "146da2e2a991493e": "", - "15d76f1c211cc140": "", - "168e845a86bc3703": "", - "16aa4ae1427fccce": "", - "16d0d7e2b332af72": "", - "1813d362a82fd437": "", - "18d25065b3f7db39": "", + "00929f23850e4ff0": "Successful calls: {count}", + "01f3e69a5a9b2c9b": "The key required to access the model service.", + "025b70bde54ed1ef": "Whether Cursor can select and use this model.", + "028a4de61bff743d": "Regular input: {tokens} × ${price}/1M = {cost}", + "03ff62ab4b818492": "Cache write: {tokens} × ${price}/1M = {cost}", + "051836569928a9f9": "Edit", + "0580e0a99a6f1afc": "Artifacts", + "05912a17829faacc": "All call records and detailed traces will be deleted. Providers, models, CA, and application settings are unaffected. This action cannot be undone.", + "076832c1b2de22c3": "Cache write: {tokens}", + "07879e064ae16542": "Estimated output: {tokens}", + "07ec86e0f1d44f91": "Created", + "08d742566ffc9fa3": "Clear all statistics?", + "099008ea7a42ebd1": "All custom Header values must be strings", + "09ebc2643631ba25": "Estimated value", + "0a5f9a892960a00a": "Updated", + "0b96da34f6fbdd3b": "Cache read: {tokens} × ${price}/1M = {cost}", + "0bd20614caf56647": "Select or enter multiple models. When adding in bulk, each display name defaults to its model name.", + "0c2fee621d406ea7": "Select an existing provider or create a new one.", + "0c70665b6eb65f1a": "No", + "0c72229b7db0e1a9": "Model output", + "0d5e2bdb15579fc4": "Messages", + "0e41f8e3d59ec47b": "Storage management", + "0ec1e85b0c3cfa65": "Call details", + "124be3f86f197802": "Token usage", + "12ae77e6202d063e": "Custom Headers", + "12b86e89701b0241": "Enter a model name and press Enter to add it", + "13a9ac7a68c5fd96": "The CA is stored only on this device and is used to securely inspect Cursor HTTPS requests.", + "13b61c5f697b6700": "Cache hit rate", + "146da2e2a991493e": "Fetching…", + "15d76f1c211cc140": "Leave blank to keep the current API Key.", + "168e845a86bc3703": "Add model", + "16aa4ae1427fccce": "Custom Headers JSON", + "16d0d7e2b332af72": "Total calls: {count}", + "1813d362a82fd437": "Maximize window", + "18d25065b3f7db39": "The full request URL is required", "19658d9fa9aa8de4": "Installing…", - "1a3f0617d6de8e52": "", - "1ae6b0a0f8266382": "", - "1b5932b8946d2d68": "", - "1b7d5b1a9315fc64": "", - "1c6926877b1bbfb2": "", - "1cb7e646ac883fb2": "", - "1cdde778cfe6a130": "", - "1cef73a53d7ad0f2": "", - "1f9b46e61efcccb5": "", - "21296ab18ad9af25": "", - "22c6b4eb4caee6ae": "", + "1a3f0617d6de8e52": "Username", + "1ae6b0a0f8266382": "Close window", + "1b5932b8946d2d68": "Delete model", + "1b7d5b1a9315fc64": "Calculating…", + "1c6926877b1bbfb2": "Total requests: {tokens}", + "1cb7e646ac883fb2": "Proxy port", + "1cdde778cfe6a130": "Formula: cache read / (cache read + non-cached input)", + "1cef73a53d7ad0f2": "For example: OpenAI", + "1f9b46e61efcccb5": "Filter: {label}, {summary}", + "21296ab18ad9af25": "Extra parameters JSON", + "217cfe7db1e3d10a": "Use system language", + "22c6b4eb4caee6ae": "Proxy settings saved", "23e49479e15e6770": "Version {version} is available", "29585d7193539200": "Current version {version}", - "2a2773134a829016": "", - "2cd0f3be8738a86c": "", - "2d30c2a98ebb5278": "", - "2eb2bf7c6597ab9a": "", - "2f1b67cdaa23351a": "", - "2f4a361f878176d1": "", - "2f5f1d6fbfb061ed": "", - "2f6416a2c424856b": "", - "2f7dec3be28d7597": "", - "2f9daa828907b93f": "", - "378bb0eec39fa8a2": "", - "37cb98ff4d5dcfcc": "", - "393df9bb13ea4900": "", - "393e1241552b1870": "", - "398f8e6c6f0a0b97": "", - "39f52eee100131d7": "", - "3a0fb74abe2460b6": "", - "3a3f595df70ec8ff": "", - "3a5040b68abf75f9": "", - "3bd5b3f538e3270a": "", - "3c94b4c75940c178": "", - "3cfae5728b92b334": "", - "3da0bf1610ff5db5": "", - "3fd47edce45b3603": "", - "42655ed8e4108ae2": "", - "42a1d9e5b037c210": "", - "42aa8e01e98c0d8c": "", - "43cb41d62de2d179": "", - "470049252e54de6a": "", - "48b970b568a7f8f9": "", - "48d8db17bae06246": "", + "29fbbef32a6eb58b": "Do not show this ad again", + "2a2773134a829016": "Aggregated from historical LLM calls; in-progress calls are excluded.", + "2cd0f3be8738a86c": "Cancel", + "2d30c2a98ebb5278": "Current: {rate}", + "2eb2bf7c6597ab9a": "Detailed records", + "2f1b67cdaa23351a": "Custom full request URL", + "2f4a361f878176d1": "{label} must be valid JSON", + "2f5f1d6fbfb061ed": "Not set", + "2f6416a2c424856b": "Final request URL", + "2f7dec3be28d7597": "{count} selected", + "2f9daa828907b93f": "Delete", + "36f33adaf0942634": "Confirm", + "378bb0eec39fa8a2": "Last page", + "37cb98ff4d5dcfcc": "Successful {successful} / failed {failed}", + "393df9bb13ea4900": "Hit", + "393e1241552b1870": "Request", + "398f8e6c6f0a0b97": "Continue selecting or typing", + "39f52eee100131d7": "Cached input", + "3a0fb74abe2460b6": "Chunks", + "3a3f595df70ec8ff": "Clear storage", + "3a5040b68abf75f9": "Select all", + "3bd5b3f538e3270a": "New provider", + "3c94b4c75940c178": "Input Tokens", + "3cfae5728b92b334": "Token usage: {tokens}", + "3d13868593ae4eeb": "Display language", + "3da0bf1610ff5db5": "Recommended", + "3fd47edce45b3603": "Close", + "4125fc7ba333524c": "Default light", + "42655ed8e4108ae2": "Input (non-cached)", + "42a1d9e5b037c210": "Bytes", + "42aa8e01e98c0d8c": "Total duration", + "43cb41d62de2d179": "Proxy requires authentication", + "4510b63fdcf7451f": "Cursor Assistant v0.1.0", + "470049252e54de6a": "Success rate: {rate}", + "48b970b568a7f8f9": "Proxy settings", + "48d8db17bae06246": "{count} total", "492042ed1fdc29ed": "Version {version} is ready to install", - "4927a53bcc886afb": "", - "497c85690c4cc0fc": "", - "4d0680f9efaef147": "", - "4eafa9e925b30bcd": "", - "5228358a6db59fe7": "", - "54644705e9c61009": "", - "54c53e5fe791d1f3": "", - "550eddc3c7fefa99": "", - "555737734a6371e6": "", - "56432ba297009bdc": "", - "5ae715656ffbc35d": "", - "5c55a67935af8f45": "", + "4927a53bcc886afb": "Loading…", + "493e097fac6bd217": "Providers", + "497c85690c4cc0fc": "No data", + "4d0680f9efaef147": "Unread", + "4eafa9e925b30bcd": "Custom", + "51d04bc3d286f018": "Last calendar day", + "5228358a6db59fe7": "For example: now, 2026-08-23 18:00", + "54644705e9c61009": "Port settings", + "54c53e5fe791d1f3": "Initialize CA", + "550eddc3c7fefa99": "Sponsored", + "555737734a6371e6": "Delete provider", + "56432ba297009bdc": "Initialize the CA first", + "5ae715656ffbc35d": "Merge into the request body for every model from this provider.", + "5c55a67935af8f45": "All", "5f8d556a9c47da3c": "Launch at login disabled", - "5fd2ec5a6e9b654c": "", - "6078a681a306930d": "", - "61a4c7bac12dc125": "", - "61c7d1f758b647d5": "", - "621f63a5f08384ac": "", + "5f9acfb945229062": "Are you sure you no longer want to see this ad?", + "5fd2ec5a6e9b654c": "Total: {cost}", + "6078a681a306930d": "Cache write", + "61a4c7bac12dc125": "Failed calls: {count}", + "61c7d1f758b647d5": "Call information", + "621f63a5f08384ac": "Cache read: {tokens}", "6320b4a8722a851f": "Status", - "63c73c4730f4473e": "", - "6478a5f1218c484e": "", + "63c73c4730f4473e": "Apply", + "6478a5f1218c484e": "Use the desktop app to copy to the system clipboard", "651f274470153a05": "Software updates", "653b123c956d3bcb": "Calls", - "656ab25e264cc4e4": "", - "65a6318e07ec1e07": "", - "65cb9a7b4f620b6b": "", - "68152165b3348852": "", - "6a9906c79f26c0ba": "", - "6ae80538c2b2572d": "", - "6d1876364ac6457d": "", + "656ab25e264cc4e4": "No models are available to Cursor yet", + "65a6318e07ec1e07": "Tools", + "65cb9a7b4f620b6b": "Prompt {tokens}", + "68152165b3348852": "Select the request protocol used by this provider.", + "68ea5dd4d7af20e6": "System settings", + "6a9906c79f26c0ba": "Start time", + "6ae80538c2b2572d": "Minimize window", + "6d1876364ac6457d": "Proxy mode", "6e86570183c3cdd0": "You're up to date", - "7005693f4f050bce": "", - "71eb2c6fce991a29": "", - "722e156c20650a3f": "", + "7005693f4f050bce": "Cache I/O {cost}", + "71eb2c6fce991a29": "Add provider", + "722e156c20650a3f": "Use macOS or Windows.", "7392e20d61abaa07": "Also store complete requests and streamed responses; by default only timing, status, and usage are stored.", - "75844712d53a24de": "", + "75844712d53a24de": "Values must be strings; when editing, null keeps the original value of the corresponding sensitive Header.", "788db1cfec2a3db5": "Theme", - "7995087e5a3dfe66": "", - "7a2229f6a6d330a5": "", - "7a3cec4ca715de80": "", - "7ba2d6728fe2531b": "", + "7995087e5a3dfe66": "Restore window", + "7a2229f6a6d330a5": "Open a terminal from the desktop app to install the CA", + "7a3cec4ca715de80": "Call statistics", + "7ba2d6728fe2531b": "Confirm clear", "7d9f043f8f7ab45c": "Version {version} is available in Settings", - "7e1845870b528392": "", - "7e1f06318e80c3af": "", - "7e9ab9ada2cbf2cb": "", - "7f3c8312816fe26a": "", + "7e1845870b528392": "Enable model", + "7e1f06318e80c3af": "Statistics cleared", + "7e9ab9ada2cbf2cb": "Name and Base URL are required", + "7f3c8312816fe26a": "Refreshing…", "7f68ebad19ba6bcd": "Check for updates", - "80a57e03f0717f91": "", - "811a3b22a5a7f2d5": "", - "83fcfb4c1f2c1641": "", + "80a57e03f0717f91": "Not configured", + "811a3b22a5a7f2d5": "Unable to connect to the local management service", + "83fcfb4c1f2c1641": "Fetch models", "842b9f11cdd96bda": "Launch at login", - "84924374710e03bd": "", - "8716e1344b0daddb": "", - "878a8ab176429a86": "", - "883cc47637fe70f3": "", - "8a8542f6964852dc": "", + "84924374710e03bd": "Base URL must be a valid URL", + "8716e1344b0daddb": "Cursor official", + "878a8ab176429a86": "View instructions", + "883cc47637fe70f3": "Custom request headers appended to every request for this provider. Values must be strings.", + "8a8542f6964852dc": "Next page", "8b6ff498515bcc2f": "Time", - "8d0c47eb9eac2d34": "", - "90800c48a1dd0655": "", - "919cb0ce0c8db4e7": "", - "91ee087c3d4f3220": "", - "940a168911ade998": "", - "946b3ffc02f026c0": "", - "94803f35c825e47a": "", - "94df1e7f04815daf": "", - "95f76d30c25d5eda": "", - "9850ed41a5bfbb0c": "", - "997ec8201c2adeda": "", - "9a026819dd1af5c5": "", - "9ac0ac940982895d": "", - "9c41b3a9e12ac994": "", - "9e46da6923836182": "", - "a026f37e613cf48b": "", - "a03a1a0cb35414f8": "", + "8d0c47eb9eac2d34": "Call type", + "8df48894086d6fbd": "Reason (optional)", + "90800c48a1dd0655": "{label} must be a JSON object", + "919cb0ce0c8db4e7": "Leave blank to keep the current password", + "91aaf184cfc17ffd": "Overview", + "91ee087c3d4f3220": "Supports a complete HTTP(S) URL or a relative path beginning with /, combined with the provider URL.", + "940a168911ade998": "Items per page", + "946b3ffc02f026c0": "Delete this model?", + "94803f35c825e47a": "Full request URL", + "94df1e7f04815daf": "Used only for display; does not change the model name sent to the provider.", + "95f76d30c25d5eda": "CA installation is not supported on this system", + "9850ed41a5bfbb0c": "{count} selected", + "997ec8201c2adeda": "Open terminal to install CA", + "9a026819dd1af5c5": "Enter a model identifier directly or select one returned by the current provider.", + "9ac0ac940982895d": "Select provider", + "9c41b3a9e12ac994": "Reasoning effort", + "9e46da6923836182": "For example: 2026-08-23 09:00, 1 hour ago", + "9f6fee1aba17a565": "Language", + "9fb48101d237ff96": "Last week", + "a026f37e613cf48b": "Output Tokens", + "a03a1a0cb35414f8": " must be an integer from 0 to 65535", "a1a42cd9b16e2162": "Application", - "a2901cd2743c1921": "", - "a3030bf8f16dc63c": "", - "a363743025795ec7": "", - "a3ab741ceb188e9e": "", - "a49ffd73bc85333d": "", - "a621ab606db2a11f": "", - "a748cc074f78de00": "", - "a98585871c5313ff": "", - "a9ab292ea9feecdc": "", - "aa678e13bda90e3c": "", + "a2901cd2743c1921": "The API root URL for the model service. Changing it also updates the routing identity of this provider's models.", + "a3030bf8f16dc63c": "Save", + "a363743025795ec7": "I've initialized it — refresh", + "a3ab741ceb188e9e": "Request content was not recorded. Enable detailed records and try again.", + "a49ffd73bc85333d": "Average", + "a621ab606db2a11f": "Password", + "a748cc074f78de00": "View details", + "a98585871c5313ff": "Display name", + "a9ab292ea9feecdc": "Provider", + "aa678e13bda90e3c": "Custom Header values must be strings or null", "ab2f31f30acf7fda": "Protocol", - "ab9084a640fbb864": "", + "ab9084a640fbb864": "Deselect all", "abecab6701177721": "Launch at login enabled", "ac69f68b7010ec79": "Download and install", - "ad6a60ee93d3ba3e": "", - "ae2d0b7f79cea4a3": "", + "ad6a60ee93d3ba3e": "Loading call details…", + "ae2d0b7f79cea4a3": "Model output: {tokens} × ${price}/1M = {cost}", "aee88743413144a2": "Refresh", - "b4411558b932266f": "", - "b502b1d414664337": "", - "b5141d3d19e9a048": "", - "b710ec36ad312918": "", - "b75a46aad3e7c132": "", - "b79354009c614ae9": "", - "b89a0e4584f27ab5": "", - "b9670c85a4ab939e": "", - "b97ad406809572e1": "", - "b9af2de88d903be7": "", - "bb2b7736433ae867": "", - "bf57afd709694b55": "", - "bf84379b75735037": "", - "bfc01caf9fe0c841": "", - "c1e98892a77f7a19": "", - "c3760858cdb6d9f4": "", - "c62a58459251b02c": "", - "c7ea2c9bc43134bd": "", - "c8df3c14a003bfcd": "", + "b4411558b932266f": "Provider type", + "b502b1d414664337": "Prompt: {tokens}", + "b5141d3d19e9a048": "Yes", + "b710ec36ad312918": "The model service API root URL, for example https://api.openai.com/v1.", + "b75a46aad3e7c132": "Non-cached input: {tokens}", + "b79354009c614ae9": "Statistics", + "b89a0e4584f27ab5": "Open terminal", + "b8c9b486c83b5778": "Hide ad", + "b9670c85a4ab939e": "Route", + "b97ad406809572e1": "Enable reasoning", + "b9af2de88d903be7": "Proxy address", + "bb2b7736433ae867": "Cursor tracing", + "bb7efdcb6af6e805": "Default dark", + "bda62ce1d5e4ace9": "Tell us why", + "bf57afd709694b55": "Overview time range", + "bf84379b75735037": "Edit provider", + "bfc01caf9fe0c841": "Cache hit rate {rate}", + "c1e98892a77f7a19": "{count} per page", + "c3760858cdb6d9f4": "Request body", + "c62a58459251b02c": "Image generation", + "c7ea2c9bc43134bd": "Edit model", + "c8df3c14a003bfcd": "Unable to load call details", "c98e118e0a43f078": "Model", - "c9b9ae7a61444ab7": "", - "c9d146d006993cc1": "", - "cb2f1709f983d2f4": "", - "cea1aafe9416de7b": "", + "c9b9ae7a61444ab7": "Previous page", + "c9d146d006993cc1": "Cache statistics policy: default ({rate})", + "cb2f1709f983d2f4": "Model name", + "cea1aafe9416de7b": "Request headers", "cfae1a14d2120c57": "Detailed mode", - "cfe085015632e9c8": "", - "cfe999e50be8ef54": "", - "d15a909c3490a7e0": "", - "d2d648bd1c94b7f9": "", - "d30d35c5ec4a888d": "", + "cfe085015632e9c8": "The local management service port used by the desktop frontend. Enter 0 to select a random port at startup.", + "cfe999e50be8ef54": "Whether the model declares image-generation support.", + "d0bfccc77315d887": "Last month", + "d15a909c3490a7e0": "Endpoint type", + "d2d648bd1c94b7f9": "Authentication", + "d30d35c5ec4a888d": "Select or enter at least one model", "d34335433395cd3a": "Start Cursor BYOK automatically after signing in.", - "d3d21191f32e79a5": "", + "d3d21191f32e79a5": "Processing…", "d44e9b3d3b31d37b": "Name", - "d667c1616e31c4c4": "", - "d86fa42c3848c680": "", - "d8c47e9776cf1082": "", - "da521d1c1cbd36af": "", - "db0a1393f05aa4bd": "", - "ddde16f8839da3ce": "", - "de8184da1ef88d03": "", - "dea7749c4cd77e6d": "", - "df3d58c7d84b85f2": "", - "df8b71c74d9b8478": "", - "dfb802238b38fbd4": "", - "e025f1ff71996425": "", - "e1295adecbb77755": "", - "e14115de7f7c5795": "", - "e14a5eee9b0b0f9f": "", - "e14f20d572c02611": "", - "e18516550b9a5105": "", + "d667c1616e31c4c4": "Leave blank to keep the current key", + "d86fa42c3848c680": "Use system proxy", + "d8c47e9776cf1082": "Main menu", + "da521d1c1cbd36af": "Authorization is required to install the certificate", + "da7ae985487c38e6": "Last hour", + "db0a1393f05aa4bd": "Inherits the provider by default; override it for this model if needed.", + "ddde16f8839da3ce": "Total requests", + "de8184da1ef88d03": "Configured", + "dea7749c4cd77e6d": "Total request Tokens include the prompt and model output.", + "df3d58c7d84b85f2": "Settings", + "df8b71c74d9b8478": "Response stream", + "dfb802238b38fbd4": "Enabled", + "e025f1ff71996425": "Set", + "e1295adecbb77755": "Close ad", + "e14115de7f7c5795": "Token usage over the past year", + "e14a5eee9b0b0f9f": "Whether the model declares reasoning support.", + "e14f20d572c02611": "Provider call sequence", + "e18516550b9a5105": "No usage", + "e25bf3f419bb68f0": "Call history", "e3fee05f688708b4": "LLM calls", - "e59ae97924d62f01": "", - "e5b9961a0d5242e3": "", - "e6ca887f22288cde": "", - "e77e3d58b0dcffaa": "", - "e828bd3a0151edc2": "", + "e5043c7a2b408271": "Last 10 minutes", + "e59ae97924d62f01": "First page", + "e5b9961a0d5242e3": "Port settings saved. Restart the app to apply them.", + "e6ca887f22288cde": "Model ID and display name are required", + "e77e3d58b0dcffaa": "Duration", + "e828bd3a0151edc2": "The local CA must be trusted by the system", "ea26b760e930a7ca": "Call observability", - "eb11e2df1d8ae387": "", - "eb1be07f2ca6e506": "", - "eb77492c9f76a7e1": "", - "ed31fbb483ee1b0a": "", - "edc70de18c6da1a6": "", - "ede6d08f397df283": "", - "ee239f3943293f87": "", - "ee6b89a6a740a4c4": "", - "f04c91a6bc3a6926": "", - "f396118b8afd2a21": "", - "f3a76d896853c1df": "", - "f4694c46b1e19602": "", - "f4dcb6a3ceb32247": "", - "f4fa9f31ea2ae58d": "", - "f50276449943286c": "", - "f69273dbbebfb3a1": "", - "f85537d1fd2ef6f6": "", - "f95ea7f4c063eea7": "", - "f9615d05d8e18595": "", - "f9aa11dbb15ce647": "", - "f9b55ca75425161b": "", - "fa5b4b8a751c7d1b": "", + "eb11e2df1d8ae387": "Provider URL", + "eb1be07f2ca6e506": "Estimated using Claude Opus 4.7 pricing.", + "eb77492c9f76a7e1": "The install command has been copied. Click “Open terminal”, paste it into the terminal, and enter your password when prompted.", + "ed31fbb483ee1b0a": "Actions", + "edc70de18c6da1a6": "Install local CA", + "ede6d08f397df283": "Cursor settings", + "ee239f3943293f87": "Sunday", + "ee6b89a6a740a4c4": "If a port is occupied, a new random port is selected and saved automatically. Restart the app after changing these settings.", + "f04c91a6bc3a6926": "Extra parameters", + "f396118b8afd2a21": "Cursor interception is active. Add a provider and its model configuration to use BYOK models.", + "f3a76d896853c1df": "Miss", + "f4694c46b1e19602": "Final request type", + "f4dcb6a3ceb32247": "Page {page} of {count}", + "f4fa9f31ea2ae58d": "Token usage calendar for the past year", + "f50276449943286c": "End time", + "f69273dbbebfb3a1": "Format", + "f78413c36d36f090": "Uses the operating system language by default; unsupported languages fall back to English. Current: {language}", + "f85537d1fd2ef6f6": "Custom overview filters", + "f95ea7f4c063eea7": "Disabled", + "f9615d05d8e18595": "Clear {label} filter", + "f9aa11dbb15ce647": "Saturday", + "f9b55ca75425161b": "Response content was not recorded. Enable detailed records and try again.", + "fa5b4b8a751c7d1b": "The local proxy port used by Cursor. Enter 0 to select a random port at startup.", "fb11aa6f29827095": "Checking…", - "fbe8778fa8b9bab5": "", - "fc3947ebe6b2177b": "", - "fcd311fd8ad42462": "", - "fd415f8e0097c832": "", - "fdc4cabc370fa3f7": "", - "fea405f9b01d1416": "", - "ff509c9ba052a21c": "", - "ff66b1c7e010bbc2": "", - "ff673195f05fb056": "" + "fbe8778fa8b9bab5": "Initialize the local CA first", + "fc3947ebe6b2177b": "Default {defaultRate} / include creation {reuseRate}", + "fcd311fd8ad42462": "Open model list", + "fd415f8e0097c832": "Cache reads and writes are included in prompt-side statistics.", + "fdc4cabc370fa3f7": "No options", + "fea405f9b01d1416": "Summary", + "fec45092945f8790": "User guide", + "ff509c9ba052a21c": "Saving…", + "ff66b1c7e010bbc2": "Paste the authorization command into the terminal and enter your password, then click the button below", + "ff673195f05fb056": "Service port" } diff --git a/apps/desktop/src/i18n/locales/zh-CN.json b/apps/desktop/src/i18n/locales/zh-CN.json index 8e9302e..f2073c0 100644 --- a/apps/desktop/src/i18n/locales/zh-CN.json +++ b/apps/desktop/src/i18n/locales/zh-CN.json @@ -45,9 +45,11 @@ "1cef73a53d7ad0f2": "例如:OpenAI", "1f9b46e61efcccb5": "筛选项:{label},{summary}", "21296ab18ad9af25": "额外参数 JSON", + "217cfe7db1e3d10a": "跟随系统", "22c6b4eb4caee6ae": "代理设置已保存", "23e49479e15e6770": "发现新版本 {version}", "29585d7193539200": "当前版本 {version}", + "29fbbef32a6eb58b": "不再显示此广告", "2a2773134a829016": "按历史 LLM 调用记录汇总,进行中的调用不计入。", "2cd0f3be8738a86c": "取消", "2d30c2a98ebb5278": "当前:{rate}", @@ -58,6 +60,7 @@ "2f6416a2c424856b": "最终请求地址", "2f7dec3be28d7597": "已选择 {count} 个", "2f9daa828907b93f": "删除", + "36f33adaf0942634": "确认", "378bb0eec39fa8a2": "最后一页", "37cb98ff4d5dcfcc": "成功 {successful} / 异常 {failed}", "393df9bb13ea4900": "命中", @@ -70,20 +73,25 @@ "3bd5b3f538e3270a": "新建上游", "3c94b4c75940c178": "输入 Token", "3cfae5728b92b334": "Token 用量:{tokens}", + "3d13868593ae4eeb": "界面语言", "3da0bf1610ff5db5": "推荐内容", "3fd47edce45b3603": "关闭", + "4125fc7ba333524c": "默认亮色", "42655ed8e4108ae2": "输入(非缓存)", "42a1d9e5b037c210": "字节数", "42aa8e01e98c0d8c": "总耗时", "43cb41d62de2d179": "代理需要认证", + "4510b63fdcf7451f": "Cursor 助手 v0.1.0", "470049252e54de6a": "成功占比:{rate}", "48b970b568a7f8f9": "代理设置", "48d8db17bae06246": "共 {count} 条", "492042ed1fdc29ed": "版本 {version} 可以安装", "4927a53bcc886afb": "加载中…", + "493e097fac6bd217": "上游配置", "497c85690c4cc0fc": "暂无数据", "4d0680f9efaef147": "未读", "4eafa9e925b30bcd": "自定义", + "51d04bc3d286f018": "近1自然日", "5228358a6db59fe7": "如:现在、2026-08-23 18:00", "54644705e9c61009": "端口设置", "54c53e5fe791d1f3": "初始化 CA", @@ -93,6 +101,7 @@ "5ae715656ffbc35d": "合并到该上游所有模型的请求体。", "5c55a67935af8f45": "全部", "5f8d556a9c47da3c": "已关闭开机启动", + "5f9acfb945229062": "你确认不想再看到此广告吗?", "5fd2ec5a6e9b654c": "合计:{cost}", "6078a681a306930d": "缓存写入", "61a4c7bac12dc125": "异常调用:{count}", @@ -107,6 +116,7 @@ "65a6318e07ec1e07": "工具数", "65cb9a7b4f620b6b": "提示词 {tokens}", "68152165b3348852": "选择上游服务使用的请求协议。", + "68ea5dd4d7af20e6": "系统设置", "6a9906c79f26c0ba": "开始时间", "6ae80538c2b2572d": "最小化窗口", "6d1876364ac6457d": "代理方式", @@ -138,8 +148,10 @@ "8a8542f6964852dc": "下一页", "8b6ff498515bcc2f": "时间", "8d0c47eb9eac2d34": "调用类型", + "8df48894086d6fbd": "原因(可选)", "90800c48a1dd0655": "{label} 必须是 JSON 对象", "919cb0ce0c8db4e7": "留空表示保留当前密码", + "91aaf184cfc17ffd": "数据概览", "91ee087c3d4f3220": "支持完整 HTTP(S) 地址或以 / 开头、与上游地址组合的相对路径。", "940a168911ade998": "每页条数", "946b3ffc02f026c0": "确定删除这个模型吗?", @@ -152,6 +164,8 @@ "9ac0ac940982895d": "选择上游", "9c41b3a9e12ac994": "思考强度", "9e46da6923836182": "如:2026-08-23 09:00、1小时前", + "9f6fee1aba17a565": "语言", + "9fb48101d237ff96": "近一周", "a026f37e613cf48b": "输出 Token", "a03a1a0cb35414f8": "必须是 0–65535 之间的整数", "a1a42cd9b16e2162": "应用设置", @@ -179,10 +193,13 @@ "b75a46aad3e7c132": "非缓存输入:{tokens}", "b79354009c614ae9": "统计数据", "b89a0e4584f27ab5": "打开终端", + "b8c9b486c83b5778": "不再显示广告", "b9670c85a4ab939e": "路由", "b97ad406809572e1": "启用推理", "b9af2de88d903be7": "代理地址", "bb2b7736433ae867": "Cursor 追踪", + "bb7efdcb6af6e805": "默认暗色", + "bda62ce1d5e4ace9": "可以告诉我们原因", "bf57afd709694b55": "概览时间范围", "bf84379b75735037": "编辑上游", "bfc01caf9fe0c841": "缓存命中率 {rate}", @@ -199,6 +216,7 @@ "cfae1a14d2120c57": "详细模式", "cfe085015632e9c8": "桌面前端连接的本地管理服务端口;填写 0 时启动时随机选择。", "cfe999e50be8ef54": "是否声明模型支持图片生成。", + "d0bfccc77315d887": "近一个月", "d15a909c3490a7e0": "端点类型", "d2d648bd1c94b7f9": "认证", "d30d35c5ec4a888d": "请至少选择或输入一个模型", @@ -209,6 +227,7 @@ "d86fa42c3848c680": "使用系统代理", "d8c47e9776cf1082": "主菜单", "da521d1c1cbd36af": "需要授权安装证书", + "da7ae985487c38e6": "近1小时", "db0a1393f05aa4bd": "默认继承上游,可为当前模型单独修改。", "ddde16f8839da3ce": "总请求", "de8184da1ef88d03": "已配置", @@ -222,7 +241,9 @@ "e14a5eee9b0b0f9f": "是否声明模型支持推理能力。", "e14f20d572c02611": "上游调用序号", "e18516550b9a5105": "无用量", + "e25bf3f419bb68f0": "调用详细", "e3fee05f688708b4": "LLM 调用", + "e5043c7a2b408271": "近10分钟", "e59ae97924d62f01": "第一页", "e5b9961a0d5242e3": "端口设置已保存,重启软件后生效", "e6ca887f22288cde": "Model ID 和显示名称不能为空", @@ -245,6 +266,7 @@ "f4fa9f31ea2ae58d": "过去一年的 Token 用量日历", "f50276449943286c": "结束时间", "f69273dbbebfb3a1": "格式化", + "f78413c36d36f090": "默认跟随操作系统;不支持的系统语言使用英文。当前:{language}", "f85537d1fd2ef6f6": "自定义概览筛选", "f95ea7f4c063eea7": "未启用", "f9615d05d8e18595": "清除{label}筛选", @@ -258,6 +280,7 @@ "fd415f8e0097c832": "缓存读写已计入提示词侧统计。", "fdc4cabc370fa3f7": "暂无选项", "fea405f9b01d1416": "概览", + "fec45092945f8790": "使用教程", "ff509c9ba052a21c": "保存中…", "ff66b1c7e010bbc2": "请在终端中粘贴授权命令并输入密码,完成后点击下方按钮", "ff673195f05fb056": "服务端口" diff --git a/apps/desktop/src/i18n/store.ts b/apps/desktop/src/i18n/store.ts new file mode 100644 index 0000000..b0ae989 --- /dev/null +++ b/apps/desktop/src/i18n/store.ts @@ -0,0 +1,76 @@ +import { useSyncExternalStore } from "react"; +import { setRuntimeLocale, type Locale } from "./runtime"; + +export type LocalePreference = "system" | Locale; + +type I18nSnapshot = { + preference: LocalePreference; + locale: Locale; +}; + +const storageKey = "cursor-byok.locale"; +const listeners = new Set<() => void>(); +let initialized = false; +let snapshot: I18nSnapshot = { preference: "system", locale: "en-US" }; + +function isLocale(value: string | null): value is Locale { + return value === "zh-CN" || value === "en-US"; +} + +export function resolveSystemLocale( + languages: readonly string[] = navigator.languages.length + ? navigator.languages + : [navigator.language], +): Locale { + const normalized = languages[0]?.toLowerCase() ?? ""; + if (normalized === "zh" || normalized.startsWith("zh-")) return "zh-CN"; + if (normalized === "en" || normalized.startsWith("en-")) return "en-US"; + return "en-US"; +} + +function readPreference(): LocalePreference { + try { + const saved = localStorage.getItem(storageKey); + return isLocale(saved) ? saved : "system"; + } catch { + return "system"; + } +} + +function applyPreference(preference: LocalePreference, notify: boolean) { + const locale = preference === "system" ? resolveSystemLocale() : preference; + const changed = snapshot.preference !== preference || snapshot.locale !== locale; + snapshot = { preference, locale }; + setRuntimeLocale(locale); + document.documentElement.lang = locale; + if (notify && changed) listeners.forEach((listener) => listener()); +} + +export function initializeI18n() { + if (initialized) return; + initialized = true; + applyPreference(readPreference(), false); + window.addEventListener("languagechange", () => { + if (snapshot.preference === "system") applyPreference("system", true); + }); +} + +export function setLocalePreference(preference: LocalePreference) { + try { + if (preference === "system") localStorage.removeItem(storageKey); + else localStorage.setItem(storageKey, preference); + } catch { + // The preference still applies for the current session when storage is unavailable. + } + applyPreference(preference, true); +} + +export function useI18n(): I18nSnapshot { + return useSyncExternalStore( + (listener) => { + listeners.add(listener); + return () => listeners.delete(listener); + }, + () => snapshot, + ); +} diff --git a/apps/desktop/src/index.tsx b/apps/desktop/src/index.tsx index 3b9e87c..4ce86a5 100644 --- a/apps/desktop/src/index.tsx +++ b/apps/desktop/src/index.tsx @@ -1,15 +1,15 @@ import ReactDOM from "react-dom/client"; import "../node_modules/monaco-editor/min/vs/editor/editor.main.css"; -import { App } from "./App"; -import { setRuntimeLocale } from "./i18n/runtime"; +import { I18nRoot } from "./i18n/I18nRoot"; +import { initializeI18n } from "./i18n/store"; import { appStore } from "./store/appStore"; import { applyTheme } from "./theme/theme"; import "./styles/globals.scss"; -setRuntimeLocale("zh-CN"); +initializeI18n(); applyTheme(appStore.getSnapshot().theme); void appStore.refresh(); ReactDOM.createRoot(document.getElementById("root")!).render( - , + , ); diff --git a/apps/desktop/src/layouts/AppHeader.tsx b/apps/desktop/src/layouts/AppHeader.tsx index 27fa339..82b68c5 100644 --- a/apps/desktop/src/layouts/AppHeader.tsx +++ b/apps/desktop/src/layouts/AppHeader.tsx @@ -17,7 +17,7 @@ export function AppHeader({ platform, nativeDesktop }: AppHeaderProps) { {showNativeUi && <>
- Cursor 助手 v0.1.0 + {t("Cursor 助手 v0.1.0")}
} diff --git a/apps/desktop/src/layouts/AppLayout.module.scss b/apps/desktop/src/layouts/AppLayout.module.scss index 266a707..7353c1e 100644 --- a/apps/desktop/src/layouts/AppLayout.module.scss +++ b/apps/desktop/src/layouts/AppLayout.module.scss @@ -53,7 +53,8 @@ display: flex; align-items: stretch; - a { + a, + button { width: 100%; height: 36px; display: flex; @@ -73,6 +74,22 @@ background: var(--vscode-list-activeSelectionBackground); } } + + button { + font: inherit; + text-align: left; + background: transparent; + border: 0; + } +} + +.menuUnreadDot { + width: 7px; + height: 7px; + flex: 0 0 7px; + margin-left: auto; + background: #ef4444; + border-radius: 50%; } .dismissReason { diff --git a/apps/desktop/src/layouts/AppLayout.tsx b/apps/desktop/src/layouts/AppLayout.tsx index a49524d..099a0a5 100644 --- a/apps/desktop/src/layouts/AppLayout.tsx +++ b/apps/desktop/src/layouts/AppLayout.tsx @@ -13,50 +13,24 @@ import controls from "../components/ui/Controls.module.scss"; import { Icon } from "../components/ui/Icon"; import { Modal } from "../components/ui/Modal"; import { TooltipTrigger } from "../components/ui/TooltipTrigger"; -import { flatColorAreaChartIcon, flatColorOrganizationIcon, flatColorSalesPerformanceIcon, flatColorSettingsIcon, refreshIcon } from "../components/ui/icons"; +import { flatColorAboutIcon, flatColorAreaChartIcon, flatColorOrganizationIcon, flatColorSalesPerformanceIcon, flatColorSettingsIcon, refreshIcon } from "../components/ui/icons"; import { useMessage } from "../components/ui/message"; import { VirtualList } from "../components/virtual/VirtualList"; +import { useI18n } from "../i18n/store"; import { appStore, useAppStore } from "../store/appStore"; import styles from "./AppLayout.module.scss"; import { PageActionsTarget } from "./PageActions"; type MenuItem = | { kind: "page"; path: string; label: string; icon: IconifyIcon | string } + | { kind: "external"; id: string; label: string; icon: IconifyIcon | string } | { kind: "group"; label: string }; -const menuItems: MenuItem[] = [ - { kind: "page", path: "/", label: "数据概览", icon: flatColorAreaChartIcon }, - { - kind: "page", - path: "/calls", - label: "调用详细", - icon: flatColorSalesPerformanceIcon, - }, - { - kind: "page", - path: "/providers", - label: "上游配置", - icon: flatColorOrganizationIcon, - }, - { kind: "group", label: "Harness" }, - - { - kind: "page", - path: "/settings", - label: "系统设置", - icon: flatColorSettingsIcon, - }, - { - kind: "page", - path: "/harness/cursor", - label: "Cursor 设置", - icon: cursorIconUrl, - }, -]; - const keptAlivePages = ["/", "/calls", "/providers", "/settings", "/harness/cursor"]; const readAdStorageKey = "cursor-byok:read-ad-ids"; const dismissedAdStorageKey = "cursor-byok:dismissed-ad-ids"; +const tutorialReadStorageKey = "cursor-byok:tutorial-read"; +const tutorialUrl = "https://docs.leokun.cn"; function loadStoredAdIds(key: string): Set { try { @@ -69,6 +43,7 @@ function loadStoredAdIds(key: string): Set { export function AppLayout() { const { busy } = useAppStore(); + const { locale } = useI18n(); const message = useMessage(); const location = useLocation(); const [actionTarget, setActionTarget] = useState(null); @@ -76,6 +51,13 @@ export function AppLayout() { const [activeAd, setActiveAd] = useState(null); const [dismissCandidate, setDismissCandidate] = useState(null); const [dismissReason, setDismissReason] = useState(""); + const [tutorialRead, setTutorialRead] = useState(() => { + try { + return localStorage.getItem(tutorialReadStorageKey) === "true"; + } catch { + return false; + } + }); const [readAdIds, setReadAdIds] = useState(() => loadStoredAdIds(readAdStorageKey)); const [dismissedAdIds, setDismissedAdIds] = useState(() => loadStoredAdIds(dismissedAdStorageKey)); const dismissedAdIdsRef = useRef(dismissedAdIds); @@ -83,6 +65,26 @@ export function AppLayout() { const adTriggers = useRef(new Map()); const closeAd = useCallback(() => setActiveAd(null), []); const visibleAds = ads.filter((ad) => ad.enabled && ad.placement === "menu" && !dismissedAdIds.has(ad.id)); + const menuItems: MenuItem[] = [ + { kind: "page", path: "/", label: t("数据概览"), icon: flatColorAreaChartIcon }, + { kind: "page", path: "/calls", label: t("调用详细"), icon: flatColorSalesPerformanceIcon }, + { kind: "page", path: "/providers", label: t("上游配置"), icon: flatColorOrganizationIcon }, + { kind: "external", id: "tutorial", label: t("使用教程"), icon: flatColorAboutIcon }, + { kind: "group", label: "Harness" }, + { kind: "page", path: "/settings", label: t("系统设置"), icon: flatColorSettingsIcon }, + { kind: "page", path: "/harness/cursor", label: t("Cursor 设置"), icon: cursorIconUrl }, + ]; + + const openTutorial = useCallback(() => { + setTutorialRead(true); + try { + localStorage.setItem(tutorialReadStorageKey, "true"); + } catch { + // Read state remains valid for the current session when storage is unavailable. + } + void api.openExternalUrl(tutorialUrl) + .catch((cause) => message(cause instanceof Error ? cause.message : String(cause))); + }, [message]); useEffect(() => { let disposed = false; @@ -93,7 +95,7 @@ export function AppLayout() { if (pending || now - lastRequestedAt < 10_000) return; pending = true; lastRequestedAt = now; - void api.ads(dismissedAdIdsRef.current) + void api.ads(dismissedAdIdsRef.current, locale) .then((runtime) => { if (disposed) return; setAds(runtime.slots); @@ -120,7 +122,7 @@ export function AppLayout() { window.removeEventListener("focus", refreshAds); document.removeEventListener("visibilitychange", refreshVisibleAds); }; - }, []); + }, [locale]); const openAd = useCallback((ad: AdSlot) => { setReadAdIds((current) => { @@ -178,13 +180,27 @@ export function AppLayout() {