refactor: extract i18n translation strings to dedicated i18n-strings.ts

Moves the STRINGS object (330 lines of zh/en translations) from i18n.ts
into a separate i18n-strings.ts file. i18n.ts now contains only the
resolveUiLanguage and translate logic (28 lines).

Change-Id: I9ddea79bddb4e8313dc16aae2a7c8d4eeff96fe1
This commit is contained in:
wujunchen 2026-04-27 15:13:31 +08:00
parent f4e0d84a9c
commit 15b471efad
3 changed files with 346 additions and 342 deletions

28
main.js

File diff suppressed because one or more lines are too long

330
src/i18n-strings.ts Normal file
View file

@ -0,0 +1,330 @@
'use strict';
export const STRINGS: Record<string, Record<string, string>> = {
zh: {
appTitle: '对照阅读笔记',
settingsTitle: 'Parallel Reader 设置',
emptyOpenNote: '打开一篇笔记,然后运行命令:',
emptyNoCache: '该笔记尚无对照笔记缓存。运行命令:',
commandGenerate: 'Parallel Reader: 为当前笔记生成对照笔记',
displayName: '对照阅读笔记',
ribbonOpen: '打开对照笔记面板',
cmdRun: '为当前笔记生成对照笔记(缓存优先)',
cmdRegen: '强制重新生成(绕过缓存)',
cmdOpenView: '打开对照笔记面板',
cmdExport: '导出当前对照笔记到 Vault',
cmdCopyMarkdown: '复制当前对照笔记 Markdown',
cmdCancel: '取消当前对照笔记生成',
cmdClearCurrent: '清除当前笔记的缓存',
cmdClearAll: '清除所有缓存',
cmdCardPrev: '聚焦上一张摘要卡片',
cmdCardNext: '聚焦下一张摘要卡片',
cmdCardJump: '跳转到当前摘要卡片原文',
actionCancel: '取消生成',
actionRegenerate: '重新生成',
actionCopyAll: '复制全部 Markdown',
actionExport: '导出到 Vault',
staleBanner: '源笔记或生成配置已修改,当前是旧缓存。',
loadingDefault: '正在生成对照笔记...',
loadingGenerating: '对照阅读:让 LLM 读全文并自适应切段...',
loadingSubtitle: '可以继续阅读原文,生成完成后会自动刷新右侧卡片。',
errorTitle: '生成失败',
actionCopyError: '复制错误信息',
emptyCard: '(未生成)',
anchorMismatch: 'anchor 匹配失败,无法滚动联动',
menuCopyMarkdown: '复制 Markdown',
menuCopyPlain: '复制纯文本',
menuCopyAnchor: '复制 anchor 引用',
menuJumpSource: '跳转到原文',
menuEditCard: '编辑此卡片',
menuDeleteCard: '删除此卡片',
copiedMarkdown: '已复制 Markdown',
copiedPlain: '已复制纯文本',
copiedAnchor: '已复制引用原文',
copiedAllMarkdown: '已复制全部 Markdown',
copyFailed: '复制失败:{error}',
actionFailed: '{label}失败:{error}',
exported: '已导出 → {path}',
noCurrentNote: '没有当前笔记',
cacheClearedFile: '已清除缓存:{name}',
cacheClearedAll: '已清除 {count} 条缓存',
noCancelableJob: '当前没有可取消的生成任务',
cancelRequested: '已请求取消生成',
cancelRequestedApiInFlight: '已请求取消生成;当前 API 请求无法立即中断,返回后会丢弃结果。',
actionGenerate: '生成对照笔记',
fileMenuGenerate: '生成对照笔记',
fileMenuRegen: '强制重新生成对照笔记',
fileMenuClear: '清除对照笔记缓存',
noExportContent: '当前没有可导出的对照笔记',
noCopyContent: '当前没有可复制的对照笔记',
noActiveCard: '当前没有可跳转的摘要卡片',
viewOpenFailed: '无法打开对照笔记面板',
confirmRegenerateEditedCards: '这篇笔记的对照卡片已被手动编辑。重新生成会覆盖这些修改,是否继续?',
regenerateCancelled: '已取消重新生成',
cardDeleted: '已删除此卡片',
cardSaved: '已保存此卡片',
editCardTitle: '编辑摘要卡片',
editCardTitleField: '标题',
editCardGistField: '领读',
editCardBulletsField: '要点(每行一条)',
editCardCancel: '取消',
editCardSave: '保存',
openNoteFirst: '先打开一篇笔记',
alreadyGenerating: '该笔记正在生成对照笔记',
emptyNote: '笔记为空',
longNoteTruncated: '笔记较长:仅发送前 {count} 个字符给模型',
generatingNotice: '对照阅读:让 LLM 读全文并自适应切段…',
noCardsReturned: 'LLM 未返回任何 card',
generationDone: '对照笔记生成完成:{count} 段{suffix}',
unanchoredSuffix: '(⚠ {count} 段 anchor 未匹配)',
cancelled: '已取消生成',
cancelledError: '生成已取消',
generationFailed: '生成失败{kind}{error}',
errorCustomProviderBaseUrlRequired: '自定义 provider 需要填写 API Base URL。',
errorApiBaseUrlMissing: 'API Base URL 未设置。请在设置里选择 provider 或填写自定义 base URL。',
errorModelMissing: 'Model 未设置。请在设置里填写模型 ID。',
errorApiKeyMissing: 'API key 未设置。请在设置里填写 API Key{hint}。',
errorApiKeyEnvHint: ' 或环境变量 {envVar}',
errorLlmNonJson: 'LLM 返回非 JSON\n{excerpt}',
errorCustomHeadersJsonParse: '自定义 headers JSON 解析失败:{error}',
errorCustomHeadersJsonObject: '自定义 headers JSON 必须是对象',
errorCustomHeadersLineFormat: '自定义 headers 每行格式应为 `Header-Name: value`',
errorProviderNonJson: '{label} 返回非 JSON\n{excerpt}',
errorProviderRequestFailed: '{label} 请求失败:{error}',
errorProviderApiStatus: '{label} API 返回 HTTP {status}{excerpt}',
noEditor: '找不到源笔记对应的编辑器窗口',
settingUiLanguageName: '界面语言',
settingUiLanguageDesc: '控制插件界面、命令和提示文案Auto 跟随 Obsidian/系统语言',
settingBackendName: 'Backend',
settingBackendDesc: '生成 bullet 的后端CLI 复用本机登录API 支持 OpenAI/Anthropic/Gemini 及兼容代理',
settingCliPathName: 'CLI 路径(可选)',
settingCliPathDesc: '留空则自动探测常见位置Obsidian GUI 启动时不继承 shell PATH必要时填绝对路径',
settingCliPathPlaceholder: '例:/Users/you/bin/codex',
settingCliTimeoutName: 'CLI 超时 (ms)',
apiProviderHeader: 'API Provider',
settingProviderPresetName: 'Provider preset',
settingProviderPresetDesc: '参考 OpenClaw 的 provider/model 思路preset 只负责协议、base URL 和认证默认值',
settingApiFormatName: 'API format',
settingApiFormatDesc: '不同 provider 的 wire protocolOpenAI-compatible 代理通常选 Chat Completions',
settingBaseUrlName: 'Base URL',
settingBaseUrlDesc: '填 provider 根地址,不要附加 /chat/completions留空时使用 preset 默认值',
settingApiKeyName: 'API Key',
settingApiKeyDesc: '本地 Ollama/LM Studio 可留空;其他 provider 通常需要 key',
settingApiKeyEnvName: 'API Key 环境变量',
settingApiKeyEnvDesc: '可选Obsidian GUI 不一定继承 shell 环境,直接填 API Key 更稳定',
settingAuthTypeName: '认证方式',
settingAuthTypeDesc: 'Auto 使用 provider preset自定义代理可按需要改成 Bearer、x-api-key 或 none',
settingHeadersName: '额外 headers',
settingHeadersDesc: '可选。支持 JSON 对象或每行 `Header: value`,用于 Cloudflare AI Gateway 等代理',
settingMaxTokensName: '最大输出 tokens',
settingModelName: 'Model',
settingModelDescApi:
'API 调用的模型 ID支持 OpenClaw 风格 provider/model若 provider 前缀匹配当前 preset 会自动剥离',
settingModelDescCli: 'Claude Code 下会传 --modelCodex 下通常忽略(用 Codex 默认配置)',
settingMaxInputName: '最大输入字符数',
settingMaxInputDesc: '超过该长度会截断后再发送给模型;长上下文模型可适当调大',
promptHeader: 'Prompt',
settingPromptLanguageName: '输出语言',
settingPromptLanguageDesc: '控制 title/gist/bullets 的语言anchor 始终逐字复制原文',
settingCardRangeName: '卡片数量范围',
settingCardRangeDesc: '模型会在这个范围内自适应切段',
settingCustomPromptName: '自定义 system prompt',
settingCustomPromptDesc: '可选。支持变量:{minCards}、{maxCards}、{languageInstruction}、{schema}、{example}',
settingCustomPromptPlaceholder: '留空使用内置 prompt',
settingTestBackendName: '测试当前后端',
settingTestBackendDescApi: '会发起一次最小 LLM 请求验证 API 设置',
settingTestBackendDescCli: '调用 `<cli> --version` 验证 spawn 能找到二进制',
backendTestFailed: '✗ 后端测试失败:{error}',
settingExportFolderName: '导出文件夹',
settingExportFolderDesc: '对照笔记生成位置(相对 Vault 根)',
cacheHeader: '缓存',
settingMaxCacheName: '最大缓存篇数',
settingMaxCacheDesc: '超过上限后按最近访问时间淘汰最旧的笔记缓存;缓存保存在插件目录的 cache.json',
cachePruned: '已淘汰 {count} 条旧缓存',
cachedNotesName: '已缓存笔记:{count} 篇',
cachedNotesDesc: '缓存以源笔记 SHA1 + 生成配置指纹作为失效键,源笔记或模型配置修改后会显示 stale 提示',
clearAllCacheButton: '清除所有缓存',
settingStreamingName: '流式输出',
settingStreamingDesc: '启用后生成时实时显示 LLM 输出进度(仅 OpenAI Chat 和 Anthropic 格式支持)',
settingStreamingTimeoutName: '流式超时 (ms)',
settingStreamingTimeoutDesc: '单次流式请求的超时时间;低于 1000ms 或非法值会恢复默认值',
cmdBatchGenerate: '批量生成对照笔记(当前文件夹)',
batchSelectFolder: '请输入要批量处理的文件夹路径(留空为 Vault 根目录)',
batchFolderPrompt: '文件夹路径',
batchAlreadyRunning: '批量生成正在运行',
batchCancelRequested: '已请求取消批量生成;当前文件结束后停止',
batchInvalidFolderInput: '文件夹路径无效:{path}',
batchFolderNotFound: '文件夹不存在:{path}',
batchNoMarkdown: '该文件夹下没有 Markdown 文件',
batchProgress: '批量生成:{current}/{total}',
batchDone: '批量生成完成:共处理 {processed}/{total} 篇,跳过缓存 {skipped} 篇,失败 {errors} 篇',
batchCancelled: '批量生成已取消:已处理 {processed}/{total} 篇,跳过缓存 {skipped} 篇,失败 {errors} 篇',
},
en: {
appTitle: 'Parallel Reader',
settingsTitle: 'Parallel Reader Settings',
emptyOpenNote: 'Open a note, then run:',
emptyNoCache: 'This note has no cached parallel notes. Run:',
commandGenerate: 'Parallel Reader: Generate notes for current note',
displayName: 'Parallel Reader',
ribbonOpen: 'Open Parallel Reader pane',
cmdRun: 'Generate parallel notes for current note (cache first)',
cmdRegen: 'Regenerate parallel notes (bypass cache)',
cmdOpenView: 'Open Parallel Reader pane',
cmdExport: 'Export current parallel notes to Vault',
cmdCopyMarkdown: 'Copy current parallel notes as Markdown',
cmdCancel: 'Cancel current parallel-note generation',
cmdClearCurrent: 'Clear cache for current note',
cmdClearAll: 'Clear all caches',
cmdCardPrev: 'Focus previous summary card',
cmdCardNext: 'Focus next summary card',
cmdCardJump: 'Jump current summary card to source',
actionCancel: 'Cancel generation',
actionRegenerate: 'Regenerate',
actionCopyAll: 'Copy all Markdown',
actionExport: 'Export to Vault',
staleBanner: 'The source note or generation settings changed. This is stale cache.',
loadingDefault: 'Generating parallel notes...',
loadingGenerating: 'Parallel Reader: asking the LLM to read and segment the full note...',
loadingSubtitle: 'You can keep reading. Cards will refresh when generation finishes.',
errorTitle: 'Generation failed',
actionCopyError: 'Copy error',
emptyCard: '(not generated)',
anchorMismatch: 'Anchor did not match; scroll sync is unavailable',
menuCopyMarkdown: 'Copy Markdown',
menuCopyPlain: 'Copy plain text',
menuCopyAnchor: 'Copy anchor quote',
menuJumpSource: 'Jump to source',
menuEditCard: 'Edit this card',
menuDeleteCard: 'Delete this card',
copiedMarkdown: 'Copied Markdown',
copiedPlain: 'Copied plain text',
copiedAnchor: 'Copied anchor quote',
copiedAllMarkdown: 'Copied all Markdown',
copyFailed: 'Copy failed: {error}',
actionFailed: '{label} failed: {error}',
exported: 'Exported → {path}',
noCurrentNote: 'No current note',
cacheClearedFile: 'Cleared cache: {name}',
cacheClearedAll: 'Cleared {count} cache entries',
noCancelableJob: 'No cancellable generation job',
cancelRequested: 'Cancel requested',
cancelRequestedApiInFlight:
'Cancel requested. The in-flight API request cannot be aborted immediately; its result will be ignored.',
actionGenerate: 'Generate parallel notes',
fileMenuGenerate: 'Generate parallel notes',
fileMenuRegen: 'Regenerate parallel notes',
fileMenuClear: 'Clear parallel-note cache',
noExportContent: 'No parallel notes to export',
noCopyContent: 'No parallel notes to copy',
noActiveCard: 'No active summary card to jump',
viewOpenFailed: 'Could not open the Parallel Reader pane',
confirmRegenerateEditedCards:
'These parallel-reader cards were edited manually. Regenerating will overwrite those edits. Continue?',
regenerateCancelled: 'Regeneration cancelled',
cardDeleted: 'Deleted this card',
cardSaved: 'Saved this card',
editCardTitle: 'Edit summary card',
editCardTitleField: 'Title',
editCardGistField: 'Gist',
editCardBulletsField: 'Bullets (one per line)',
editCardCancel: 'Cancel',
editCardSave: 'Save',
openNoteFirst: 'Open a note first',
alreadyGenerating: 'This note is already being generated',
emptyNote: 'The note is empty',
longNoteTruncated: 'Long note: only the first {count} characters will be sent to the model',
generatingNotice: 'Parallel Reader: asking the LLM to read and segment the full note...',
noCardsReturned: 'LLM returned no cards',
generationDone: 'Generated {count} sections{suffix}',
unanchoredSuffix: ' (⚠ {count} anchors unmatched)',
cancelled: 'Generation cancelled',
cancelledError: 'Generation cancelled',
generationFailed: 'Generation failed{kind}: {error}',
errorCustomProviderBaseUrlRequired: 'Custom provider requires an API Base URL.',
errorApiBaseUrlMissing: 'API Base URL is not set. Choose a provider or enter a custom base URL in settings.',
errorModelMissing: 'Model is not set. Enter a model ID in settings.',
errorApiKeyMissing: 'API key is not set. Enter an API Key in settings{hint}.',
errorApiKeyEnvHint: ' or set environment variable {envVar}',
errorLlmNonJson: 'LLM returned non-JSON:\n{excerpt}',
errorCustomHeadersJsonParse: 'Custom headers JSON parse failed: {error}',
errorCustomHeadersJsonObject: 'Custom headers JSON must be an object',
errorCustomHeadersLineFormat: 'Custom headers lines must use `Header-Name: value`',
errorProviderNonJson: '{label} returned non-JSON:\n{excerpt}',
errorProviderRequestFailed: '{label} request failed: {error}',
errorProviderApiStatus: '{label} API returned HTTP {status}: {excerpt}',
noEditor: 'Could not find the source note editor',
settingUiLanguageName: 'UI language',
settingUiLanguageDesc: 'Controls plugin UI, commands, and notices. Auto follows Obsidian/system language.',
settingBackendName: 'Backend',
settingBackendDesc:
'Backend for generating bullets: CLI reuses local login; API supports OpenAI, Anthropic, Gemini, and compatible proxies.',
settingCliPathName: 'CLI path (optional)',
settingCliPathDesc:
'Leave blank to auto-detect common paths. Obsidian launched from the GUI may not inherit shell PATH.',
settingCliPathPlaceholder: 'Example: /Users/you/bin/codex',
settingCliTimeoutName: 'CLI timeout (ms)',
apiProviderHeader: 'API Provider',
settingProviderPresetName: 'Provider preset',
settingProviderPresetDesc:
'OpenClaw-style provider/model setup: presets define protocol, base URL, and auth defaults.',
settingApiFormatName: 'API format',
settingApiFormatDesc: 'Wire protocol for the provider. OpenAI-compatible proxies usually use Chat Completions.',
settingBaseUrlName: 'Base URL',
settingBaseUrlDesc: 'Provider root URL, without /chat/completions. Blank uses the preset default.',
settingApiKeyName: 'API Key',
settingApiKeyDesc: 'Can be blank for local Ollama/LM Studio; most hosted providers require a key.',
settingApiKeyEnvName: 'API key env var',
settingApiKeyEnvDesc: 'Optional. Obsidian GUI may not inherit shell env; direct API Key is usually more reliable.',
settingAuthTypeName: 'Auth type',
settingAuthTypeDesc: 'Auto uses provider preset. Custom proxies can use Bearer, x-api-key, or none.',
settingHeadersName: 'Extra headers',
settingHeadersDesc: 'Optional. JSON object or one `Header: value` per line, useful for Cloudflare AI Gateway.',
settingMaxTokensName: 'Max output tokens',
settingModelName: 'Model',
settingModelDescApi:
'Model ID for API calls. Supports OpenClaw-style provider/model; matching provider prefixes are stripped.',
settingModelDescCli: 'Passed as --model for Claude Code. Usually ignored by Codex, which uses its default config.',
settingMaxInputName: 'Max input characters',
settingMaxInputDesc: 'Longer notes are truncated before sending to the model. Raise this for long-context models.',
promptHeader: 'Prompt',
settingPromptLanguageName: 'Output language',
settingPromptLanguageDesc: 'Controls title/gist/bullets language. Anchor is always copied verbatim from source.',
settingCardRangeName: 'Card count range',
settingCardRangeDesc: 'The model adapts segmentation within this range.',
settingCustomPromptName: 'Custom system prompt',
settingCustomPromptDesc: 'Optional. Variables: {minCards}, {maxCards}, {languageInstruction}, {schema}, {example}',
settingCustomPromptPlaceholder: 'Leave blank to use built-in prompt',
settingTestBackendName: 'Test current backend',
settingTestBackendDescApi: 'Sends a minimal LLM request to validate API settings.',
settingTestBackendDescCli: 'Runs `<cli> --version` to verify the binary can be spawned.',
backendTestFailed: '✗ Backend test failed: {error}',
settingExportFolderName: 'Export folder',
settingExportFolderDesc: 'Parallel-note output location, relative to the Vault root.',
cacheHeader: 'Cache',
settingMaxCacheName: 'Max cached notes',
settingMaxCacheDesc:
'Prunes least-recently accessed note caches above this limit. Cache is stored in plugin cache.json.',
cachePruned: 'Pruned {count} old cache entries',
cachedNotesName: 'Cached notes: {count}',
cachedNotesDesc: 'Cache is invalidated by source SHA1 and generation settings fingerprint.',
clearAllCacheButton: 'Clear all caches',
settingStreamingName: 'Streaming output',
settingStreamingDesc:
'Show real-time LLM output progress during generation (OpenAI Chat and Anthropic formats only)',
settingStreamingTimeoutName: 'Streaming timeout (ms)',
settingStreamingTimeoutDesc:
'Timeout for one streaming request; invalid values or values below 1000ms use the default',
cmdBatchGenerate: 'Batch generate parallel notes (current folder)',
batchSelectFolder: 'Enter the folder path to batch-process (leave blank for Vault root)',
batchFolderPrompt: 'Folder path',
batchAlreadyRunning: 'Batch generation is already running',
batchCancelRequested: 'Batch cancellation requested; it will stop after the current note',
batchInvalidFolderInput: 'Invalid folder path: {path}',
batchFolderNotFound: 'Folder not found: {path}',
batchNoMarkdown: 'No Markdown files found in that folder',
batchProgress: 'Batch generating: {current}/{total}',
batchDone: 'Batch complete: processed {processed}/{total}, skipped {skipped} (cached), errors {errors}',
batchCancelled: 'Batch cancelled: processed {processed}/{total}, skipped {skipped} (cached), errors {errors}',
},
};

