diff --git a/README.md b/README.md index 578e5b8..46d1dbb 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ Inspired by [this reading workflow demo](https://www.bilibili.com/video/BV1FxoGB - **Rich rendering** — cards render through Obsidian's `MarkdownRenderer`, so tables, bold, code, and wikilinks all work natively. - **Card editing** — right-click any card to copy, edit, delete, or jump to source. - **Export** — save cards as a Markdown note in your vault, or copy to clipboard. -- **Bilingual UI** — full Chinese and English support for commands, settings, and notices. +- **Multi-language UI and output** — UI supports Auto, Chinese, English, Japanese, Korean, French, German, and Spanish; generated titles, gists, and bullets can use the same fixed languages or follow the source document. ## Quick Start diff --git a/README.zh-CN.md b/README.zh-CN.md index 8b4a2dc..1074cec 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -39,7 +39,7 @@ - **Markdown 渲染** — 通过 Obsidian 的 `MarkdownRenderer` 渲染,表格、加粗、代码、wikilink 均正常显示。 - **卡片编辑** — 右键任意卡片:复制、编辑、删除、跳转原文。 - **导出** — 保存为 Vault 中的 Markdown 文件,或复制到剪贴板。 -- **中英双语 UI** — 命令、设置、面板文案全部支持中文和英文。 +- **多语言 UI 与输出** — UI 支持 Auto、中文、英文、日文、韩文、法文、德文、西班牙文;生成的 title/gist/bullets 也可选择这些固定语言,或跟随原文语言。 ## 快速开始 diff --git a/manifest.json b/manifest.json index b825ea5..134bbe5 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "id": "parallel-reader", "name": "Parallel Reader", - "version": "1.0.22", + "version": "1.0.23", "minAppVersion": "1.8.7", "description": "AI-powered split-view reading: source note on the left, LLM-generated summary cards on the right with scroll-sync highlighting.", "author": "lancivez", diff --git a/src/i18n-strings.ts b/src/i18n-strings.ts index c76eab1..d415c43 100644 --- a/src/i18n-strings.ts +++ b/src/i18n-strings.ts @@ -438,3 +438,1170 @@ export const STRINGS: Record> = { batchCancelled: 'Batch cancelled: processed {processed}/{total}, skipped {skipped} (cached), errors {errors}', }, }; + +function withEnglishFallback(overrides: Record): Record { + return { ...STRINGS.en, ...overrides }; +} + +export const LOCALE_OVERRIDES: Record> = {}; + +function defineLocale(locale: string, overrides: Record): void { + LOCALE_OVERRIDES[locale] = overrides; + STRINGS[locale] = withEnglishFallback(overrides); +} + +defineLocale('ja', { + appTitle: 'Parallel Reader', + settingsTitle: 'Parallel Reader 設定', + emptyOpenNote: 'ノートを開いてから実行:', + emptyNoCache: 'このノートにはキャッシュ済みの並列ノートがありません。実行:', + commandGenerate: 'Parallel Reader: 現在のノートの並列ノートを生成', + displayName: 'Parallel Reader', + ribbonOpen: 'Parallel Reader ペインを開く', + cmdRun: '現在のノートの並列ノートを生成(キャッシュ優先)', + cmdRegen: '並列ノートを再生成(キャッシュを無視)', + cmdOpenView: 'Parallel Reader ペインを開く', + cmdExport: '現在の並列ノートを Vault にエクスポート', + cmdCopyMarkdown: '現在の並列ノートを Markdown としてコピー', + cmdCancel: '現在の並列ノート生成をキャンセル', + cmdClearCurrent: '現在のノートのキャッシュを削除', + cmdClearAll: 'すべてのキャッシュを削除', + actionCancel: '生成をキャンセル', + actionRegenerate: '再生成', + actionCopyAll: 'すべての Markdown をコピー', + actionExport: 'Vault にエクスポート', + staleBanner: '元ノートまたは生成設定が変更されました。これは古いキャッシュです。', + loadingDefault: '並列ノートを生成中...', + loadingGenerating: 'Parallel Reader: LLM がノート全体を読み取り分割しています...', + loadingSubtitle: '読み続けられます。生成が完了するとカードが更新されます。', + errorTitle: '生成に失敗しました', + actionCopyError: 'エラーをコピー', + emptyCard: '(未生成)', + cardUntitled: '(無題)', + generationAlreadyRunning: 'このノートはすでに生成中です。', + anchorMismatch: 'アンカーが一致せず、スクロール同期を利用できません', + menuCopyMarkdown: 'Markdown をコピー', + menuCopyPlain: 'プレーンテキストをコピー', + menuCopyAnchor: 'アンカー引用をコピー', + menuJumpSource: '元ノートへジャンプ', + menuEditCard: 'このカードを編集', + menuDeleteCard: 'このカードを削除', + copiedMarkdown: 'Markdown をコピーしました', + copiedPlain: 'プレーンテキストをコピーしました', + copiedAnchor: 'アンカー引用をコピーしました', + copiedAllMarkdown: 'すべての Markdown をコピーしました', + copyFailed: 'コピーに失敗しました: {error}', + actionFailed: '{label} に失敗しました: {error}', + exported: 'エクスポートしました → {path}', + exportCancelled: 'エクスポートをキャンセルしました', + exportFailed: 'エクスポートに失敗しました: {error}', + confirmExportOverwrite: 'エクスポート先ファイルは既に存在します: {path}\n上書きしますか?', + confirmExportCancel: 'キャンセル', + confirmExportOverwriteButton: '上書き', + noCurrentNote: '現在のノートがありません', + cacheClearedFile: 'キャッシュを削除しました: {name}', + cacheClearedAll: '{count} 件のキャッシュを削除しました', + noCancelableJob: 'キャンセル可能な生成ジョブはありません', + cancelRequested: 'キャンセルを要求しました', + actionGenerate: '並列ノートを生成', + fileMenuGenerate: '並列ノートを生成', + fileMenuRegen: '並列ノートを強制再生成', + fileMenuClear: '並列ノートのキャッシュを削除', + noExportContent: 'エクスポートできる並列ノートはありません', + noCopyContent: 'コピーできる並列ノートはありません', + noActiveCard: 'ジャンプできるアクティブな要約カードはありません', + viewOpenFailed: 'Parallel Reader ペインを開けませんでした', + confirmRegenerateEditedCards: + 'この並列リーダーカードは手動編集されています。再生成すると編集内容が上書きされます。続行しますか?', + confirmRegenerateCancel: 'キャンセル', + confirmRegenerateProceed: '再生成', + regenerateCancelled: '再生成をキャンセルしました', + cardDeleted: 'このカードを削除しました', + cardSaved: 'このカードを保存しました', + editCardTitle: '要約カードを編集', + editCardTitleField: 'タイトル', + editCardGistField: '要旨', + editCardBulletsField: '箇条書き(1行に1件)', + editCardCancel: 'キャンセル', + editCardSave: '保存', + openNoteFirst: '先にノートを開いてください', + alreadyGenerating: 'このノートは生成中です', + emptyNote: 'ノートは空です', + longNoteTruncated: '長いノート: 最初の {count} 文字だけをモデルに送信します', + generatingNotice: 'Parallel Reader: LLM がノート全体を読み取り分割しています...', + errorNoticeTimeout: + '生成がタイムアウトしました。ネットワーク停止、CLI 起動失敗、またはモデル応答の遅延が考えられます。', + errorNoticeAuth: 'API key がないか無効です。設定を確認して再試行してください。', + errorNoticeRateLimit: 'レート制限: {error}\n少し待ってから再試行してください。', + errorNoticeSchema: 'モデルが JSON 以外の出力を返しました。生出力をコピーして確認するか再試行してください。', + errorNoticeConfig: '設定エラー: {error}', + errorActionOpenSettings: '設定を開く', + errorActionCopyDetails: '詳細をコピー', + errorActionCopyRaw: '生出力をコピー', + noCardsReturned: 'LLM はカードを返しませんでした', + generationDone: '{count} セクションを生成しました{suffix}', + cancelled: '生成をキャンセルしました', + cancelledError: '生成をキャンセルしました', + generationFailed: '生成に失敗しました{kind}: {error}', + noEditor: '元ノートのエディターが見つかりません', + settingUiLanguageName: 'UI 言語', + settingUiLanguageDesc: + 'プラグイン UI、コマンド、通知の言語を制御します。Auto は対応する Obsidian/システム言語に従います。', + settingBackendName: 'バックエンド', + settingBackendDesc: + '箇条書き生成のバックエンド: CLI はローカルログインを再利用し、API は OpenAI/Anthropic/Gemini と互換プロキシをサポートします。', + settingCliPathName: 'CLI パス(任意)', + settingCliPathDesc: + '空欄なら一般的な場所を自動検出します。GUI から起動した Obsidian は shell PATH を継承しない場合があります。', + settingCliTimeoutName: 'CLI タイムアウト (ms)', + settingCliTimeoutDesc: 'CLI 呼び出しの最大待機時間 (ms)。最小 1000、既定 300000。', + sectionQuickSetup: 'クイック設定', + sectionReadingOutput: '読書出力', + sectionAdvancedConnection: '詳細接続', + sectionAdvancedPrompt: '詳細プロンプト', + sectionCacheMaintenance: 'キャッシュとメンテナンス', + settingProviderPresetName: 'Provider プリセット', + settingApiFormatName: 'API 形式', + settingBaseUrlName: 'Base URL', + settingApiKeyName: 'API Key', + settingApiKeyEnvName: 'API key 環境変数', + settingAuthTypeName: '認証方式', + settingHeadersName: '追加 headers', + settingMaxTokensName: '最大出力 tokens', + settingModelName: 'Model', + settingMaxInputName: '最大入力文字数', + promptHeader: 'プロンプト', + settingPromptLanguageName: '出力言語', + settingPromptLanguageDesc: 'title/gist/bullets の言語を制御します。anchor は常に原文から逐字コピーされます。', + settingCardRangeName: 'カード数の範囲', + settingCardRangeDesc: 'モデルはこの範囲内で分割を調整します。', + settingCustomPromptName: 'カスタム system prompt', + settingCustomPromptPlaceholder: '空欄なら内蔵 prompt を使用', + settingTestBackendName: '現在のバックエンドをテスト', + settingTestBackendButton: 'テスト', + settingTestBackendButtonRunning: 'テスト中...', + settingExportFolderName: 'エクスポートフォルダー', + settingExportFolderDesc: 'Vault ルートからの相対パスで並列ノートの出力先を指定します。', + cacheHeader: 'キャッシュ', + settingMaxCacheName: '最大キャッシュノート数', + cachePruned: '{count} 件の古いキャッシュを削除しました', + cachedNotesName: 'キャッシュ済みノート: {count}', + clearAllCacheButton: 'すべてのキャッシュを削除', + settingStreamingName: 'ストリーミング出力', + settingStreamingTimeoutName: 'ストリーミングタイムアウト (ms)', + cmdBatchGenerate: '並列ノートを一括生成(現在のフォルダー)', + batchFolderPrompt: 'フォルダーパス', + batchFolderConfirm: 'OK', + batchFolderCancel: 'キャンセル', + batchAlreadyRunning: '一括生成はすでに実行中です', + batchNoMarkdown: 'そのフォルダーには Markdown ファイルがありません', + batchProgress: '一括生成: {current}/{total}', + batchDone: '一括完了: 処理 {processed}/{total}、キャッシュでスキップ {skipped}、エラー {errors}', + batchCancelled: '一括生成をキャンセルしました: 処理 {processed}/{total}、スキップ {skipped}、エラー {errors}', + cmdCardPrev: '前の要約カードにフォーカス', + cmdCardNext: '次の要約カードにフォーカス', + cmdCardJump: '現在の要約カードを原文へジャンプ', + cardPersistFailed: 'カードの変更を保存できませんでした(キャッシュがありません)。', + cancelRequestedApiInFlight: + 'キャンセルを要求しました。実行中の API リクエストはすぐには中断できないため、結果は無視されます。', + errorModalTimeoutTitle: 'CLI タイムアウト診断', + errorModalExitTitle: 'CLI の異常終了', + errorModalStartupTitle: 'CLI 起動失敗', + errorModalReasonWall: + 'CLI は壁時計タイムアウトまでに完了しませんでした。下の stderr を確認してから、CLI タイムアウトを延長するか API バックエンドへ切り替えることを検討してください。', + errorModalReasonExit: + 'CLI プロセスが非ゼロ終了コードで終了しました。stdout と stderr の末尾(秘匿済み)を下に表示します。`claude --output-format json` はエラーイベントを stdout に書くため、まず stdout を確認してください。', + errorModalReasonStartup: + 'CLI プロセスを起動できませんでした。よくある原因は、パスの誤り、実行権限のないバイナリ、または GUI から起動した Obsidian が shell PATH を継承していないことです。設定で絶対パスを指定してください。', + errorModalFieldExit: '終了 {code}(シグナル {signal})', + errorModalFieldCmd: 'コマンド', + errorModalFieldPid: 'PID', + errorModalFieldElapsed: '経過時間', + errorModalFieldIdle: '最後の活動からの無通信時間', + errorModalFieldBytes: '受信バイト数', + errorModalStderrTail: 'stderr 末尾(秘匿済み)', + errorModalStdoutTail: 'stdout 末尾(秘匿済み)', + errorModalNoOutput: 'CLI は終了前に何も出力しませんでした。通常は起動時の停止(ネットワーク/認証)が原因です。', + errorModalActionCopy: '完全な診断をコピー', + errorModalActionOpenSettings: '設定を開く', + errorModalActionClose: '閉じる', + errorModalCopySuccess: 'クリップボードにコピーしました', + unanchoredSuffix: '(⚠ {count} 件のアンカーが未一致)', + 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 以外を返しました({length} 文字)。抜粋は console を確認してください。', + errorClaudeCliBadJson: 'Claude CLI が想定外の出力を返しました({length} 文字)。抜粋は console を確認してください。', + errorClaudeCliNoResult: 'Claude CLI が結果を返しませんでした({length} 文字)。抜粋は console を確認してください。', + 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}', + settingCliPathPlaceholder: '例: /Users/you/bin/codex', + settingApiKeyEnvSummary: '代わりに環境変数を使用', + settingProviderPresetSummaryEmpty: '(base URL なし)', + apiProviderHeader: 'API Provider', + settingProviderPresetDesc: + 'OpenClaw 風の provider/model 設定: プリセットはプロトコル、base URL、認証の既定値を定義します。', + settingApiFormatDesc: 'provider の wire protocol。OpenAI 互換プロキシでは通常 Chat Completions を使います。', + settingBaseUrlDesc: 'provider のルート URL。/chat/completions は付けません。空欄ならプリセット既定値を使います。', + settingApiKeyDesc: 'ローカル Ollama/LM Studio では空欄可。多くのホスト型 provider では key が必要です。', + settingApiKeyEnvDesc: + '任意。Obsidian GUI は shell 環境を継承しない場合があるため、API Key を直接入力する方が安定します。', + settingAuthTypeDesc: + 'Auto は provider preset を使用します。カスタムプロキシでは Bearer、x-api-key、none を選べます。', + settingHeadersDesc: '任意。JSON オブジェクトまたは 1 行 `Header: value`。Cloudflare AI Gateway などに便利です。', + settingModelDescApi: + 'API 呼び出しのモデル ID。OpenClaw 風の provider/model をサポートし、一致する provider prefix は取り除かれます。', + settingModelDescCli: 'Claude Code では --model として渡されます。Codex は常に自身の config の既定 model を使います。', + settingMaxInputDesc: '長いノートはモデルに送る前に切り詰められます。長文コンテキストモデルでは増やしてください。', + settingCustomPromptDesc: '任意。変数: {minCards}, {maxCards}, {languageInstruction}, {schema}, {example}', + settingTestBackendDescApi: 'API 設定を検証するため、最小限の LLM リクエストを送信します。', + settingTestBackendDescCli: + '` --version` を実行してから最小限の JSON 生成を行い、認証、出力解析、タイムアウトを検証します。', + backendTestFailed: '✗ バックエンドテストに失敗しました: {error}', + settingMaxCacheDesc: + '上限を超えたノートキャッシュを最近使われていない順に削除します。キャッシュはプラグインの cache.json に保存されます。', + cachedNotesDesc: 'キャッシュはソース SHA1 と生成設定フィンガープリントで無効化されます。', + settingStreamingDesc: '生成中に LLM の出力進捗をリアルタイム表示します(OpenAI Chat と Anthropic 形式のみ対応)', + settingStreamingTimeoutDesc: + '1 回のストリーミングリクエストのタイムアウト。無効な値や 1000ms 未満は既定値を使います。', + batchSelectFolder: '一括処理するフォルダーパスを入力してください(空欄なら Vault ルート)', + batchCancelRequested: '一括生成のキャンセルを要求しました。現在のノート完了後に停止します。', + batchInvalidFolderInput: '無効なフォルダーパス: {path}', + batchFolderNotFound: 'フォルダーが見つかりません: {path}', +}); + +defineLocale('ko', { + appTitle: 'Parallel Reader', + settingsTitle: 'Parallel Reader 설정', + emptyOpenNote: '노트를 연 다음 실행:', + emptyNoCache: '이 노트에는 캐시된 병렬 노트가 없습니다. 실행:', + commandGenerate: 'Parallel Reader: 현재 노트의 병렬 노트 생성', + displayName: 'Parallel Reader', + ribbonOpen: 'Parallel Reader 패널 열기', + cmdRun: '현재 노트의 병렬 노트 생성(캐시 우선)', + cmdRegen: '병렬 노트 재생성(캐시 무시)', + cmdOpenView: 'Parallel Reader 패널 열기', + cmdExport: '현재 병렬 노트를 Vault로 내보내기', + cmdCopyMarkdown: '현재 병렬 노트를 Markdown으로 복사', + cmdCancel: '현재 병렬 노트 생성 취소', + cmdClearCurrent: '현재 노트 캐시 지우기', + cmdClearAll: '모든 캐시 지우기', + actionCancel: '생성 취소', + actionRegenerate: '재생성', + actionCopyAll: '모든 Markdown 복사', + actionExport: 'Vault로 내보내기', + staleBanner: '원본 노트 또는 생성 설정이 변경되었습니다. 오래된 캐시입니다.', + loadingDefault: '병렬 노트를 생성 중...', + loadingGenerating: 'Parallel Reader: LLM이 전체 노트를 읽고 분할하는 중...', + loadingSubtitle: '계속 읽을 수 있습니다. 생성이 끝나면 카드가 갱신됩니다.', + errorTitle: '생성 실패', + actionCopyError: '오류 복사', + emptyCard: '(생성되지 않음)', + cardUntitled: '(제목 없음)', + generationAlreadyRunning: '이 노트는 이미 생성 중입니다.', + anchorMismatch: '앵커가 일치하지 않아 스크롤 동기화를 사용할 수 없습니다', + menuCopyMarkdown: 'Markdown 복사', + menuCopyPlain: '일반 텍스트 복사', + menuCopyAnchor: '앵커 인용 복사', + menuJumpSource: '원문으로 이동', + menuEditCard: '이 카드 편집', + menuDeleteCard: '이 카드 삭제', + copiedMarkdown: 'Markdown을 복사했습니다', + copiedPlain: '일반 텍스트를 복사했습니다', + copiedAnchor: '앵커 인용을 복사했습니다', + copiedAllMarkdown: '모든 Markdown을 복사했습니다', + copyFailed: '복사 실패: {error}', + actionFailed: '{label} 실패: {error}', + exported: '내보냈습니다 → {path}', + exportCancelled: '내보내기를 취소했습니다', + exportFailed: '내보내기 실패: {error}', + confirmExportOverwrite: '내보낼 파일이 이미 있습니다: {path}\n덮어쓸까요?', + confirmExportCancel: '취소', + confirmExportOverwriteButton: '덮어쓰기', + noCurrentNote: '현재 노트가 없습니다', + cacheClearedFile: '캐시를 지웠습니다: {name}', + cacheClearedAll: '캐시 항목 {count}개를 지웠습니다', + noCancelableJob: '취소할 수 있는 생성 작업이 없습니다', + cancelRequested: '취소를 요청했습니다', + actionGenerate: '병렬 노트 생성', + fileMenuGenerate: '병렬 노트 생성', + fileMenuRegen: '병렬 노트 강제 재생성', + fileMenuClear: '병렬 노트 캐시 지우기', + noExportContent: '내보낼 병렬 노트가 없습니다', + noCopyContent: '복사할 병렬 노트가 없습니다', + noActiveCard: '이동할 활성 요약 카드가 없습니다', + viewOpenFailed: 'Parallel Reader 패널을 열 수 없습니다', + confirmRegenerateEditedCards: + '이 병렬 리더 카드는 수동 편집되었습니다. 재생성하면 편집 내용이 덮어써집니다. 계속할까요?', + confirmRegenerateCancel: '취소', + confirmRegenerateProceed: '재생성', + regenerateCancelled: '재생성을 취소했습니다', + cardDeleted: '이 카드를 삭제했습니다', + cardSaved: '이 카드를 저장했습니다', + editCardTitle: '요약 카드 편집', + editCardTitleField: '제목', + editCardGistField: '요지', + editCardBulletsField: '불릿(한 줄에 하나)', + editCardCancel: '취소', + editCardSave: '저장', + openNoteFirst: '먼저 노트를 여세요', + alreadyGenerating: '이 노트는 생성 중입니다', + emptyNote: '노트가 비어 있습니다', + longNoteTruncated: '긴 노트: 처음 {count}자만 모델에 전송합니다', + generatingNotice: 'Parallel Reader: LLM이 전체 노트를 읽고 분할하는 중...', + errorNoticeTimeout: '생성 시간이 초과되었습니다. 네트워크 정지, CLI 시작 실패 또는 느린 모델 응답일 수 있습니다.', + errorNoticeAuth: 'API key가 없거나 올바르지 않습니다. 설정을 확인한 뒤 다시 시도하세요.', + errorNoticeRateLimit: '요청 제한: {error}\n잠시 후 다시 시도하세요.', + errorNoticeSchema: '모델이 JSON이 아닌 출력을 반환했습니다. 원본 출력을 복사해 확인하거나 다시 시도하세요.', + errorNoticeConfig: '설정 오류: {error}', + errorActionOpenSettings: '설정 열기', + errorActionCopyDetails: '세부 정보 복사', + errorActionCopyRaw: '원본 출력 복사', + noCardsReturned: 'LLM이 카드를 반환하지 않았습니다', + generationDone: '{count}개 섹션을 생성했습니다{suffix}', + cancelled: '생성을 취소했습니다', + cancelledError: '생성을 취소했습니다', + generationFailed: '생성 실패{kind}: {error}', + noEditor: '원본 노트 편집기를 찾을 수 없습니다', + settingUiLanguageName: 'UI 언어', + settingUiLanguageDesc: '플러그인 UI, 명령, 알림 언어를 제어합니다. Auto는 지원되는 Obsidian/시스템 언어를 따릅니다.', + settingBackendName: '백엔드', + settingBackendDesc: + '불릿 생성 백엔드: CLI는 로컬 로그인을 재사용하고 API는 OpenAI/Anthropic/Gemini 및 호환 프록시를 지원합니다.', + settingCliPathName: 'CLI 경로(선택)', + settingCliTimeoutName: 'CLI 시간 제한 (ms)', + sectionQuickSetup: '빠른 설정', + sectionReadingOutput: '읽기 출력', + sectionAdvancedConnection: '고급 연결', + sectionAdvancedPrompt: '고급 프롬프트', + sectionCacheMaintenance: '캐시 및 유지관리', + settingProviderPresetName: 'Provider preset', + settingApiFormatName: 'API format', + settingBaseUrlName: 'Base URL', + settingApiKeyName: 'API Key', + settingAuthTypeName: '인증 방식', + settingHeadersName: '추가 headers', + settingModelName: 'Model', + promptHeader: '프롬프트', + settingPromptLanguageName: '출력 언어', + settingPromptLanguageDesc: 'title/gist/bullets의 언어를 제어합니다. anchor는 항상 원문에서 그대로 복사됩니다.', + settingCardRangeName: '카드 수 범위', + settingCustomPromptName: '사용자 지정 system prompt', + settingCustomPromptPlaceholder: '비워 두면 내장 prompt 사용', + settingTestBackendName: '현재 백엔드 테스트', + settingTestBackendButton: '테스트', + settingTestBackendButtonRunning: '테스트 중...', + settingExportFolderName: '내보내기 폴더', + cacheHeader: '캐시', + settingMaxCacheName: '최대 캐시 노트 수', + cachePruned: '오래된 캐시 {count}개를 정리했습니다', + cachedNotesName: '캐시된 노트: {count}', + clearAllCacheButton: '모든 캐시 지우기', + settingStreamingName: '스트리밍 출력', + settingStreamingTimeoutName: '스트리밍 시간 제한 (ms)', + cmdBatchGenerate: '병렬 노트 일괄 생성(현재 폴더)', + batchFolderPrompt: '폴더 경로', + batchFolderConfirm: '확인', + batchFolderCancel: '취소', + batchAlreadyRunning: '일괄 생성이 이미 실행 중입니다', + batchNoMarkdown: '해당 폴더에 Markdown 파일이 없습니다', + batchProgress: '일괄 생성: {current}/{total}', + batchDone: '일괄 완료: 처리 {processed}/{total}, 캐시로 건너뜀 {skipped}, 오류 {errors}', + batchCancelled: '일괄 생성 취소: 처리 {processed}/{total}, 건너뜀 {skipped}, 오류 {errors}', + cmdCardPrev: '이전 요약 카드에 포커스', + cmdCardNext: '다음 요약 카드에 포커스', + cmdCardJump: '현재 요약 카드를 원문으로 이동', + cardPersistFailed: '카드 변경 사항을 저장하지 못했습니다(캐시 없음).', + cancelRequestedApiInFlight: '취소를 요청했습니다. 진행 중인 API 요청은 즉시 중단할 수 없으며 결과는 무시됩니다.', + errorModalTimeoutTitle: 'CLI 시간 초과 진단', + errorModalExitTitle: 'CLI 비정상 종료', + errorModalStartupTitle: 'CLI 시작 실패', + errorModalReasonWall: + 'CLI가 벽시계 시간 제한 전에 완료되지 않았습니다. 아래 stderr를 먼저 확인한 뒤 CLI 시간 제한을 늘리거나 API 백엔드로 전환하는 것을 고려하세요.', + errorModalReasonExit: + 'CLI 프로세스가 0이 아닌 종료 코드로 종료되었습니다. stdout 및 stderr 끝부분(마스킹됨)이 아래에 있습니다. `claude --output-format json`은 오류 이벤트를 stdout에 쓰므로 먼저 확인하세요.', + errorModalReasonStartup: + 'CLI 프로세스를 시작하지 못했습니다. 흔한 원인은 잘못된 경로, 실행 불가능한 바이너리, 또는 GUI에서 시작한 Obsidian이 shell PATH를 상속하지 않는 경우입니다. 설정에서 절대 경로를 지정해 보세요.', + errorModalFieldExit: '종료 {code}(신호 {signal})', + errorModalFieldCmd: '명령', + errorModalFieldPid: 'PID', + errorModalFieldElapsed: '경과 시간', + errorModalFieldIdle: '마지막 활동 이후 유휴 시간', + errorModalFieldBytes: '수신 바이트', + errorModalStderrTail: 'stderr 끝부분(마스킹됨)', + errorModalStdoutTail: 'stdout 끝부분(마스킹됨)', + errorModalNoOutput: 'CLI가 종료되기 전 아무 출력도 만들지 않았습니다. 보통 시작 시 멈춤(네트워크/인증)입니다.', + errorModalActionCopy: '전체 진단 복사', + errorModalActionOpenSettings: '설정 열기', + errorModalActionClose: '닫기', + errorModalCopySuccess: '클립보드에 복사했습니다', + unanchoredSuffix: ' (⚠ 앵커 {count}개 불일치)', + 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이 아닌 출력을 반환했습니다({length}자). 발췌는 console을 확인하세요.', + errorClaudeCliBadJson: 'Claude CLI가 예상치 못한 출력을 반환했습니다({length}자). 발췌는 console을 확인하세요.', + errorClaudeCliNoResult: 'Claude CLI가 결과를 반환하지 않았습니다({length}자). 발췌는 console을 확인하세요.', + 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}', + settingCliPathDesc: + '비워 두면 일반적인 위치를 자동으로 찾습니다. GUI에서 시작한 Obsidian은 shell PATH를 상속하지 않을 수 있습니다.', + settingCliPathPlaceholder: '예: /Users/you/bin/codex', + settingCliTimeoutDesc: 'CLI 호출의 최대 대기 시간(ms). 최소 1000, 기본값 300000.', + settingApiKeyEnvSummary: '대신 환경 변수 사용', + settingProviderPresetSummaryEmpty: '(base URL 없음)', + apiProviderHeader: 'API Provider', + settingProviderPresetDesc: + 'OpenClaw 스타일 provider/model 설정: preset은 프로토콜, base URL, 인증 기본값을 정의합니다.', + settingApiFormatDesc: 'provider의 wire protocol입니다. OpenAI 호환 프록시는 보통 Chat Completions를 사용합니다.', + settingBaseUrlDesc: + 'provider 루트 URL입니다. /chat/completions를 붙이지 마세요. 비워 두면 preset 기본값을 사용합니다.', + settingApiKeyDesc: '로컬 Ollama/LM Studio는 비워 둘 수 있습니다. 대부분의 호스팅 provider는 key가 필요합니다.', + settingApiKeyEnvName: 'API key 환경 변수', + settingApiKeyEnvDesc: + '선택 사항입니다. Obsidian GUI가 shell 환경을 상속하지 않을 수 있으므로 API Key 직접 입력이 보통 더 안정적입니다.', + settingAuthTypeDesc: + 'Auto는 provider preset을 사용합니다. 사용자 지정 프록시는 Bearer, x-api-key 또는 none을 사용할 수 있습니다.', + settingHeadersDesc: + '선택 사항입니다. JSON 객체 또는 줄마다 `Header: value` 형식이며 Cloudflare AI Gateway 등에 유용합니다.', + settingMaxTokensName: '최대 출력 tokens', + settingModelDescApi: + 'API 호출용 모델 ID입니다. OpenClaw 스타일 provider/model을 지원하며 일치하는 provider prefix는 제거됩니다.', + settingModelDescCli: 'Claude Code에서는 --model로 전달됩니다. Codex는 항상 자체 config의 기본 model을 사용합니다.', + settingMaxInputName: '최대 입력 문자 수', + settingMaxInputDesc: '긴 노트는 모델에 보내기 전에 잘립니다. 긴 컨텍스트 모델에는 값을 늘리세요.', + settingCardRangeDesc: '모델은 이 범위 안에서 분할을 조정합니다.', + settingCustomPromptDesc: '선택 사항입니다. 변수: {minCards}, {maxCards}, {languageInstruction}, {schema}, {example}', + settingTestBackendDescApi: 'API 설정을 검증하기 위해 최소 LLM 요청을 보냅니다.', + settingTestBackendDescCli: + '` --version`을 실행한 뒤 최소 JSON 생성을 수행하여 인증, 출력 파싱, 시간 제한을 검증합니다.', + backendTestFailed: '✗ 백엔드 테스트 실패: {error}', + settingExportFolderDesc: 'Vault 루트를 기준으로 한 병렬 노트 출력 위치입니다.', + settingMaxCacheDesc: + '이 제한을 넘는 노트 캐시는 최근 사용 시간이 오래된 것부터 정리됩니다. 캐시는 플러그인 cache.json에 저장됩니다.', + cachedNotesDesc: '캐시는 원본 SHA1과 생성 설정 지문으로 무효화됩니다.', + settingStreamingDesc: '생성 중 LLM 출력 진행 상황을 실시간으로 표시합니다(OpenAI Chat 및 Anthropic 형식만 지원).', + settingStreamingTimeoutDesc: '단일 스트리밍 요청의 시간 제한입니다. 잘못된 값이나 1000ms 미만은 기본값을 사용합니다.', + batchSelectFolder: '일괄 처리할 폴더 경로를 입력하세요(비워 두면 Vault 루트)', + batchCancelRequested: '일괄 생성 취소를 요청했습니다. 현재 노트가 끝난 뒤 중지됩니다.', + batchInvalidFolderInput: '잘못된 폴더 경로: {path}', + batchFolderNotFound: '폴더를 찾을 수 없습니다: {path}', +}); + +defineLocale('fr', { + appTitle: 'Parallel Reader', + settingsTitle: 'Paramètres de Parallel Reader', + emptyOpenNote: 'Ouvrez une note, puis lancez :', + emptyNoCache: 'Cette note n’a pas de notes parallèles en cache. Lancez :', + commandGenerate: 'Parallel Reader : générer les notes de la note actuelle', + displayName: 'Parallel Reader', + ribbonOpen: 'Ouvrir le panneau Parallel Reader', + cmdRun: 'Générer les notes parallèles de la note actuelle (cache en priorité)', + cmdRegen: 'Regénérer les notes parallèles (ignorer le cache)', + cmdOpenView: 'Ouvrir le panneau Parallel Reader', + cmdExport: 'Exporter les notes parallèles actuelles vers le Vault', + cmdCopyMarkdown: 'Copier les notes parallèles actuelles en Markdown', + cmdCancel: 'Annuler la génération de notes parallèles en cours', + cmdClearCurrent: 'Effacer le cache de la note actuelle', + cmdClearAll: 'Effacer tous les caches', + actionCancel: 'Annuler la génération', + actionRegenerate: 'Regénérer', + actionCopyAll: 'Copier tout le Markdown', + actionExport: 'Exporter vers le Vault', + staleBanner: 'La note source ou les paramètres de génération ont changé. Ce cache est obsolète.', + loadingDefault: 'Génération des notes parallèles...', + loadingGenerating: 'Parallel Reader : le LLM lit et segmente toute la note...', + loadingSubtitle: 'Vous pouvez continuer à lire. Les cartes seront actualisées à la fin de la génération.', + errorTitle: 'Échec de la génération', + actionCopyError: 'Copier l’erreur', + emptyCard: '(non généré)', + cardUntitled: '(Sans titre)', + generationAlreadyRunning: 'Cette note est déjà en cours de génération.', + anchorMismatch: 'L’ancre ne correspond pas ; la synchronisation du défilement est indisponible', + menuCopyMarkdown: 'Copier le Markdown', + menuCopyPlain: 'Copier le texte brut', + menuCopyAnchor: 'Copier la citation d’ancre', + menuJumpSource: 'Aller à la source', + menuEditCard: 'Modifier cette carte', + menuDeleteCard: 'Supprimer cette carte', + copiedMarkdown: 'Markdown copié', + copiedPlain: 'Texte brut copié', + copiedAnchor: 'Citation d’ancre copiée', + copiedAllMarkdown: 'Tout le Markdown a été copié', + copyFailed: 'Échec de la copie : {error}', + actionFailed: 'Échec de {label} : {error}', + exported: 'Exporté → {path}', + exportCancelled: 'Export annulé', + exportFailed: 'Échec de l’export : {error}', + confirmExportOverwrite: 'Le fichier d’export existe déjà : {path}\nL’écraser ?', + confirmExportCancel: 'Annuler', + confirmExportOverwriteButton: 'Écraser', + noCurrentNote: 'Aucune note actuelle', + cacheClearedFile: 'Cache effacé : {name}', + cacheClearedAll: '{count} entrées de cache effacées', + noCancelableJob: 'Aucune génération annulable', + cancelRequested: 'Annulation demandée', + actionGenerate: 'Générer les notes parallèles', + fileMenuGenerate: 'Générer les notes parallèles', + fileMenuRegen: 'Forcer la regénération des notes parallèles', + fileMenuClear: 'Effacer le cache des notes parallèles', + noExportContent: 'Aucune note parallèle à exporter', + noCopyContent: 'Aucune note parallèle à copier', + noActiveCard: 'Aucune carte de résumé active vers laquelle aller', + viewOpenFailed: 'Impossible d’ouvrir le panneau Parallel Reader', + confirmRegenerateEditedCards: + 'Ces cartes ont été modifiées manuellement. La regénération écrasera ces modifications. Continuer ?', + confirmRegenerateCancel: 'Annuler', + confirmRegenerateProceed: 'Regénérer', + regenerateCancelled: 'Regénération annulée', + cardDeleted: 'Carte supprimée', + cardSaved: 'Carte enregistrée', + editCardTitle: 'Modifier la carte de résumé', + editCardTitleField: 'Titre', + editCardGistField: 'Synthèse', + editCardBulletsField: 'Puces (une par ligne)', + editCardCancel: 'Annuler', + editCardSave: 'Enregistrer', + openNoteFirst: 'Ouvrez d’abord une note', + alreadyGenerating: 'Cette note est en cours de génération', + emptyNote: 'La note est vide', + longNoteTruncated: 'Note longue : seuls les {count} premiers caractères seront envoyés au modèle', + generatingNotice: 'Parallel Reader : le LLM lit et segmente toute la note...', + errorNoticeTimeout: 'La génération a expiré. Réseau bloqué, échec de démarrage CLI ou modèle lent possible.', + errorNoticeAuth: 'API key absente ou invalide. Vérifiez les paramètres puis réessayez.', + errorNoticeRateLimit: 'Limite de débit atteinte : {error}\nPatientez puis réessayez.', + errorNoticeSchema: 'Le modèle a renvoyé une sortie non JSON. Copiez la sortie brute pour l’inspecter ou réessayez.', + errorNoticeConfig: 'Erreur de configuration : {error}', + errorActionOpenSettings: 'Ouvrir les paramètres', + errorActionCopyDetails: 'Copier les détails', + errorActionCopyRaw: 'Copier la sortie brute', + noCardsReturned: 'Le LLM n’a renvoyé aucune carte', + generationDone: '{count} sections générées{suffix}', + cancelled: 'Génération annulée', + cancelledError: 'Génération annulée', + generationFailed: 'Échec de la génération{kind} : {error}', + noEditor: 'Impossible de trouver l’éditeur de la note source', + settingUiLanguageName: 'Langue de l’interface', + settingUiLanguageDesc: + 'Contrôle l’interface, les commandes et les notifications. Auto suit la langue Obsidian/système prise en charge.', + settingBackendName: 'Backend', + settingBackendDesc: + 'Backend de génération des puces : le CLI réutilise la connexion locale ; l’API prend en charge OpenAI, Anthropic, Gemini et les proxys compatibles.', + settingCliPathName: 'Chemin du CLI (facultatif)', + settingCliTimeoutName: 'Délai CLI (ms)', + sectionQuickSetup: 'Configuration rapide', + sectionReadingOutput: 'Sortie de lecture', + sectionAdvancedConnection: 'Connexion avancée', + sectionAdvancedPrompt: 'Prompt avancé', + sectionCacheMaintenance: 'Cache et maintenance', + settingProviderPresetName: 'Préréglage provider', + settingApiFormatName: 'Format API', + settingBaseUrlName: 'Base URL', + settingApiKeyName: 'API Key', + settingAuthTypeName: 'Type d’authentification', + settingHeadersName: 'Headers supplémentaires', + settingModelName: 'Model', + promptHeader: 'Prompt', + settingPromptLanguageName: 'Langue de sortie', + settingPromptLanguageDesc: + 'Contrôle la langue de title/gist/bullets. anchor est toujours copié mot pour mot depuis la source.', + settingCardRangeName: 'Plage du nombre de cartes', + settingCustomPromptName: 'System prompt personnalisé', + settingCustomPromptPlaceholder: 'Laisser vide pour utiliser le prompt intégré', + settingTestBackendName: 'Tester le backend actuel', + settingTestBackendButton: 'Tester', + settingTestBackendButtonRunning: 'Test en cours...', + settingExportFolderName: 'Dossier d’export', + cacheHeader: 'Cache', + settingMaxCacheName: 'Notes en cache max.', + cachePruned: '{count} anciens caches supprimés', + cachedNotesName: 'Notes en cache : {count}', + clearAllCacheButton: 'Effacer tous les caches', + settingStreamingName: 'Sortie en streaming', + settingStreamingTimeoutName: 'Délai streaming (ms)', + cmdBatchGenerate: 'Générer les notes parallèles par lot (dossier actuel)', + batchFolderPrompt: 'Chemin du dossier', + batchFolderConfirm: 'OK', + batchFolderCancel: 'Annuler', + batchAlreadyRunning: 'La génération par lot est déjà en cours', + batchNoMarkdown: 'Aucun fichier Markdown dans ce dossier', + batchProgress: 'Génération par lot : {current}/{total}', + batchDone: 'Lot terminé : {processed}/{total} traités, {skipped} ignorés (cache), {errors} erreurs', + batchCancelled: 'Lot annulé : {processed}/{total} traités, {skipped} ignorés, {errors} erreurs', + cmdCardPrev: 'Focus sur la carte de résumé précédente', + cmdCardNext: 'Focus sur la carte de résumé suivante', + cmdCardJump: 'Aller de la carte de résumé actuelle vers la source', + cardPersistFailed: 'Modification de carte non enregistrée (cache manquant).', + cancelRequestedApiInFlight: + 'Annulation demandée. La requête API en cours ne peut pas être interrompue immédiatement ; son résultat sera ignoré.', + errorModalTimeoutTitle: 'Diagnostic de délai CLI', + errorModalExitTitle: 'Sortie CLI non nulle', + errorModalStartupTitle: 'Échec de démarrage du CLI', + errorModalReasonWall: + 'Le CLI ne s’est pas terminé avant le délai réel. Inspectez stderr ci-dessous, puis envisagez d’augmenter le délai CLI ou de passer au backend API.', + errorModalReasonExit: + 'Le processus CLI s’est terminé avec un statut non nul. Les fins de stdout et stderr sont ci-dessous (masquées). Notez que `claude --output-format json` écrit les événements d’erreur dans stdout ; vérifiez-le d’abord.', + errorModalReasonStartup: + 'Le processus CLI n’a pas pu démarrer. Causes fréquentes : mauvais chemin, binaire non exécutable ou Obsidian lancé par l’interface graphique sans hériter du shell PATH. Essayez de définir un chemin CLI absolu dans les paramètres.', + errorModalFieldExit: 'sortie {code} (signal {signal})', + errorModalFieldCmd: 'Commande', + errorModalFieldPid: 'PID', + errorModalFieldElapsed: 'Écoulé', + errorModalFieldIdle: 'Inactivité depuis la dernière activité', + errorModalFieldBytes: 'Octets reçus', + errorModalStderrTail: 'fin de stderr (masquée)', + errorModalStdoutTail: 'fin de stdout (masquée)', + errorModalNoOutput: + 'Le CLI n’a produit aucune sortie avant d’être tué — généralement un blocage au démarrage (réseau/authentification).', + errorModalActionCopy: 'Copier le diagnostic complet', + errorModalActionOpenSettings: 'Ouvrir les paramètres', + errorModalActionClose: 'Fermer', + errorModalCopySuccess: 'Copié dans le presse-papiers', + unanchoredSuffix: ' (⚠ {count} ancres non trouvées)', + errorCustomProviderBaseUrlRequired: 'Un provider personnalisé nécessite une API Base URL.', + errorApiBaseUrlMissing: + 'API Base URL n’est pas définie. Choisissez un provider ou saisissez une base URL personnalisée dans les paramètres.', + errorModelMissing: 'Model n’est pas défini. Saisissez un ID de modèle dans les paramètres.', + errorApiKeyMissing: 'API key n’est pas définie. Saisissez une API Key dans les paramètres{hint}.', + errorApiKeyEnvHint: ' ou définissez la variable d’environnement {envVar}', + errorLlmNonJson: 'Le LLM a renvoyé une sortie non JSON ({length} caractères). Voir la console pour un extrait.', + errorClaudeCliBadJson: + 'Claude CLI a renvoyé une sortie inattendue ({length} caractères). Voir la console pour un extrait.', + errorClaudeCliNoResult: + 'Claude CLI n’a renvoyé aucun résultat ({length} caractères). Voir la console pour un extrait.', + errorCustomHeadersJsonParse: 'Échec du parsing JSON des headers personnalisés : {error}', + errorCustomHeadersJsonObject: 'Les headers personnalisés JSON doivent être un objet', + errorCustomHeadersLineFormat: 'Les lignes de headers personnalisés doivent utiliser `Header-Name: value`', + errorProviderNonJson: '{label} a renvoyé une sortie non JSON :\n{excerpt}', + errorProviderRequestFailed: 'Requête {label} échouée : {error}', + errorProviderApiStatus: 'L’API {label} a renvoyé HTTP {status} : {excerpt}', + settingCliPathDesc: + 'Laisser vide pour détecter les emplacements courants. Obsidian lancé depuis l’interface graphique peut ne pas hériter du shell PATH.', + settingCliPathPlaceholder: 'Exemple : /Users/you/bin/codex', + settingCliTimeoutDesc: 'Temps d’attente maximal des appels CLI (ms). Minimum 1000, défaut 300000.', + settingApiKeyEnvSummary: 'Utiliser plutôt une variable d’environnement', + settingProviderPresetSummaryEmpty: '(pas de base URL)', + apiProviderHeader: 'API Provider', + settingProviderPresetDesc: + 'Configuration provider/model façon OpenClaw : les presets définissent le protocole, la base URL et les valeurs d’auth par défaut.', + settingApiFormatDesc: + 'Wire protocol du provider. Les proxys compatibles OpenAI utilisent généralement Chat Completions.', + settingBaseUrlDesc: 'URL racine du provider, sans /chat/completions. Vide utilise la valeur par défaut du preset.', + settingApiKeyDesc: 'Peut être vide pour Ollama/LM Studio locaux ; la plupart des providers hébergés exigent une key.', + settingApiKeyEnvName: 'Variable d’environnement API key', + settingApiKeyEnvDesc: + 'Facultatif. L’interface graphique d’Obsidian peut ne pas hériter de l’environnement shell ; saisir directement l’API Key est souvent plus fiable.', + settingAuthTypeDesc: + 'Auto utilise le preset du provider. Les proxys personnalisés peuvent utiliser Bearer, x-api-key ou none.', + settingHeadersDesc: 'Facultatif. Objet JSON ou une ligne `Header: value`, utile pour Cloudflare AI Gateway.', + settingMaxTokensName: 'Tokens de sortie max.', + settingModelDescApi: + 'ID de modèle pour les appels API. Prend en charge provider/model façon OpenClaw ; les préfixes provider correspondants sont retirés.', + settingModelDescCli: + 'Transmis comme --model pour Claude Code. Codex utilise toujours le modèle par défaut de sa propre config.', + settingMaxInputName: 'Caractères d’entrée max.', + settingMaxInputDesc: + 'Les notes plus longues sont tronquées avant envoi au modèle. Augmentez cette valeur pour les modèles à long contexte.', + settingCardRangeDesc: 'Le modèle adapte la segmentation dans cette plage.', + settingCustomPromptDesc: 'Facultatif. Variables : {minCards}, {maxCards}, {languageInstruction}, {schema}, {example}', + settingTestBackendDescApi: 'Envoie une requête LLM minimale pour valider les paramètres API.', + settingTestBackendDescCli: + 'Exécute ` --version`, puis une génération JSON minimale pour valider l’authentification, le parsing de sortie et les délais.', + backendTestFailed: '✗ Test backend échoué : {error}', + settingExportFolderDesc: 'Emplacement de sortie des notes parallèles, relatif à la racine du Vault.', + settingMaxCacheDesc: + 'Supprime les caches de notes les moins récemment utilisés au-delà de cette limite. Le cache est stocké dans le cache.json du plugin.', + cachedNotesDesc: 'Le cache est invalidé par le SHA1 source et l’empreinte des paramètres de génération.', + settingStreamingDesc: + 'Affiche la progression de sortie LLM en temps réel pendant la génération (formats OpenAI Chat et Anthropic seulement).', + settingStreamingTimeoutDesc: + 'Délai d’une requête streaming ; les valeurs invalides ou inférieures à 1000 ms utilisent la valeur par défaut.', + batchSelectFolder: 'Saisissez le chemin du dossier à traiter par lot (vide pour la racine du Vault)', + batchCancelRequested: 'Annulation du lot demandée ; il s’arrêtera après la note en cours.', + batchInvalidFolderInput: 'Chemin de dossier invalide : {path}', + batchFolderNotFound: 'Dossier introuvable : {path}', +}); + +defineLocale('de', { + appTitle: 'Parallel Reader', + settingsTitle: 'Parallel Reader Einstellungen', + emptyOpenNote: 'Öffnen Sie eine Notiz und führen Sie dann aus:', + emptyNoCache: 'Diese Notiz hat keine zwischengespeicherten Parallelnotizen. Ausführen:', + commandGenerate: 'Parallel Reader: Notizen für aktuelle Notiz erzeugen', + displayName: 'Parallel Reader', + ribbonOpen: 'Parallel Reader Bereich öffnen', + cmdRun: 'Parallelnotizen für aktuelle Notiz erzeugen (Cache zuerst)', + cmdRegen: 'Parallelnotizen neu erzeugen (Cache umgehen)', + cmdOpenView: 'Parallel Reader Bereich öffnen', + cmdExport: 'Aktuelle Parallelnotizen in den Vault exportieren', + cmdCopyMarkdown: 'Aktuelle Parallelnotizen als Markdown kopieren', + cmdCancel: 'Aktuelle Erzeugung von Parallelnotizen abbrechen', + cmdClearCurrent: 'Cache für aktuelle Notiz löschen', + cmdClearAll: 'Alle Caches löschen', + actionCancel: 'Erzeugung abbrechen', + actionRegenerate: 'Neu erzeugen', + actionCopyAll: 'Alles als Markdown kopieren', + actionExport: 'In Vault exportieren', + staleBanner: 'Die Quellnotiz oder die Erzeugungseinstellungen haben sich geändert. Dies ist ein veralteter Cache.', + loadingDefault: 'Parallelnotizen werden erzeugt...', + loadingGenerating: 'Parallel Reader: LLM liest und segmentiert die gesamte Notiz...', + loadingSubtitle: 'Sie können weiterlesen. Karten werden nach Abschluss aktualisiert.', + errorTitle: 'Erzeugung fehlgeschlagen', + actionCopyError: 'Fehler kopieren', + emptyCard: '(nicht erzeugt)', + cardUntitled: '(Ohne Titel)', + generationAlreadyRunning: 'Diese Notiz wird bereits erzeugt.', + anchorMismatch: 'Anker stimmt nicht überein; Scroll-Sync ist nicht verfügbar', + menuCopyMarkdown: 'Markdown kopieren', + menuCopyPlain: 'Nur Text kopieren', + menuCopyAnchor: 'Ankerzitat kopieren', + menuJumpSource: 'Zur Quelle springen', + menuEditCard: 'Diese Karte bearbeiten', + menuDeleteCard: 'Diese Karte löschen', + copiedMarkdown: 'Markdown kopiert', + copiedPlain: 'Nur Text kopiert', + copiedAnchor: 'Ankerzitat kopiert', + copiedAllMarkdown: 'Alles als Markdown kopiert', + copyFailed: 'Kopieren fehlgeschlagen: {error}', + actionFailed: '{label} fehlgeschlagen: {error}', + exported: 'Exportiert → {path}', + exportCancelled: 'Export abgebrochen', + exportFailed: 'Export fehlgeschlagen: {error}', + confirmExportOverwrite: 'Exportdatei existiert bereits: {path}\nÜberschreiben?', + confirmExportCancel: 'Abbrechen', + confirmExportOverwriteButton: 'Überschreiben', + noCurrentNote: 'Keine aktuelle Notiz', + cacheClearedFile: 'Cache gelöscht: {name}', + cacheClearedAll: '{count} Cache-Einträge gelöscht', + noCancelableJob: 'Keine abbrechbare Erzeugung', + cancelRequested: 'Abbruch angefordert', + actionGenerate: 'Parallelnotizen erzeugen', + fileMenuGenerate: 'Parallelnotizen erzeugen', + fileMenuRegen: 'Parallelnotizen erzwungen neu erzeugen', + fileMenuClear: 'Parallelnotizen-Cache löschen', + noExportContent: 'Keine Parallelnotizen zum Exportieren', + noCopyContent: 'Keine Parallelnotizen zum Kopieren', + noActiveCard: 'Keine aktive Zusammenfassungskarte zum Springen', + viewOpenFailed: 'Parallel Reader Bereich konnte nicht geöffnet werden', + confirmRegenerateEditedCards: + 'Diese Karten wurden manuell bearbeitet. Eine Neuerzeugung überschreibt diese Änderungen. Fortfahren?', + confirmRegenerateCancel: 'Abbrechen', + confirmRegenerateProceed: 'Neu erzeugen', + regenerateCancelled: 'Neuerzeugung abgebrochen', + cardDeleted: 'Diese Karte wurde gelöscht', + cardSaved: 'Diese Karte wurde gespeichert', + editCardTitle: 'Zusammenfassungskarte bearbeiten', + editCardTitleField: 'Titel', + editCardGistField: 'Kernpunkt', + editCardBulletsField: 'Stichpunkte (einer pro Zeile)', + editCardCancel: 'Abbrechen', + editCardSave: 'Speichern', + openNoteFirst: 'Öffnen Sie zuerst eine Notiz', + alreadyGenerating: 'Diese Notiz wird erzeugt', + emptyNote: 'Die Notiz ist leer', + longNoteTruncated: 'Lange Notiz: Nur die ersten {count} Zeichen werden an das Modell gesendet', + generatingNotice: 'Parallel Reader: LLM liest und segmentiert die gesamte Notiz...', + errorNoticeTimeout: + 'Erzeugung timed out. Mögliche Ursachen: Netzwerk hängt, CLI-Start fehlgeschlagen oder langsames Modell.', + errorNoticeAuth: 'API key fehlt oder ist ungültig. Einstellungen prüfen und erneut versuchen.', + errorNoticeRateLimit: 'Rate Limit erreicht: {error}\nKurz warten und erneut versuchen.', + errorNoticeSchema: + 'Das Modell hat keine JSON-Ausgabe zurückgegeben. Rohdaten kopieren und prüfen oder erneut versuchen.', + errorNoticeConfig: 'Konfigurationsfehler: {error}', + errorActionOpenSettings: 'Einstellungen öffnen', + errorActionCopyDetails: 'Details kopieren', + errorActionCopyRaw: 'Rohdaten kopieren', + noCardsReturned: 'LLM hat keine Karten zurückgegeben', + generationDone: '{count} Abschnitte erzeugt{suffix}', + cancelled: 'Erzeugung abgebrochen', + cancelledError: 'Erzeugung abgebrochen', + generationFailed: 'Erzeugung fehlgeschlagen{kind}: {error}', + noEditor: 'Editor der Quellnotiz konnte nicht gefunden werden', + settingUiLanguageName: 'UI-Sprache', + settingUiLanguageDesc: + 'Steuert Plugin-UI, Befehle und Hinweise. Auto folgt einer unterstützten Obsidian-/Systemsprache.', + settingBackendName: 'Backend', + settingBackendDesc: + 'Backend für Stichpunkte: CLI nutzt lokale Anmeldung; API unterstützt OpenAI, Anthropic, Gemini und kompatible Proxys.', + settingCliPathName: 'CLI-Pfad (optional)', + settingCliTimeoutName: 'CLI-Timeout (ms)', + sectionQuickSetup: 'Schnelle Einrichtung', + sectionReadingOutput: 'Leseausgabe', + sectionAdvancedConnection: 'Erweiterte Verbindung', + sectionAdvancedPrompt: 'Erweiterter Prompt', + sectionCacheMaintenance: 'Cache und Wartung', + settingProviderPresetName: 'Provider-Preset', + settingApiFormatName: 'API-Format', + settingBaseUrlName: 'Base URL', + settingApiKeyName: 'API Key', + settingAuthTypeName: 'Auth-Typ', + settingHeadersName: 'Zusätzliche headers', + settingModelName: 'Model', + promptHeader: 'Prompt', + settingPromptLanguageName: 'Ausgabesprache', + settingPromptLanguageDesc: + 'Steuert die Sprache von title/gist/bullets. anchor wird immer wortgetreu aus der Quelle kopiert.', + settingCardRangeName: 'Kartenanzahl', + settingCustomPromptName: 'Benutzerdefinierter system prompt', + settingCustomPromptPlaceholder: 'Leer lassen, um den eingebauten prompt zu verwenden', + settingTestBackendName: 'Aktuelles Backend testen', + settingTestBackendButton: 'Testen', + settingTestBackendButtonRunning: 'Test läuft...', + settingExportFolderName: 'Exportordner', + cacheHeader: 'Cache', + settingMaxCacheName: 'Max. zwischengespeicherte Notizen', + cachePruned: '{count} alte Cache-Einträge entfernt', + cachedNotesName: 'Zwischengespeicherte Notizen: {count}', + clearAllCacheButton: 'Alle Caches löschen', + settingStreamingName: 'Streaming-Ausgabe', + settingStreamingTimeoutName: 'Streaming-Timeout (ms)', + cmdBatchGenerate: 'Parallelnotizen stapelweise erzeugen (aktueller Ordner)', + batchFolderPrompt: 'Ordnerpfad', + batchFolderConfirm: 'OK', + batchFolderCancel: 'Abbrechen', + batchAlreadyRunning: 'Stapelverarbeitung läuft bereits', + batchNoMarkdown: 'Keine Markdown-Dateien in diesem Ordner gefunden', + batchProgress: 'Stapelverarbeitung: {current}/{total}', + batchDone: 'Stapel abgeschlossen: {processed}/{total} verarbeitet, {skipped} übersprungen (Cache), {errors} Fehler', + batchCancelled: 'Stapel abgebrochen: {processed}/{total} verarbeitet, {skipped} übersprungen, {errors} Fehler', + cmdCardPrev: 'Vorherige Zusammenfassungskarte fokussieren', + cmdCardNext: 'Nächste Zusammenfassungskarte fokussieren', + cmdCardJump: 'Aktuelle Zusammenfassungskarte zur Quelle springen lassen', + cardPersistFailed: 'Kartenänderung nicht gespeichert (Cache fehlt).', + cancelRequestedApiInFlight: + 'Abbruch angefordert. Die laufende API-Anfrage kann nicht sofort abgebrochen werden; ihr Ergebnis wird ignoriert.', + errorModalTimeoutTitle: 'CLI-Timeout-Diagnose', + errorModalExitTitle: 'CLI mit Nicht-Null-Code beendet', + errorModalStartupTitle: 'CLI-Start fehlgeschlagen', + errorModalReasonWall: + 'Die CLI wurde vor dem Wall-Clock-Timeout nicht fertig. Prüfen Sie stderr unten und erwägen Sie dann, das CLI-Timeout zu erhöhen oder zum API-Backend zu wechseln.', + errorModalReasonExit: + 'Der CLI-Prozess wurde mit einem Nicht-Null-Status beendet. stdout- und stderr-Enden stehen unten (redigiert). Beachten Sie, dass `claude --output-format json` Fehlerereignisse nach stdout schreibt; prüfen Sie das zuerst.', + errorModalReasonStartup: + 'Der CLI-Prozess konnte nicht gestartet werden. Häufige Ursachen: falscher Pfad, nicht ausführbare Binärdatei oder Obsidian aus der GUI erbt den shell PATH nicht. Setzen Sie in den Einstellungen einen absoluten CLI-Pfad.', + errorModalFieldExit: 'Exit {code} (Signal {signal})', + errorModalFieldCmd: 'Befehl', + errorModalFieldPid: 'PID', + errorModalFieldElapsed: 'Verstrichen', + errorModalFieldIdle: 'Inaktiv seit letzter Aktivität', + errorModalFieldBytes: 'Empfangene Bytes', + errorModalStderrTail: 'stderr-Ende (redigiert)', + errorModalStdoutTail: 'stdout-Ende (redigiert)', + errorModalNoOutput: + 'Die CLI erzeugte vor dem Beenden keine Ausgabe — typisch für einen Start-Hänger (Netzwerk/Auth).', + errorModalActionCopy: 'Vollständige Diagnose kopieren', + errorModalActionOpenSettings: 'Einstellungen öffnen', + errorModalActionClose: 'Schließen', + errorModalCopySuccess: 'In die Zwischenablage kopiert', + unanchoredSuffix: ' (⚠ {count} Anker nicht gefunden)', + errorCustomProviderBaseUrlRequired: 'Ein benutzerdefinierter provider benötigt eine API Base URL.', + errorApiBaseUrlMissing: + 'API Base URL ist nicht gesetzt. Wählen Sie in den Einstellungen einen provider oder geben Sie eine benutzerdefinierte base URL ein.', + errorModelMissing: 'Model ist nicht gesetzt. Geben Sie in den Einstellungen eine Modell-ID ein.', + errorApiKeyMissing: 'API key ist nicht gesetzt. Geben Sie in den Einstellungen eine API Key ein{hint}.', + errorApiKeyEnvHint: ' oder setzen Sie die Umgebungsvariable {envVar}', + errorLlmNonJson: 'LLM gab Nicht-JSON zurück ({length} Zeichen). Siehe console für einen Auszug.', + errorClaudeCliBadJson: + 'Claude CLI gab unerwartete Ausgabe zurück ({length} Zeichen). Siehe console für einen Auszug.', + errorClaudeCliNoResult: 'Claude CLI gab kein Ergebnis zurück ({length} Zeichen). Siehe console für einen Auszug.', + errorCustomHeadersJsonParse: 'Parsing von benutzerdefinierten headers als JSON fehlgeschlagen: {error}', + errorCustomHeadersJsonObject: 'Benutzerdefinierte headers als JSON müssen ein Objekt sein', + errorCustomHeadersLineFormat: 'Zeilen für benutzerdefinierte headers müssen `Header-Name: value` verwenden', + errorProviderNonJson: '{label} gab Nicht-JSON zurück:\n{excerpt}', + errorProviderRequestFailed: '{label}-Anfrage fehlgeschlagen: {error}', + errorProviderApiStatus: '{label} API gab HTTP {status} zurück: {excerpt}', + settingCliPathDesc: + 'Leer lassen, um übliche Pfade automatisch zu erkennen. Aus der GUI gestartetes Obsidian erbt möglicherweise den shell PATH nicht.', + settingCliPathPlaceholder: 'Beispiel: /Users/you/bin/codex', + settingCliTimeoutDesc: 'Maximale Wartezeit für CLI-Aufrufe (ms). Minimum 1000, Standard 300000.', + settingApiKeyEnvSummary: 'Stattdessen Umgebungsvariable verwenden', + settingProviderPresetSummaryEmpty: '(keine base URL)', + apiProviderHeader: 'API Provider', + settingProviderPresetDesc: + 'OpenClaw-artige provider/model-Einrichtung: Presets definieren Protokoll, base URL und Auth-Standardwerte.', + settingApiFormatDesc: + 'Wire protocol für den provider. OpenAI-kompatible Proxys verwenden normalerweise Chat Completions.', + settingBaseUrlDesc: 'Provider-Root-URL ohne /chat/completions. Leer verwendet den Standard des Presets.', + settingApiKeyDesc: + 'Kann für lokales Ollama/LM Studio leer bleiben; die meisten gehosteten provider benötigen einen key.', + settingApiKeyEnvName: 'API key Umgebungsvariable', + settingApiKeyEnvDesc: + 'Optional. Obsidian GUI erbt die shell-Umgebung möglicherweise nicht; direkte API Key Eingabe ist meist zuverlässiger.', + settingAuthTypeDesc: + 'Auto verwendet das provider preset. Benutzerdefinierte Proxys können Bearer, x-api-key oder none verwenden.', + settingHeadersDesc: 'Optional. JSON-Objekt oder eine Zeile `Header: value`, nützlich für Cloudflare AI Gateway.', + settingMaxTokensName: 'Max. Ausgabe-tokens', + settingModelDescApi: + 'Modell-ID für API-Aufrufe. Unterstützt OpenClaw-artiges provider/model; passende provider-Präfixe werden entfernt.', + settingModelDescCli: + 'Wird für Claude Code als --model übergeben. Codex verwendet immer das Standardmodell seiner eigenen config.', + settingMaxInputName: 'Max. Eingabezeichen', + settingMaxInputDesc: + 'Längere Notizen werden vor dem Senden an das Modell gekürzt. Erhöhen Sie dies für Long-Context-Modelle.', + settingCardRangeDesc: 'Das Modell passt die Segmentierung innerhalb dieses Bereichs an.', + settingCustomPromptDesc: 'Optional. Variablen: {minCards}, {maxCards}, {languageInstruction}, {schema}, {example}', + settingTestBackendDescApi: 'Sendet eine minimale LLM-Anfrage, um API-Einstellungen zu validieren.', + settingTestBackendDescCli: + 'Führt ` --version` aus und dann eine minimale JSON-Erzeugung, um Auth, Ausgabe-Parsing und Timeout zu validieren.', + backendTestFailed: '✗ Backend-Test fehlgeschlagen: {error}', + settingExportFolderDesc: 'Ausgabeort für Parallelnotizen, relativ zur Vault-Wurzel.', + settingMaxCacheDesc: + 'Entfernt über diesem Limit am längsten nicht genutzte Notiz-Caches. Der Cache liegt in cache.json des Plugins.', + cachedNotesDesc: 'Der Cache wird durch Quell-SHA1 und Fingerprint der Erzeugungseinstellungen ungültig.', + settingStreamingDesc: + 'Zeigt während der Erzeugung den LLM-Ausgabefortschritt in Echtzeit (nur OpenAI Chat- und Anthropic-Formate).', + settingStreamingTimeoutDesc: + 'Timeout für eine Streaming-Anfrage; ungültige Werte oder Werte unter 1000 ms nutzen den Standard.', + batchSelectFolder: 'Ordnerpfad für Stapelverarbeitung eingeben (leer für Vault-Wurzel)', + batchCancelRequested: 'Abbruch der Stapelverarbeitung angefordert; sie stoppt nach der aktuellen Notiz.', + batchInvalidFolderInput: 'Ungültiger Ordnerpfad: {path}', + batchFolderNotFound: 'Ordner nicht gefunden: {path}', +}); + +defineLocale('es', { + appTitle: 'Parallel Reader', + settingsTitle: 'Configuración de Parallel Reader', + emptyOpenNote: 'Abre una nota y luego ejecuta:', + emptyNoCache: 'Esta nota no tiene notas paralelas en caché. Ejecuta:', + commandGenerate: 'Parallel Reader: generar notas para la nota actual', + displayName: 'Parallel Reader', + ribbonOpen: 'Abrir panel de Parallel Reader', + cmdRun: 'Generar notas paralelas para la nota actual (caché primero)', + cmdRegen: 'Regenerar notas paralelas (omitir caché)', + cmdOpenView: 'Abrir panel de Parallel Reader', + cmdExport: 'Exportar notas paralelas actuales al Vault', + cmdCopyMarkdown: 'Copiar notas paralelas actuales como Markdown', + cmdCancel: 'Cancelar la generación actual de notas paralelas', + cmdClearCurrent: 'Borrar caché de la nota actual', + cmdClearAll: 'Borrar todas las cachés', + actionCancel: 'Cancelar generación', + actionRegenerate: 'Regenerar', + actionCopyAll: 'Copiar todo el Markdown', + actionExport: 'Exportar al Vault', + staleBanner: 'La nota fuente o la configuración de generación cambiaron. Esta caché está obsoleta.', + loadingDefault: 'Generando notas paralelas...', + loadingGenerating: 'Parallel Reader: el LLM está leyendo y segmentando toda la nota...', + loadingSubtitle: 'Puedes seguir leyendo. Las tarjetas se actualizarán cuando termine la generación.', + errorTitle: 'Falló la generación', + actionCopyError: 'Copiar error', + emptyCard: '(no generado)', + cardUntitled: '(Sin título)', + generationAlreadyRunning: 'Esta nota ya se está generando.', + anchorMismatch: 'El ancla no coincide; la sincronización de desplazamiento no está disponible', + menuCopyMarkdown: 'Copiar Markdown', + menuCopyPlain: 'Copiar texto plano', + menuCopyAnchor: 'Copiar cita de ancla', + menuJumpSource: 'Ir a la fuente', + menuEditCard: 'Editar esta tarjeta', + menuDeleteCard: 'Eliminar esta tarjeta', + copiedMarkdown: 'Markdown copiado', + copiedPlain: 'Texto plano copiado', + copiedAnchor: 'Cita de ancla copiada', + copiedAllMarkdown: 'Todo el Markdown copiado', + copyFailed: 'Error al copiar: {error}', + actionFailed: '{label} falló: {error}', + exported: 'Exportado → {path}', + exportCancelled: 'Exportación cancelada', + exportFailed: 'Error al exportar: {error}', + confirmExportOverwrite: 'El archivo de exportación ya existe: {path}\n¿Sobrescribir?', + confirmExportCancel: 'Cancelar', + confirmExportOverwriteButton: 'Sobrescribir', + noCurrentNote: 'No hay nota actual', + cacheClearedFile: 'Caché borrada: {name}', + cacheClearedAll: 'Se borraron {count} entradas de caché', + noCancelableJob: 'No hay generación cancelable', + cancelRequested: 'Cancelación solicitada', + actionGenerate: 'Generar notas paralelas', + fileMenuGenerate: 'Generar notas paralelas', + fileMenuRegen: 'Forzar regeneración de notas paralelas', + fileMenuClear: 'Borrar caché de notas paralelas', + noExportContent: 'No hay notas paralelas para exportar', + noCopyContent: 'No hay notas paralelas para copiar', + noActiveCard: 'No hay tarjeta de resumen activa a la que saltar', + viewOpenFailed: 'No se pudo abrir el panel de Parallel Reader', + confirmRegenerateEditedCards: + 'Estas tarjetas se editaron manualmente. Regenerar sobrescribirá esos cambios. ¿Continuar?', + confirmRegenerateCancel: 'Cancelar', + confirmRegenerateProceed: 'Regenerar', + regenerateCancelled: 'Regeneración cancelada', + cardDeleted: 'Tarjeta eliminada', + cardSaved: 'Tarjeta guardada', + editCardTitle: 'Editar tarjeta de resumen', + editCardTitleField: 'Título', + editCardGistField: 'Resumen', + editCardBulletsField: 'Viñetas (una por línea)', + editCardCancel: 'Cancelar', + editCardSave: 'Guardar', + openNoteFirst: 'Abre una nota primero', + alreadyGenerating: 'Esta nota se está generando', + emptyNote: 'La nota está vacía', + longNoteTruncated: 'Nota larga: solo se enviarán los primeros {count} caracteres al modelo', + generatingNotice: 'Parallel Reader: el LLM está leyendo y segmentando toda la nota...', + errorNoticeTimeout: 'La generación agotó el tiempo. Puede haber red colgada, fallo de inicio de CLI o modelo lento.', + errorNoticeAuth: 'API key ausente o no válida. Revisa Configuración y vuelve a intentar.', + errorNoticeRateLimit: 'Límite de tasa: {error}\nEspera un momento y vuelve a intentar.', + errorNoticeSchema: + 'El modelo devolvió una salida que no es JSON. Copia la salida bruta para inspeccionarla o vuelve a intentar.', + errorNoticeConfig: 'Error de configuración: {error}', + errorActionOpenSettings: 'Abrir configuración', + errorActionCopyDetails: 'Copiar detalles', + errorActionCopyRaw: 'Copiar salida bruta', + noCardsReturned: 'El LLM no devolvió tarjetas', + generationDone: 'Se generaron {count} secciones{suffix}', + cancelled: 'Generación cancelada', + cancelledError: 'Generación cancelada', + generationFailed: 'Generación fallida{kind}: {error}', + noEditor: 'No se pudo encontrar el editor de la nota fuente', + settingUiLanguageName: 'Idioma de la interfaz', + settingUiLanguageDesc: + 'Controla la interfaz, comandos y avisos. Auto sigue un idioma compatible de Obsidian/sistema.', + settingBackendName: 'Backend', + settingBackendDesc: + 'Backend para generar viñetas: CLI reutiliza el inicio local; API admite OpenAI, Anthropic, Gemini y proxies compatibles.', + settingCliPathName: 'Ruta de CLI (opcional)', + settingCliTimeoutName: 'Tiempo de espera CLI (ms)', + sectionQuickSetup: 'Configuración rápida', + sectionReadingOutput: 'Salida de lectura', + sectionAdvancedConnection: 'Conexión avanzada', + sectionAdvancedPrompt: 'Prompt avanzado', + sectionCacheMaintenance: 'Caché y mantenimiento', + settingProviderPresetName: 'Preset de provider', + settingApiFormatName: 'Formato API', + settingBaseUrlName: 'Base URL', + settingApiKeyName: 'API Key', + settingAuthTypeName: 'Tipo de autenticación', + settingHeadersName: 'Headers extra', + settingModelName: 'Model', + promptHeader: 'Prompt', + settingPromptLanguageName: 'Idioma de salida', + settingPromptLanguageDesc: + 'Controla el idioma de title/gist/bullets. anchor siempre se copia literalmente de la fuente.', + settingCardRangeName: 'Rango de tarjetas', + settingCustomPromptName: 'System prompt personalizado', + settingCustomPromptPlaceholder: 'Déjalo vacío para usar el prompt integrado', + settingTestBackendName: 'Probar backend actual', + settingTestBackendButton: 'Probar', + settingTestBackendButtonRunning: 'Probando...', + settingExportFolderName: 'Carpeta de exportación', + cacheHeader: 'Caché', + settingMaxCacheName: 'Máximo de notas en caché', + cachePruned: 'Se eliminaron {count} entradas antiguas de caché', + cachedNotesName: 'Notas en caché: {count}', + clearAllCacheButton: 'Borrar todas las cachés', + settingStreamingName: 'Salida en streaming', + settingStreamingTimeoutName: 'Tiempo de espera streaming (ms)', + cmdBatchGenerate: 'Generar notas paralelas por lote (carpeta actual)', + batchFolderPrompt: 'Ruta de carpeta', + batchFolderConfirm: 'OK', + batchFolderCancel: 'Cancelar', + batchAlreadyRunning: 'La generación por lote ya está en curso', + batchNoMarkdown: 'No se encontraron archivos Markdown en esa carpeta', + batchProgress: 'Generación por lote: {current}/{total}', + batchDone: 'Lote completo: procesadas {processed}/{total}, omitidas {skipped} (caché), errores {errors}', + batchCancelled: 'Lote cancelado: procesadas {processed}/{total}, omitidas {skipped}, errores {errors}', + cmdCardPrev: 'Enfocar tarjeta de resumen anterior', + cmdCardNext: 'Enfocar tarjeta de resumen siguiente', + cmdCardJump: 'Ir desde la tarjeta de resumen actual a la fuente', + cardPersistFailed: 'Cambio de tarjeta no guardado (falta caché).', + cancelRequestedApiInFlight: + 'Cancelación solicitada. La solicitud API en curso no puede abortarse de inmediato; su resultado se ignorará.', + errorModalTimeoutTitle: 'Diagnóstico de tiempo de espera CLI', + errorModalExitTitle: 'Salida CLI con código distinto de cero', + errorModalStartupTitle: 'Fallo de inicio de CLI', + errorModalReasonWall: + 'La CLI no terminó antes del tiempo límite real. Inspecciona stderr abajo y luego considera aumentar el tiempo de espera CLI o cambiar al backend API.', + errorModalReasonExit: + 'El proceso CLI salió con un estado distinto de cero. Las colas de stdout y stderr están abajo (censuradas). Ten en cuenta que `claude --output-format json` escribe eventos de error en stdout, así que revisa eso primero.', + errorModalReasonStartup: + 'El proceso CLI no pudo iniciarse. Causas comunes: ruta incorrecta, binario no ejecutable u Obsidian iniciado desde la GUI sin heredar shell PATH. Prueba a establecer una ruta CLI absoluta en Configuración.', + errorModalFieldExit: 'salida {code} (señal {signal})', + errorModalFieldCmd: 'Comando', + errorModalFieldPid: 'PID', + errorModalFieldElapsed: 'Transcurrido', + errorModalFieldIdle: 'Inactivo desde la última actividad', + errorModalFieldBytes: 'Bytes recibidos', + errorModalStderrTail: 'cola de stderr (censurada)', + errorModalStdoutTail: 'cola de stdout (censurada)', + errorModalNoOutput: + 'La CLI no produjo salida antes de ser finalizada; normalmente es un bloqueo al inicio (red/autenticación).', + errorModalActionCopy: 'Copiar diagnóstico completo', + errorModalActionOpenSettings: 'Abrir configuración', + errorModalActionClose: 'Cerrar', + errorModalCopySuccess: 'Copiado al portapapeles', + unanchoredSuffix: ' (⚠ {count} anclas sin coincidencia)', + errorCustomProviderBaseUrlRequired: 'Un provider personalizado requiere una API Base URL.', + errorApiBaseUrlMissing: + 'API Base URL no está configurada. Elige un provider o introduce una base URL personalizada en configuración.', + errorModelMissing: 'Model no está configurado. Introduce un ID de modelo en configuración.', + errorApiKeyMissing: 'API key no está configurada. Introduce una API Key en configuración{hint}.', + errorApiKeyEnvHint: ' o establece la variable de entorno {envVar}', + errorLlmNonJson: + 'El LLM devolvió una salida que no es JSON ({length} caracteres). Consulta la console para ver un extracto.', + errorClaudeCliBadJson: + 'Claude CLI devolvió una salida inesperada ({length} caracteres). Consulta la console para ver un extracto.', + errorClaudeCliNoResult: + 'Claude CLI no devolvió resultado ({length} caracteres). Consulta la console para ver un extracto.', + errorCustomHeadersJsonParse: 'Error al parsear headers personalizados como JSON: {error}', + errorCustomHeadersJsonObject: 'Los headers personalizados JSON deben ser un objeto', + errorCustomHeadersLineFormat: 'Las líneas de headers personalizados deben usar `Header-Name: value`', + errorProviderNonJson: '{label} devolvió una salida que no es JSON:\n{excerpt}', + errorProviderRequestFailed: 'Solicitud {label} fallida: {error}', + errorProviderApiStatus: 'La API {label} devolvió HTTP {status}: {excerpt}', + settingCliPathDesc: + 'Déjalo vacío para detectar rutas comunes. Obsidian iniciado desde la GUI puede no heredar shell PATH.', + settingCliPathPlaceholder: 'Ejemplo: /Users/you/bin/codex', + settingCliTimeoutDesc: 'Tiempo máximo de espera para llamadas CLI (ms). Mínimo 1000, predeterminado 300000.', + settingApiKeyEnvSummary: 'Usar variable de entorno en su lugar', + settingProviderPresetSummaryEmpty: '(sin base URL)', + apiProviderHeader: 'API Provider', + settingProviderPresetDesc: + 'Configuración provider/model estilo OpenClaw: los presets definen protocolo, base URL y valores de auth predeterminados.', + settingApiFormatDesc: 'Wire protocol del provider. Los proxies compatibles con OpenAI suelen usar Chat Completions.', + settingBaseUrlDesc: 'URL raíz del provider, sin /chat/completions. Vacío usa el valor predeterminado del preset.', + settingApiKeyDesc: 'Puede quedar vacío para Ollama/LM Studio local; la mayoría de providers alojados requieren key.', + settingApiKeyEnvName: 'Variable de entorno API key', + settingApiKeyEnvDesc: + 'Opcional. La GUI de Obsidian puede no heredar el entorno shell; introducir API Key directamente suele ser más fiable.', + settingAuthTypeDesc: 'Auto usa el provider preset. Los proxies personalizados pueden usar Bearer, x-api-key o none.', + settingHeadersDesc: 'Opcional. Objeto JSON o una línea `Header: value`, útil para Cloudflare AI Gateway.', + settingMaxTokensName: 'Tokens máximos de salida', + settingModelDescApi: + 'ID de modelo para llamadas API. Admite provider/model estilo OpenClaw; los prefijos provider coincidentes se eliminan.', + settingModelDescCli: + 'Se pasa como --model para Claude Code. Codex siempre usa el modelo predeterminado de su propia config.', + settingMaxInputName: 'Máximo de caracteres de entrada', + settingMaxInputDesc: + 'Las notas más largas se truncan antes de enviarse al modelo. Auméntalo para modelos de contexto largo.', + settingCardRangeDesc: 'El modelo adapta la segmentación dentro de este rango.', + settingCustomPromptDesc: 'Opcional. Variables: {minCards}, {maxCards}, {languageInstruction}, {schema}, {example}', + settingTestBackendDescApi: 'Envía una solicitud LLM mínima para validar la configuración API.', + settingTestBackendDescCli: + 'Ejecuta ` --version` y luego una generación JSON mínima para validar autenticación, parseo de salida y tiempo de espera.', + backendTestFailed: '✗ Prueba de backend fallida: {error}', + settingExportFolderDesc: 'Ubicación de salida de notas paralelas, relativa a la raíz del Vault.', + settingMaxCacheDesc: + 'Elimina las cachés de notas menos usadas recientemente por encima de este límite. La caché se guarda en cache.json del plugin.', + cachedNotesDesc: 'La caché se invalida por el SHA1 de la fuente y la huella de la configuración de generación.', + settingStreamingDesc: + 'Muestra el progreso de salida LLM en tiempo real durante la generación (solo formatos OpenAI Chat y Anthropic).', + settingStreamingTimeoutDesc: + 'Tiempo de espera para una solicitud streaming; valores inválidos o menores de 1000 ms usan el predeterminado.', + batchSelectFolder: 'Introduce la ruta de carpeta a procesar por lote (vacío para la raíz del Vault)', + batchCancelRequested: 'Cancelación por lote solicitada; se detendrá después de la nota actual.', + batchInvalidFolderInput: 'Ruta de carpeta inválida: {path}', + batchFolderNotFound: 'Carpeta no encontrada: {path}', +}); diff --git a/src/i18n.ts b/src/i18n.ts index 8ee4675..98b4bd1 100644 --- a/src/i18n.ts +++ b/src/i18n.ts @@ -3,14 +3,23 @@ import { STRINGS } from './i18n-strings'; import type { PluginSettings } from './types'; -export { STRINGS } from './i18n-strings'; +export { LOCALE_OVERRIDES, STRINGS } from './i18n-strings'; + +function supportedBaseLanguage(value: unknown): string | null { + const base = String(value || '') + .trim() + .toLowerCase() + .split(/[-_]/)[0]; + return base && STRINGS[base] ? base : null; +} export function resolveUiLanguage(settings: Pick | null): string { const configured = settings?.uiLanguage; - if (configured === 'zh' || configured === 'en') return configured; + if (configured && configured !== 'auto') { + return supportedBaseLanguage(configured) || 'en'; + } const nav = typeof navigator !== 'undefined' ? navigator : null; - const language = String(nav?.language || '').toLowerCase(); - return language.startsWith('zh') ? 'zh' : 'en'; + return supportedBaseLanguage(nav?.language) || 'en'; } export function translate( diff --git a/src/prompt.ts b/src/prompt.ts index 711b9d9..853f65d 100644 --- a/src/prompt.ts +++ b/src/prompt.ts @@ -3,21 +3,51 @@ import { DEFAULT_SETTINGS, MAX_DOC_CHARS, normalizeCardCount, PROMPT_LANGUAGES } from './settings'; import type { PluginSettings, PromptPair } from './types'; +const PROMPT_LANGUAGE_INSTRUCTIONS: Record = { + auto: 'Write title, gist, and bullets in the main language of the source document.', + zh: '用中文输出 title、gist 和 bullets。', + en: 'Write title, gist, and bullets in English.', + ja: 'Write title, gist, and bullets in Japanese.', + ko: 'Write title, gist, and bullets in Korean.', + fr: 'Write title, gist, and bullets in French.', + de: 'Write title, gist, and bullets in German.', + es: 'Write title, gist, and bullets in Spanish.', +}; + +const PROMPT_SCHEMA_EXAMPLES: Record = { + zh: `{"cards":[ + {"title":"U 型收益曲线","anchor":"那谁又会被 AI 所受益?整体来看,它把整个分数变成了一分到七分","gist":"AI 生产力收益呈 U 型,两端受益最大、中间层塌陷","bullets":["最高薪岗位(软件管理)通过加速既有工作受益最大","最低薪岗位(外卖员、园艺工)用 AI 开副业创造新收入","中间层科学家、律师收益最少,部分因对 prompt 精度信任不足","全体均分 5.1/7,42% 报告收益模糊"]} +]}`, + en: `{"cards":[ + {"title":"U-shaped gains","anchor":"Who benefits from AI? Overall, it shifts the score from one to seven","gist":"AI productivity gains form a U shape, with both ends benefiting most","bullets":["Top-paid software managers benefit by accelerating existing work","Low-paid workers use AI to create new side income","Middle-layer specialists gain less because prompt precision is hard to trust","Average reported benefit is 5.1/7, with 42% describing gains as unclear"]} +]}`, + ja: `{"cards":[ + {"title":"U字型の利益","anchor":"Who benefits from AI? Overall, it shifts the score from one to seven","gist":"AIによる生産性向上はU字型になり、両端の層がもっとも大きな恩恵を受ける","bullets":["高所得のソフトウェア管理職は既存業務を加速できるため大きく恩恵を受ける","低所得の労働者はAIを使って新しい副収入を作り出せる","中間層の専門職は、プロンプト精度への信頼が難しいため利益が小さい","平均利益は5.1/7で、42%が効果は不明確だと報告している"]} +]}`, + ko: `{"cards":[ + {"title":"U자형 이득","anchor":"Who benefits from AI? Overall, it shifts the score from one to seven","gist":"AI 생산성 향상은 U자형을 보이며 양끝 집단이 가장 큰 혜택을 얻는다","bullets":["고소득 소프트웨어 관리자는 기존 업무를 더 빠르게 처리해 큰 이득을 얻는다","저소득 노동자는 AI로 새로운 부수입 기회를 만들 수 있다","중간층 전문가는 프롬프트 정확도를 신뢰하기 어려워 상대적으로 이득이 작다","평균 체감 이득은 5.1/7이며 42%는 효과가 불명확하다고 답했다"]} +]}`, + fr: `{"cards":[ + {"title":"Gains en U","anchor":"Who benefits from AI? Overall, it shifts the score from one to seven","gist":"Les gains de productivité liés à l'AI forment une courbe en U, les deux extrémités en profitant le plus","bullets":["Les managers logiciels très rémunérés gagnent surtout en accélérant leur travail existant","Les travailleurs peu rémunérés utilisent l'AI pour créer de nouveaux revenus complémentaires","Les spécialistes intermédiaires gagnent moins, car la précision des prompts reste difficile à fiabiliser","Le bénéfice moyen déclaré est de 5,1/7, avec 42% de gains jugés peu clairs"]} +]}`, + de: `{"cards":[ + {"title":"U-förmige Gewinne","anchor":"Who benefits from AI? Overall, it shifts the score from one to seven","gist":"AI-Produktivitätsgewinne bilden eine U-Form, bei der beide Enden am stärksten profitieren","bullets":["Hochbezahlte Softwaremanager profitieren, weil sie bestehende Arbeit beschleunigen","Geringverdienende nutzen AI, um neue Nebeneinnahmen zu schaffen","Spezialisten in der Mitte gewinnen weniger, weil präzise Prompts schwer zu vertrauen sind","Der gemeldete Durchschnittsnutzen liegt bei 5,1/7; 42% beschreiben die Gewinne als unklar"]} +]}`, + es: `{"cards":[ + {"title":"Ganancias en forma de U","anchor":"Who benefits from AI? Overall, it shifts the score from one to seven","gist":"Las mejoras de productividad con AI forman una U: los extremos son quienes más se benefician","bullets":["Los gerentes de software mejor pagados se benefician al acelerar su trabajo existente","Los trabajadores con menores ingresos usan AI para crear nuevas fuentes de ingreso","Los especialistas intermedios ganan menos porque es difícil confiar en la precisión del prompt","El beneficio medio reportado es 5,1/7, con un 42% que describe las ganancias como poco claras"]} +]}`, +}; + +function usesEnglishPromptShell(language: string): boolean { + return language !== 'zh' && language !== 'auto'; +} + export function promptLanguageInstruction(language: string): string { - if (language === 'en') return 'Write title, gist, and bullets in English.'; - if (language === 'auto') return 'Write title, gist, and bullets in the main language of the source document.'; - return '用中文输出 title、gist 和 bullets。'; + return PROMPT_LANGUAGE_INSTRUCTIONS[language] || PROMPT_LANGUAGE_INSTRUCTIONS.zh; } export function promptSchemaExample(language: string): string { - if (language === 'en') { - return `{"cards":[ - {"title":"U-shaped gains","anchor":"Who benefits from AI? Overall, it shifts the score from one to seven","gist":"AI productivity gains form a U shape, with both ends benefiting most","bullets":["Top-paid software managers benefit by accelerating existing work","Low-paid workers use AI to create new side income","Middle-layer specialists gain less because prompt precision is hard to trust","Average reported benefit is 5.1/7, with 42% describing gains as unclear"]} -]}`; - } - return `{"cards":[ - {"title":"U 型收益曲线","anchor":"那谁又会被 AI 所受益?整体来看,它把整个分数变成了一分到七分","gist":"AI 生产力收益呈 U 型,两端受益最大、中间层塌陷","bullets":["最高薪岗位(软件管理)通过加速既有工作受益最大","最低薪岗位(外卖员、园艺工)用 AI 开副业创造新收入","中间层科学家、律师收益最少,部分因对 prompt 精度信任不足","全体均分 5.1/7,42% 报告收益模糊"]} -]}`; + return PROMPT_SCHEMA_EXAMPLES[language] || PROMPT_SCHEMA_EXAMPLES.zh; } export function renderPromptTemplate(template: string, vars: Record): string { @@ -34,7 +64,7 @@ function defaultSystemPrompt( schema: string, example: string, ): string { - if (language === 'en') { + if (usesEnglishPromptShell(language)) { return `You are a long-form reading summary assistant. After reading the full document, split it into ${minCards}-${maxCards} natural topic units. They do not need to match markdown headings; use a complete argument or topic as the unit, merging short sections and splitting long ones when needed. Each card has one guiding sentence plus several bullets. Bullets carry details; gist is the lead-in. @@ -98,7 +128,7 @@ function systemPromptContract( languageInstruction: string, schema: string, ): string { - if (language === 'en') { + if (usesEnglishPromptShell(language)) { return `Non-overridable output contract: - Must output ${minCards}-${maxCards} cards. - ${languageInstruction} @@ -125,7 +155,7 @@ export function buildPrompts(content: string, settings: PluginSettings): PromptP const doc = content.length > maxDocChars ? content.slice(0, maxDocChars) + - (promptLanguage === 'en' ? '\n\n[Document truncated]' : '\n\n[文档过长,已截断]') + (usesEnglishPromptShell(promptLanguage) ? '\n\n[Document truncated]' : '\n\n[文档过长,已截断]') : content; const schema = '{"cards":[{"title":"...","anchor":"...","gist":"...","bullets":["...","..."]}]}'; @@ -141,6 +171,8 @@ export function buildPrompts(content: string, settings: PluginSettings): PromptP ${contract}` : defaultSystem; - const user = promptLanguage === 'en' ? `Source document:\n\n${doc}` : `以下是需要处理的文档全文:\n\n${doc}`; + const user = usesEnglishPromptShell(promptLanguage) + ? `Source document:\n\n${doc}` + : `以下是需要处理的文档全文:\n\n${doc}`; return { system, user }; } diff --git a/src/settings.ts b/src/settings.ts index 9cebda4..0b17a48 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -15,14 +15,24 @@ const DEFAULT_CLI_TIMEOUT_MS = 300000; const MIN_STREAMING_TIMEOUT_MS = 1000; const MIN_CLI_TIMEOUT_MS = 1000; export const PROMPT_LANGUAGES = { + auto: 'Auto-detect', zh: '中文', en: 'English', - auto: 'Auto-detect', + ja: '日本語', + ko: '한국어', + fr: 'Français', + de: 'Deutsch', + es: 'Español', }; export const UI_LANGUAGES = { auto: 'Auto', zh: '中文', en: 'English', + ja: '日本語', + ko: '한국어', + fr: 'Français', + de: 'Deutsch', + es: 'Español', }; export const DEFAULT_SETTINGS: PluginSettings = { diff --git a/tests/direct-i18n.test.js b/tests/direct-i18n.test.js index 57944bd..2839c10 100644 --- a/tests/direct-i18n.test.js +++ b/tests/direct-i18n.test.js @@ -28,16 +28,64 @@ const { assert, requireBundledModule, cleanup } = require('./direct-test-setup') assert.strictEqual(i18n.resolveUiLanguage({ uiLanguage: 'en' }), 'en'); assert.strictEqual(i18n.resolveUiLanguage({ uiLanguage: 'zh' }), 'zh'); + assert.strictEqual(i18n.resolveUiLanguage({ uiLanguage: 'ja' }), 'ja'); + assert.strictEqual(i18n.resolveUiLanguage({ uiLanguage: 'ko' }), 'ko'); + assert.strictEqual(i18n.resolveUiLanguage({ uiLanguage: 'fr' }), 'fr'); + assert.strictEqual(i18n.resolveUiLanguage({ uiLanguage: 'de' }), 'de'); + assert.strictEqual(i18n.resolveUiLanguage({ uiLanguage: 'es' }), 'es'); assert.strictEqual(i18n.resolveUiLanguage(null), 'en'); assert.strictEqual(i18n.resolveUiLanguage({ uiLanguage: 'auto' }), 'en'); + const originalNavigator = Object.getOwnPropertyDescriptor(globalThis, 'navigator'); + const setNavigatorLanguage = (language) => { + Object.defineProperty(globalThis, 'navigator', { + value: { language }, + configurable: true, + }); + }; + try { + setNavigatorLanguage('ja-JP'); + assert.strictEqual(i18n.resolveUiLanguage({ uiLanguage: 'auto' }), 'ja'); + setNavigatorLanguage('ko-KR'); + assert.strictEqual(i18n.resolveUiLanguage({ uiLanguage: 'auto' }), 'ko'); + setNavigatorLanguage('fr-CA'); + assert.strictEqual(i18n.resolveUiLanguage({ uiLanguage: 'auto' }), 'fr'); + setNavigatorLanguage('de_DE'); + assert.strictEqual(i18n.resolveUiLanguage({ uiLanguage: 'auto' }), 'de'); + setNavigatorLanguage('es-MX'); + assert.strictEqual(i18n.resolveUiLanguage({ uiLanguage: 'auto' }), 'es'); + setNavigatorLanguage('it-IT'); + assert.strictEqual(i18n.resolveUiLanguage({ uiLanguage: 'auto' }), 'en'); + } finally { + if (originalNavigator) { + Object.defineProperty(globalThis, 'navigator', originalNavigator); + } else { + delete globalThis.navigator; + } + } + assert.ok(i18n.STRINGS.zh); assert.ok(i18n.STRINGS.en); - const zhKeys = Object.keys(i18n.STRINGS.zh); - const enKeys = Object.keys(i18n.STRINGS.en); - assert.strictEqual(zhKeys.length, enKeys.length); - for (const key of zhKeys) { - assert.ok(i18n.STRINGS.en[key], `en missing key: ${key}`); + assert.ok(i18n.STRINGS.ja); + assert.ok(i18n.STRINGS.ko); + assert.ok(i18n.STRINGS.fr); + assert.ok(i18n.STRINGS.de); + assert.ok(i18n.STRINGS.es); + const supportedLocales = ['zh', 'en', 'ja', 'ko', 'fr', 'de', 'es']; + const enKeys = Object.keys(i18n.STRINGS.en).sort(); + for (const locale of supportedLocales) { + const keys = Object.keys(i18n.STRINGS[locale]).sort(); + assert.deepStrictEqual(keys, enKeys, `${locale} keys must match en`); + for (const key of enKeys) { + assert.ok(i18n.STRINGS[locale][key], `${locale} empty key: ${key}`); + } + } + for (const locale of ['ja', 'ko', 'fr', 'de', 'es']) { + const overrideKeys = Object.keys(i18n.LOCALE_OVERRIDES[locale]).sort(); + assert.deepStrictEqual(overrideKeys, enKeys, `${locale} raw override keys must match en`); + for (const key of enKeys) { + assert.ok(i18n.LOCALE_OVERRIDES[locale][key], `${locale} empty raw override: ${key}`); + } } console.log('direct i18n tests passed'); diff --git a/tests/direct-prompt.test.js b/tests/direct-prompt.test.js index fe6b770..72df5e2 100644 --- a/tests/direct-prompt.test.js +++ b/tests/direct-prompt.test.js @@ -13,8 +13,13 @@ const { assert, requireBundledModule, cleanup } = require('./direct-test-setup') prompt.promptLanguageInstruction('auto'), 'Write title, gist, and bullets in the main language of the source document.', ); + assert.strictEqual(prompt.promptLanguageInstruction('ja'), 'Write title, gist, and bullets in Japanese.'); + assert.strictEqual(prompt.promptLanguageInstruction('ko'), 'Write title, gist, and bullets in Korean.'); + assert.strictEqual(prompt.promptLanguageInstruction('fr'), 'Write title, gist, and bullets in French.'); + assert.strictEqual(prompt.promptLanguageInstruction('de'), 'Write title, gist, and bullets in German.'); + assert.strictEqual(prompt.promptLanguageInstruction('es'), 'Write title, gist, and bullets in Spanish.'); assert.strictEqual( - prompt.promptLanguageInstruction('fr'), + prompt.promptLanguageInstruction('xx'), '用中文输出 title、gist 和 bullets。', 'unknown language falls to zh', ); @@ -29,6 +34,11 @@ const { assert, requireBundledModule, cleanup } = require('./direct-test-setup') assert.ok(zhExample.includes('"cards"'), 'zh example is JSON-like'); assert.ok(prompt.promptSchemaExample('auto').includes('U 型收益曲线'), 'auto falls to zh example'); + assert.ok(prompt.promptSchemaExample('ja').includes('U字型の利益'), 'ja example has expected title'); + assert.ok(prompt.promptSchemaExample('ko').includes('U자형 이득'), 'ko example has expected title'); + assert.ok(prompt.promptSchemaExample('fr').includes('Gains en U'), 'fr example has expected title'); + assert.ok(prompt.promptSchemaExample('de').includes('U-förmige Gewinne'), 'de example has expected title'); + assert.ok(prompt.promptSchemaExample('es').includes('Ganancias en forma de U'), 'es example has expected title'); // ── renderPromptTemplate ── assert.strictEqual(prompt.renderPromptTemplate('Hello {name}!', { name: 'World' }), 'Hello World!'); @@ -55,6 +65,19 @@ const { assert, requireBundledModule, cleanup } = require('./direct-test-setup') const autoResult = prompt.buildPrompts('content', { ...base, promptLanguage: 'auto' }); assert.ok(autoResult.system.includes('main language'), 'auto language instruction'); + // ── buildPrompts: expanded output languages ── + for (const [language, expected] of [ + ['ja', 'Japanese'], + ['ko', 'Korean'], + ['fr', 'French'], + ['de', 'German'], + ['es', 'Spanish'], + ]) { + const result = prompt.buildPrompts('content', { ...base, promptLanguage: language }); + assert.ok(result.system.includes(expected), `${language} language instruction`); + assert.ok(result.user.includes('Source document:'), `${language} uses provider-facing source prefix`); + } + // ── buildPrompts: truncation ── const longDoc = 'x'.repeat(25000); const truncResult = prompt.buildPrompts(longDoc, { ...base, maxDocChars: 20000 }); diff --git a/tests/direct-settings-extra.test.js b/tests/direct-settings-extra.test.js index f1b6b2d..6dc0680 100644 --- a/tests/direct-settings-extra.test.js +++ b/tests/direct-settings-extra.test.js @@ -4,6 +4,10 @@ const { assert, requireBundledModule, cleanup } = require('./direct-test-setup') try { const settings = await requireBundledModule('src/settings.ts'); const base = settings.DEFAULT_SETTINGS; + const supportedLanguageIds = ['auto', 'zh', 'en', 'ja', 'ko', 'fr', 'de', 'es']; + + assert.deepStrictEqual(Object.keys(settings.PROMPT_LANGUAGES), supportedLanguageIds, 'prompt language order'); + assert.deepStrictEqual(Object.keys(settings.UI_LANGUAGES), supportedLanguageIds, 'ui language order'); // ── stableStringify ── assert.strictEqual(settings.stableStringify(42), '42', 'number'); @@ -99,6 +103,19 @@ const { assert, requireBundledModule, cleanup } = require('./direct-test-setup') assert.strictEqual(normed.maxDocChars, base.maxDocChars, 'maxDocChars <1000 → default'); assert.strictEqual(normed.customSystemPrompt, '', 'non-string customSystemPrompt → empty string'); + for (const language of supportedLanguageIds) { + assert.strictEqual( + settings.normalizeSettings({ ...base, uiLanguage: language }).uiLanguage, + language, + `normalizeSettings preserves uiLanguage ${language}`, + ); + assert.strictEqual( + settings.normalizeSettings({ ...base, promptLanguage: language }).promptLanguage, + language, + `normalizeSettings preserves promptLanguage ${language}`, + ); + } + // ── pruneCacheEntries edge cases ── assert.deepStrictEqual(settings.pruneCacheEntries(null, 10), [], 'null cache'); assert.deepStrictEqual(settings.pruneCacheEntries({}, 10), [], 'empty cache'); diff --git a/tests/i18n.test.js b/tests/i18n.test.js index ddaa091..ec07236 100644 --- a/tests/i18n.test.js +++ b/tests/i18n.test.js @@ -2,12 +2,22 @@ const { assert, t } = require('./test-setup'); assert.strictEqual(t.translate({ uiLanguage: 'zh' }, 'appTitle'), '对照阅读笔记', 'zh translation'); assert.strictEqual(t.translate({ uiLanguage: 'en' }, 'appTitle'), 'Parallel Reader', 'en translation'); +assert.strictEqual(t.translate({ uiLanguage: 'ja' }, 'settingTestBackendButton'), 'テスト'); +assert.strictEqual(t.translate({ uiLanguage: 'ko' }, 'settingTestBackendButton'), '테스트'); +assert.strictEqual(t.translate({ uiLanguage: 'fr' }, 'settingTestBackendButton'), 'Tester'); +assert.strictEqual(t.translate({ uiLanguage: 'de' }, 'settingTestBackendButton'), 'Testen'); +assert.strictEqual(t.translate({ uiLanguage: 'es' }, 'settingTestBackendButton'), 'Probar'); assert.strictEqual(t.translate({ uiLanguage: 'en' }, 'nonexistent_key'), 'nonexistent_key', 'missing key returns key'); assert.strictEqual( t.translate({ uiLanguage: 'en' }, 'cacheClearedAll', { count: 5 }), 'Cleared 5 cache entries', 'variable interpolation', ); +assert.strictEqual( + t.translate({ uiLanguage: 'fr' }, 'cacheClearedAll', { count: 5 }), + '5 entrées de cache effacées', + 'new locale interpolation', +); assert.strictEqual( t.translate({ uiLanguage: 'en' }, 'exported', { path: 'foo/bar.md' }), 'Exported → foo/bar.md', diff --git a/versions.json b/versions.json index c6e6c0d..61a7e1c 100644 --- a/versions.json +++ b/versions.json @@ -21,5 +21,6 @@ "1.0.19": "1.8.7", "1.0.20": "1.8.7", "1.0.21": "1.8.7", - "1.0.22": "1.8.7" + "1.0.22": "1.8.7", + "1.0.23": "1.8.7" }