View file

@ -1,335 +1,9 @@
'use strict';
import { STRINGS } from './i18n-strings';
import type { PluginSettings } from './types';
export const STRINGS: Record<string, Record<string, string>> = {
zh: {
appTitle: '对照阅读笔记',
settingsTitle: 'Parallel Reader 设置',
emptyOpenNote: '打开一篇笔记,然后运行命令:',
emptyNoCache: '该笔记尚无对照笔记缓存。运行命令:',
commandGenerate: 'Parallel Reader: 为当前笔记生成对照笔记',
displayName: '对照阅读笔记',
ribbonOpen: '打开对照笔记面板',
cmdRun: '为当前笔记生成对照笔记(缓存优先)',
cmdRegen: '强制重新生成(绕过缓存)',
cmdOpenView: '打开对照笔记面板',
cmdExport: '导出当前对照笔记到 Vault',
cmdCopyMarkdown: '复制当前对照笔记 Markdown',
cmdCancel: '取消当前对照笔记生成',
cmdClearCurrent: '清除当前笔记的缓存',
cmdClearAll: '清除所有缓存',
cmdCardPrev: '聚焦上一张摘要卡片',
cmdCardNext: '聚焦下一张摘要卡片',
cmdCardJump: '跳转到当前摘要卡片原文',
actionCancel: '取消生成',
actionRegenerate: '重新生成',
actionCopyAll: '复制全部 Markdown',
actionExport: '导出到 Vault',
staleBanner: '源笔记或生成配置已修改,当前是旧缓存。',
loadingDefault: '正在生成对照笔记...',
loadingGenerating: '对照阅读:让 LLM 读全文并自适应切段...',
loadingSubtitle: '可以继续阅读原文,生成完成后会自动刷新右侧卡片。',
errorTitle: '生成失败',
actionCopyError: '复制错误信息',
emptyCard: '(未生成)',
anchorMismatch: 'anchor 匹配失败,无法滚动联动',
menuCopyMarkdown: '复制 Markdown',
menuCopyPlain: '复制纯文本',
menuCopyAnchor: '复制 anchor 引用',
menuJumpSource: '跳转到原文',
menuEditCard: '编辑此卡片',
menuDeleteCard: '删除此卡片',
copiedMarkdown: '已复制 Markdown',
copiedPlain: '已复制纯文本',
copiedAnchor: '已复制引用原文',
copiedAllMarkdown: '已复制全部 Markdown',
copyFailed: '复制失败:{error}',
actionFailed: '{label}失败:{error}',
exported: '已导出 → {path}',
noCurrentNote: '没有当前笔记',
cacheClearedFile: '已清除缓存:{name}',
cacheClearedAll: '已清除 {count} 条缓存',
noCancelableJob: '当前没有可取消的生成任务',
cancelRequested: '已请求取消生成',
cancelRequestedApiInFlight: '已请求取消生成;当前 API 请求无法立即中断,返回后会丢弃结果。',
actionGenerate: '生成对照笔记',
fileMenuGenerate: '生成对照笔记',
fileMenuRegen: '强制重新生成对照笔记',
fileMenuClear: '清除对照笔记缓存',
noExportContent: '当前没有可导出的对照笔记',
noCopyContent: '当前没有可复制的对照笔记',
noActiveCard: '当前没有可跳转的摘要卡片',
viewOpenFailed: '无法打开对照笔记面板',
confirmRegenerateEditedCards: '这篇笔记的对照卡片已被手动编辑。重新生成会覆盖这些修改,是否继续?',
regenerateCancelled: '已取消重新生成',
cardDeleted: '已删除此卡片',
cardSaved: '已保存此卡片',
editCardTitle: '编辑摘要卡片',
editCardTitleField: '标题',
editCardGistField: '领读',
editCardBulletsField: '要点(每行一条)',
editCardCancel: '取消',
editCardSave: '保存',
openNoteFirst: '先打开一篇笔记',
alreadyGenerating: '该笔记正在生成对照笔记',
emptyNote: '笔记为空',
longNoteTruncated: '笔记较长:仅发送前 {count} 个字符给模型',
generatingNotice: '对照阅读:让 LLM 读全文并自适应切段…',
noCardsReturned: 'LLM 未返回任何 card',
generationDone: '对照笔记生成完成:{count} 段{suffix}',
unanchoredSuffix: '(⚠ {count} 段 anchor 未匹配)',
cancelled: '已取消生成',
cancelledError: '生成已取消',
generationFailed: '生成失败{kind}{error}',
errorCustomProviderBaseUrlRequired: '自定义 provider 需要填写 API Base URL。',
errorApiBaseUrlMissing: 'API Base URL 未设置。请在设置里选择 provider 或填写自定义 base URL。',
errorModelMissing: 'Model 未设置。请在设置里填写模型 ID。',
errorApiKeyMissing: 'API key 未设置。请在设置里填写 API Key{hint}。',
errorApiKeyEnvHint: ' 或环境变量 {envVar}',
errorLlmNonJson: 'LLM 返回非 JSON\n{excerpt}',
errorCustomHeadersJsonParse: '自定义 headers JSON 解析失败:{error}',
errorCustomHeadersJsonObject: '自定义 headers JSON 必须是对象',
errorCustomHeadersLineFormat: '自定义 headers 每行格式应为 `Header-Name: value`',
errorProviderNonJson: '{label} 返回非 JSON\n{excerpt}',
errorProviderRequestFailed: '{label} 请求失败:{error}',
errorProviderApiStatus: '{label} API 返回 HTTP {status}{excerpt}',
noEditor: '找不到源笔记对应的编辑器窗口',
settingUiLanguageName: '界面语言',
settingUiLanguageDesc: '控制插件界面、命令和提示文案Auto 跟随 Obsidian/系统语言',
settingBackendName: 'Backend',
settingBackendDesc: '生成 bullet 的后端CLI 复用本机登录API 支持 OpenAI/Anthropic/Gemini 及兼容代理',
settingCliPathName: 'CLI 路径(可选)',
settingCliPathDesc: '留空则自动探测常见位置Obsidian GUI 启动时不继承 shell PATH必要时填绝对路径',
settingCliPathPlaceholder: '例:/Users/you/bin/codex',
settingCliTimeoutName: 'CLI 超时 (ms)',
apiProviderHeader: 'API Provider',
settingProviderPresetName: 'Provider preset',
settingProviderPresetDesc: '参考 OpenClaw 的 provider/model 思路preset 只负责协议、base URL 和认证默认值',
settingApiFormatName: 'API format',
settingApiFormatDesc: '不同 provider 的 wire protocolOpenAI-compatible 代理通常选 Chat Completions',
settingBaseUrlName: 'Base URL',
settingBaseUrlDesc: '填 provider 根地址,不要附加 /chat/completions留空时使用 preset 默认值',
settingApiKeyName: 'API Key',
settingApiKeyDesc: '本地 Ollama/LM Studio 可留空;其他 provider 通常需要 key',
settingApiKeyEnvName: 'API Key 环境变量',
settingApiKeyEnvDesc: '可选Obsidian GUI 不一定继承 shell 环境,直接填 API Key 更稳定',
settingAuthTypeName: '认证方式',
settingAuthTypeDesc: 'Auto 使用 provider preset自定义代理可按需要改成 Bearer、x-api-key 或 none',
settingHeadersName: '额外 headers',
settingHeadersDesc: '可选。支持 JSON 对象或每行 `Header: value`,用于 Cloudflare AI Gateway 等代理',
settingMaxTokensName: '最大输出 tokens',
settingModelName: 'Model',
settingModelDescApi:
'API 调用的模型 ID支持 OpenClaw 风格 provider/model若 provider 前缀匹配当前 preset 会自动剥离',
settingModelDescCli: 'Claude Code 下会传 --modelCodex 下通常忽略(用 Codex 默认配置)',
settingMaxInputName: '最大输入字符数',
settingMaxInputDesc: '超过该长度会截断后再发送给模型;长上下文模型可适当调大',
promptHeader: 'Prompt',
settingPromptLanguageName: '输出语言',
settingPromptLanguageDesc: '控制 title/gist/bullets 的语言anchor 始终逐字复制原文',
settingCardRangeName: '卡片数量范围',
settingCardRangeDesc: '模型会在这个范围内自适应切段',
settingCustomPromptName: '自定义 system prompt',
settingCustomPromptDesc: '可选。支持变量:{minCards}、{maxCards}、{languageInstruction}、{schema}、{example}',
settingCustomPromptPlaceholder: '留空使用内置 prompt',
settingTestBackendName: '测试当前后端',
settingTestBackendDescApi: '会发起一次最小 LLM 请求验证 API 设置',
settingTestBackendDescCli: '调用 `<cli> --version` 验证 spawn 能找到二进制',
backendTestFailed: '✗ 后端测试失败:{error}',
settingExportFolderName: '导出文件夹',
settingExportFolderDesc: '对照笔记生成位置(相对 Vault 根)',
cacheHeader: '缓存',
settingMaxCacheName: '最大缓存篇数',
settingMaxCacheDesc: '超过上限后按最近访问时间淘汰最旧的笔记缓存;缓存保存在插件目录的 cache.json',
cachePruned: '已淘汰 {count} 条旧缓存',
cachedNotesName: '已缓存笔记:{count} 篇',
cachedNotesDesc: '缓存以源笔记 SHA1 + 生成配置指纹作为失效键,源笔记或模型配置修改后会显示 stale 提示',
clearAllCacheButton: '清除所有缓存',
settingStreamingName: '流式输出',
settingStreamingDesc: '启用后生成时实时显示 LLM 输出进度(仅 OpenAI Chat 和 Anthropic 格式支持)',
settingStreamingTimeoutName: '流式超时 (ms)',
settingStreamingTimeoutDesc: '单次流式请求的超时时间;低于 1000ms 或非法值会恢复默认值',
cmdBatchGenerate: '批量生成对照笔记(当前文件夹)',
batchSelectFolder: '请输入要批量处理的文件夹路径(留空为 Vault 根目录)',
batchFolderPrompt: '文件夹路径',
batchAlreadyRunning: '批量生成正在运行',
batchCancelRequested: '已请求取消批量生成;当前文件结束后停止',
batchInvalidFolderInput: '文件夹路径无效:{path}',
batchFolderNotFound: '文件夹不存在:{path}',
batchNoMarkdown: '该文件夹下没有 Markdown 文件',
batchProgress: '批量生成:{current}/{total}',
batchDone: '批量生成完成:共处理 {processed}/{total} 篇,跳过缓存 {skipped} 篇,失败 {errors} 篇',
batchCancelled: '批量生成已取消:已处理 {processed}/{total} 篇,跳过缓存 {skipped} 篇,失败 {errors} 篇',
},
en: {
appTitle: 'Parallel Reader',
settingsTitle: 'Parallel Reader Settings',
emptyOpenNote: 'Open a note, then run:',
emptyNoCache: 'This note has no cached parallel notes. Run:',
commandGenerate: 'Parallel Reader: Generate notes for current note',
displayName: 'Parallel Reader',
ribbonOpen: 'Open Parallel Reader pane',
cmdRun: 'Generate parallel notes for current note (cache first)',
cmdRegen: 'Regenerate parallel notes (bypass cache)',
cmdOpenView: 'Open Parallel Reader pane',
cmdExport: 'Export current parallel notes to Vault',
cmdCopyMarkdown: 'Copy current parallel notes as Markdown',
cmdCancel: 'Cancel current parallel-note generation',
cmdClearCurrent: 'Clear cache for current note',
cmdClearAll: 'Clear all caches',
cmdCardPrev: 'Focus previous summary card',
cmdCardNext: 'Focus next summary card',
cmdCardJump: 'Jump current summary card to source',
actionCancel: 'Cancel generation',
actionRegenerate: 'Regenerate',
actionCopyAll: 'Copy all Markdown',
actionExport: 'Export to Vault',
staleBanner: 'The source note or generation settings changed. This is stale cache.',
loadingDefault: 'Generating parallel notes...',
loadingGenerating: 'Parallel Reader: asking the LLM to read and segment the full note...',
loadingSubtitle: 'You can keep reading. Cards will refresh when generation finishes.',
errorTitle: 'Generation failed',
actionCopyError: 'Copy error',
emptyCard: '(not generated)',
anchorMismatch: 'Anchor did not match; scroll sync is unavailable',
menuCopyMarkdown: 'Copy Markdown',
menuCopyPlain: 'Copy plain text',
menuCopyAnchor: 'Copy anchor quote',
menuJumpSource: 'Jump to source',
menuEditCard: 'Edit this card',
menuDeleteCard: 'Delete this card',
copiedMarkdown: 'Copied Markdown',
copiedPlain: 'Copied plain text',
copiedAnchor: 'Copied anchor quote',
copiedAllMarkdown: 'Copied all Markdown',
copyFailed: 'Copy failed: {error}',
actionFailed: '{label} failed: {error}',
exported: 'Exported → {path}',
noCurrentNote: 'No current note',
cacheClearedFile: 'Cleared cache: {name}',
cacheClearedAll: 'Cleared {count} cache entries',
noCancelableJob: 'No cancellable generation job',
cancelRequested: 'Cancel requested',
cancelRequestedApiInFlight:
'Cancel requested. The in-flight API request cannot be aborted immediately; its result will be ignored.',
actionGenerate: 'Generate parallel notes',
fileMenuGenerate: 'Generate parallel notes',
fileMenuRegen: 'Regenerate parallel notes',
fileMenuClear: 'Clear parallel-note cache',
noExportContent: 'No parallel notes to export',
noCopyContent: 'No parallel notes to copy',
noActiveCard: 'No active summary card to jump',
viewOpenFailed: 'Could not open the Parallel Reader pane',
confirmRegenerateEditedCards:
'These parallel-reader cards were edited manually. Regenerating will overwrite those edits. Continue?',
regenerateCancelled: 'Regeneration cancelled',
cardDeleted: 'Deleted this card',
cardSaved: 'Saved this card',
editCardTitle: 'Edit summary card',
editCardTitleField: 'Title',
editCardGistField: 'Gist',
editCardBulletsField: 'Bullets (one per line)',
editCardCancel: 'Cancel',
editCardSave: 'Save',
openNoteFirst: 'Open a note first',
alreadyGenerating: 'This note is already being generated',
emptyNote: 'The note is empty',
longNoteTruncated: 'Long note: only the first {count} characters will be sent to the model',
generatingNotice: 'Parallel Reader: asking the LLM to read and segment the full note...',
noCardsReturned: 'LLM returned no cards',
generationDone: 'Generated {count} sections{suffix}',
unanchoredSuffix: ' (⚠ {count} anchors unmatched)',
cancelled: 'Generation cancelled',
cancelledError: 'Generation cancelled',
generationFailed: 'Generation failed{kind}: {error}',
errorCustomProviderBaseUrlRequired: 'Custom provider requires an API Base URL.',
errorApiBaseUrlMissing: 'API Base URL is not set. Choose a provider or enter a custom base URL in settings.',
errorModelMissing: 'Model is not set. Enter a model ID in settings.',
errorApiKeyMissing: 'API key is not set. Enter an API Key in settings{hint}.',
errorApiKeyEnvHint: ' or set environment variable {envVar}',
errorLlmNonJson: 'LLM returned non-JSON:\n{excerpt}',
errorCustomHeadersJsonParse: 'Custom headers JSON parse failed: {error}',
errorCustomHeadersJsonObject: 'Custom headers JSON must be an object',
errorCustomHeadersLineFormat: 'Custom headers lines must use `Header-Name: value`',
errorProviderNonJson: '{label} returned non-JSON:\n{excerpt}',
errorProviderRequestFailed: '{label} request failed: {error}',
errorProviderApiStatus: '{label} API returned HTTP {status}: {excerpt}',
noEditor: 'Could not find the source note editor',
settingUiLanguageName: 'UI language',
settingUiLanguageDesc: 'Controls plugin UI, commands, and notices. Auto follows Obsidian/system language.',
settingBackendName: 'Backend',
settingBackendDesc:
'Backend for generating bullets: CLI reuses local login; API supports OpenAI, Anthropic, Gemini, and compatible proxies.',
settingCliPathName: 'CLI path (optional)',
settingCliPathDesc:
'Leave blank to auto-detect common paths. Obsidian launched from the GUI may not inherit shell PATH.',
settingCliPathPlaceholder: 'Example: /Users/you/bin/codex',
settingCliTimeoutName: 'CLI timeout (ms)',
apiProviderHeader: 'API Provider',
settingProviderPresetName: 'Provider preset',
settingProviderPresetDesc:
'OpenClaw-style provider/model setup: presets define protocol, base URL, and auth defaults.',
settingApiFormatName: 'API format',
settingApiFormatDesc: 'Wire protocol for the provider. OpenAI-compatible proxies usually use Chat Completions.',
settingBaseUrlName: 'Base URL',
settingBaseUrlDesc: 'Provider root URL, without /chat/completions. Blank uses the preset default.',
settingApiKeyName: 'API Key',
settingApiKeyDesc: 'Can be blank for local Ollama/LM Studio; most hosted providers require a key.',
settingApiKeyEnvName: 'API key env var',
settingApiKeyEnvDesc: 'Optional. Obsidian GUI may not inherit shell env; direct API Key is usually more reliable.',
settingAuthTypeName: 'Auth type',
settingAuthTypeDesc: 'Auto uses provider preset. Custom proxies can use Bearer, x-api-key, or none.',
settingHeadersName: 'Extra headers',
settingHeadersDesc: 'Optional. JSON object or one `Header: value` per line, useful for Cloudflare AI Gateway.',
settingMaxTokensName: 'Max output tokens',
settingModelName: 'Model',
settingModelDescApi:
'Model ID for API calls. Supports OpenClaw-style provider/model; matching provider prefixes are stripped.',
settingModelDescCli: 'Passed as --model for Claude Code. Usually ignored by Codex, which uses its default config.',
settingMaxInputName: 'Max input characters',
settingMaxInputDesc: 'Longer notes are truncated before sending to the model. Raise this for long-context models.',
promptHeader: 'Prompt',
settingPromptLanguageName: 'Output language',
settingPromptLanguageDesc: 'Controls title/gist/bullets language. Anchor is always copied verbatim from source.',
settingCardRangeName: 'Card count range',
settingCardRangeDesc: 'The model adapts segmentation within this range.',
settingCustomPromptName: 'Custom system prompt',
settingCustomPromptDesc: 'Optional. Variables: {minCards}, {maxCards}, {languageInstruction}, {schema}, {example}',
settingCustomPromptPlaceholder: 'Leave blank to use built-in prompt',
settingTestBackendName: 'Test current backend',
settingTestBackendDescApi: 'Sends a minimal LLM request to validate API settings.',
settingTestBackendDescCli: 'Runs `<cli> --version` to verify the binary can be spawned.',
backendTestFailed: '✗ Backend test failed: {error}',
settingExportFolderName: 'Export folder',
settingExportFolderDesc: 'Parallel-note output location, relative to the Vault root.',
cacheHeader: 'Cache',
settingMaxCacheName: 'Max cached notes',
settingMaxCacheDesc:
'Prunes least-recently accessed note caches above this limit. Cache is stored in plugin cache.json.',
cachePruned: 'Pruned {count} old cache entries',
cachedNotesName: 'Cached notes: {count}',
cachedNotesDesc: 'Cache is invalidated by source SHA1 and generation settings fingerprint.',
clearAllCacheButton: 'Clear all caches',
settingStreamingName: 'Streaming output',
settingStreamingDesc:
'Show real-time LLM output progress during generation (OpenAI Chat and Anthropic formats only)',
settingStreamingTimeoutName: 'Streaming timeout (ms)',
settingStreamingTimeoutDesc:
'Timeout for one streaming request; invalid values or values below 1000ms use the default',
cmdBatchGenerate: 'Batch generate parallel notes (current folder)',
batchSelectFolder: 'Enter the folder path to batch-process (leave blank for Vault root)',
batchFolderPrompt: 'Folder path',
batchAlreadyRunning: 'Batch generation is already running',
batchCancelRequested: 'Batch cancellation requested; it will stop after the current note',
batchInvalidFolderInput: 'Invalid folder path: {path}',
batchFolderNotFound: 'Folder not found: {path}',
batchNoMarkdown: 'No Markdown files found in that folder',
batchProgress: 'Batch generating: {current}/{total}',
batchDone: 'Batch complete: processed {processed}/{total}, skipped {skipped} (cached), errors {errors}',
batchCancelled: 'Batch cancelled: processed {processed}/{total}, skipped {skipped} (cached), errors {errors}',
},
};
export { STRINGS } from './i18n-strings';
export function resolveUiLanguage(settings: Pick<PluginSettings, 'uiLanguage'> | null): string {
const configured = settings?.uiLanguage;