feat: implement first-principles language support architecture

This commit is contained in:
Jacobinwwey 2026-04-09 06:21:17 -05:00
parent 0bd3183783
commit a61ef462b7
59 changed files with 2578 additions and 106 deletions

View file

@ -1,3 +1,4 @@
node_modules/
main.js
ref/

View file

@ -212,6 +212,14 @@ Access plugin settings via:
<img width="817" height="428" alt="Multi-model" src="https://github.com/user-attachments/assets/85e6b854-c0ca-45cc-a55e-24638dceb120" />
### Language Architecture (UI Locale vs Task Output Language)
- **UI Locale** controls only plugin interface text (Settings labels, sidebar buttons, notices, and dialogs). The default `auto` mode follows Obsidian's current UI language.
- **Task Output Language** controls model-generated task output (links, summaries, title generation, Mermaid summary, concept extraction, translation target).
- **Per-task language mode** lets each task resolve its own output language from a unified policy layer instead of scattered per-module overrides.
- **Disable auto translation** keeps non-Translate tasks in source-language context, while explicit Translate tasks still enforce the configured target language.
- Mermaid-related generation paths follow the same language policy and can still trigger Mermaid auto-fix when enabled.
### Stable API Call Settings
- **Enable Stable API Calls (Retry Logic)**:
* **Disabled (Default)**: A single API call failure will stop the current task.
@ -618,6 +626,26 @@ This is the core functionality focused on identifying concepts and adding `[[wik
Contributions are welcome! Please refer to the GitHub repository for guidelines: [https://github.com/Jacobinwwey/obsidian-NotEMD](https://github.com/Jacobinwwey/obsidian-NotEMD)
## Maintainer Regression & Release Workflow
1. Capture a before-change baseline:
```bash
npm run regression:language-baseline
```
2. Implement changes, then compare against the baseline:
```bash
npm run regression:language-compare
```
3. Run verification gates before release:
```bash
npm run build
npm test -- --runInBand
obsidian help
obsidian-cli help
```
4. GitHub release body must be fully bilingual, with one complete English section and one complete Chinese section. Each section must be independently readable.
5. Required release assets: `main.js`, `manifest.json`, `styles.css`, and `README.md`.
## License
MIT License - See [LICENSE](LICENSE) file for details.

View file

@ -222,6 +222,14 @@ Notemd 通过与各种大型语言模型 (LLM) 集成来增强您的 Obsidian
<img width="817" height="428" alt="Multi-model" src="https://github.com/user-attachments/assets/85e6b854-c0ca-45cc-a55e-24638dceb120" />
### 语言架构UI 语言 vs 任务输出语言)
- **UI 语言UI Locale**只控制插件界面文案(设置项、侧边栏按钮、提示、弹窗)。默认 `auto` 会跟随 Obsidian 当前界面语言。
- **任务输出语言Task Output Language**控制模型生成内容的语言添加链接、研究摘要、按标题生成、Mermaid 总结、概念提取、翻译目标)。
- **按任务语言模式**通过统一策略层解析每个任务的输出语言,避免分散在各模块中的语言分支造成行为漂移。
- **禁用自动翻译**后,非翻译任务会保留原文语言上下文;显式“翻译”任务仍按目标语言执行。
- Mermaid 相关生成链路与上述统一语言策略保持一致,并在开启时继续支持自动 Mermaid 修复。
### 稳定 API 调用设置 (Stable API Call Settings)
- 启用稳定 API 调用(重试逻辑)(Enable Stable API Calls (Retry Logic))
@ -688,6 +696,26 @@ Notemd 通过与各种大型语言模型 (LLM) 集成来增强您的 Obsidian
欢迎任何形式的贡献请参考GitHub项目说明: [https://github.com/Jacobinwwey/obsidian-NotEMD](https://github.com/Jacobinwwey/obsidian-NotEMD)
## 维护者回归与发布流程
1. 先采集变更前基线:
```bash
npm run regression:language-baseline
```
2. 完成功能修改后,执行对比回归:
```bash
npm run regression:language-compare
```
3. 发布前执行验证门禁:
```bash
npm run build
npm test -- --runInBand
obsidian help
obsidian-cli help
```
4. GitHub Release 描述必须为完整双语:独立英文段落 + 独立中文段落,且各自可单独阅读。
5. Release 资产必须包含:`main.js`、`manifest.json`、`styles.css`、`README.md`。
## 许可证
MIT许可证 - 详情请见[LICENSE](LICENSE)。

View file

@ -0,0 +1,4 @@
> notemd@1.7.14 build
> tsc -noEmit -skipLibCheck && node esbuild.config.mjs production

View file

@ -0,0 +1,4 @@
> notemd@1.7.14 build
> tsc -noEmit -skipLibCheck && node esbuild.config.mjs production

View file

@ -0,0 +1,4 @@
> notemd@1.7.14 build
> tsc -noEmit -skipLibCheck && node esbuild.config.mjs production

View file

@ -0,0 +1,4 @@
> notemd@1.7.14 build
> tsc -noEmit -skipLibCheck && node esbuild.config.mjs production

View file

@ -0,0 +1,11 @@
=== DATE ===
2026-04-09T06:17:03-05:00
=== NODE/NPM ===
v24.14.1
11.11.0
=== OBSIDIAN CLI ===
Ignored: Error: ENOENT: no such file or directory, open '/root/.config/obsidian/obsidian.json'
2026-04-09 11:17:03 Loaded main app package /opt/obsidian-desktop/1.12.7/resources/obsidian.asar

View file

@ -0,0 +1,293 @@
> notemd@1.7.14 test
> jest --runInBand src/tests/workflowButtons.test.ts src/tests/sidebarDomButtonClicks.test.ts src/tests/llmUtilsProviderSupport.test.ts src/tests/providerDiagnostics.test.ts
PASS src/tests/llmUtilsProviderSupport.test.ts
● Console
console.warn
OpenAI API Call: Attempt 1 failed: OpenAI API request failed: net::ERR_CONNECTION_CLOSED
2349 | lastError = error instanceof Error ? error : new Error(errorMessage); // Store Error object if possible
2350 | progressReporter.log(`${provider.name} API Call: Attempt ${attempt} failed: ${errorMessage}`);
> 2351 | console.warn(`${provider.name} API Call: Attempt ${attempt} failed: ${errorMessage}`);
| ^
2352 |
2353 | // Handle cancellation specifically
2354 | if ((error instanceof Error && error.name === 'AbortError') || errorMessage.includes("cancelled by user")) {
at src/llmUtils.ts:2351:21
at Generator.throw (<anonymous>)
at rejected (node_modules/tslib/tslib.js:116:69)
console.warn
OpenAI API Call: Attempt 1 failed: OpenAI API request failed: net::ERR_CONNECTION_CLOSED
2349 | lastError = error instanceof Error ? error : new Error(errorMessage); // Store Error object if possible
2350 | progressReporter.log(`${provider.name} API Call: Attempt ${attempt} failed: ${errorMessage}`);
> 2351 | console.warn(`${provider.name} API Call: Attempt ${attempt} failed: ${errorMessage}`);
| ^
2352 |
2353 | // Handle cancellation specifically
2354 | if ((error instanceof Error && error.name === 'AbortError') || errorMessage.includes("cancelled by user")) {
at src/llmUtils.ts:2351:21
at Generator.throw (<anonymous>)
at rejected (node_modules/tslib/tslib.js:116:69)
console.warn
OpenAI API Call: Attempt 2 failed: OpenAI API request failed: net::ERR_CONNECTION_CLOSED
2349 | lastError = error instanceof Error ? error : new Error(errorMessage); // Store Error object if possible
2350 | progressReporter.log(`${provider.name} API Call: Attempt ${attempt} failed: ${errorMessage}`);
> 2351 | console.warn(`${provider.name} API Call: Attempt ${attempt} failed: ${errorMessage}`);
| ^
2352 |
2353 | // Handle cancellation specifically
2354 | if ((error instanceof Error && error.name === 'AbortError') || errorMessage.includes("cancelled by user")) {
at src/llmUtils.ts:2351:21
at Generator.throw (<anonymous>)
at rejected (node_modules/tslib/tslib.js:116:69)
console.warn
Anthropic API Call: Attempt 1 failed: Anthropic API request failed: net::ERR_CONNECTION_CLOSED
2349 | lastError = error instanceof Error ? error : new Error(errorMessage); // Store Error object if possible
2350 | progressReporter.log(`${provider.name} API Call: Attempt ${attempt} failed: ${errorMessage}`);
> 2351 | console.warn(`${provider.name} API Call: Attempt ${attempt} failed: ${errorMessage}`);
| ^
2352 |
2353 | // Handle cancellation specifically
2354 | if ((error instanceof Error && error.name === 'AbortError') || errorMessage.includes("cancelled by user")) {
at src/llmUtils.ts:2351:21
at Generator.throw (<anonymous>)
at rejected (node_modules/tslib/tslib.js:116:69)
console.warn
Google API Call: Attempt 1 failed: Google API request failed: net::ERR_CONNECTION_CLOSED
2349 | lastError = error instanceof Error ? error : new Error(errorMessage); // Store Error object if possible
2350 | progressReporter.log(`${provider.name} API Call: Attempt ${attempt} failed: ${errorMessage}`);
> 2351 | console.warn(`${provider.name} API Call: Attempt ${attempt} failed: ${errorMessage}`);
| ^
2352 |
2353 | // Handle cancellation specifically
2354 | if ((error instanceof Error && error.name === 'AbortError') || errorMessage.includes("cancelled by user")) {
at src/llmUtils.ts:2351:21
at Generator.throw (<anonymous>)
at rejected (node_modules/tslib/tslib.js:116:69)
console.warn
Azure OpenAI API Call: Attempt 1 failed: Azure OpenAI API request failed: net::ERR_CONNECTION_CLOSED
2349 | lastError = error instanceof Error ? error : new Error(errorMessage); // Store Error object if possible
2350 | progressReporter.log(`${provider.name} API Call: Attempt ${attempt} failed: ${errorMessage}`);
> 2351 | console.warn(`${provider.name} API Call: Attempt ${attempt} failed: ${errorMessage}`);
| ^
2352 |
2353 | // Handle cancellation specifically
2354 | if ((error instanceof Error && error.name === 'AbortError') || errorMessage.includes("cancelled by user")) {
at src/llmUtils.ts:2351:21
at Generator.throw (<anonymous>)
at rejected (node_modules/tslib/tslib.js:116:69)
console.warn
Ollama API Call: Attempt 1 failed: Ollama API request failed: net::ERR_CONNECTION_CLOSED
2349 | lastError = error instanceof Error ? error : new Error(errorMessage); // Store Error object if possible
2350 | progressReporter.log(`${provider.name} API Call: Attempt ${attempt} failed: ${errorMessage}`);
> 2351 | console.warn(`${provider.name} API Call: Attempt ${attempt} failed: ${errorMessage}`);
| ^
2352 |
2353 | // Handle cancellation specifically
2354 | if ((error instanceof Error && error.name === 'AbortError') || errorMessage.includes("cancelled by user")) {
at src/llmUtils.ts:2351:21
at Generator.throw (<anonymous>)
at rejected (node_modules/tslib/tslib.js:116:69)
console.warn
OpenAI API Call: Attempt 1 failed: OpenAI API error: 400 - Bad request
2349 | lastError = error instanceof Error ? error : new Error(errorMessage); // Store Error object if possible
2350 | progressReporter.log(`${provider.name} API Call: Attempt ${attempt} failed: ${errorMessage}`);
> 2351 | console.warn(`${provider.name} API Call: Attempt ${attempt} failed: ${errorMessage}`);
| ^
2352 |
2353 | // Handle cancellation specifically
2354 | if ((error instanceof Error && error.name === 'AbortError') || errorMessage.includes("cancelled by user")) {
at src/llmUtils.ts:2351:21
at Generator.throw (<anonymous>)
at rejected (node_modules/tslib/tslib.js:116:69)
console.warn
OpenAI API Call: Attempt 1 failed: OpenAI API request failed: socket hang up
2349 | lastError = error instanceof Error ? error : new Error(errorMessage); // Store Error object if possible
2350 | progressReporter.log(`${provider.name} API Call: Attempt ${attempt} failed: ${errorMessage}`);
> 2351 | console.warn(`${provider.name} API Call: Attempt ${attempt} failed: ${errorMessage}`);
| ^
2352 |
2353 | // Handle cancellation specifically
2354 | if ((error instanceof Error && error.name === 'AbortError') || errorMessage.includes("cancelled by user")) {
at src/llmUtils.ts:2351:21
at Generator.throw (<anonymous>)
at rejected (node_modules/tslib/tslib.js:116:69)
console.error
OpenAI API Call: All configured attempts failed.
2407 | }
2408 |
> 2409 | console.error(`${provider.name} API Call: All configured attempts failed.`);
| ^
2410 | throw lastError || new Error(`${provider.name} API call failed after multiple retries.`);
2411 | }
2412 |
at src/llmUtils.ts:2409:13
at Generator.throw (<anonymous>)
at rejected (node_modules/tslib/tslib.js:116:69)
console.warn
OpenAI API Call: Attempt 1 failed: OpenAI API request failed: The operation was aborted.
2349 | lastError = error instanceof Error ? error : new Error(errorMessage); // Store Error object if possible
2350 | progressReporter.log(`${provider.name} API Call: Attempt ${attempt} failed: ${errorMessage}`);
> 2351 | console.warn(`${provider.name} API Call: Attempt ${attempt} failed: ${errorMessage}`);
| ^
2352 |
2353 | // Handle cancellation specifically
2354 | if ((error instanceof Error && error.name === 'AbortError') || errorMessage.includes("cancelled by user")) {
at src/llmUtils.ts:2351:21
at Generator.throw (<anonymous>)
at rejected (node_modules/tslib/tslib.js:116:69)
console.error
OpenAI API Call: All configured attempts failed.
2407 | }
2408 |
> 2409 | console.error(`${provider.name} API Call: All configured attempts failed.`);
| ^
2410 | throw lastError || new Error(`${provider.name} API call failed after multiple retries.`);
2411 | }
2412 |
at src/llmUtils.ts:2409:13
at Generator.throw (<anonymous>)
at rejected (node_modules/tslib/tslib.js:116:69)
console.warn
OpenAI API Call: Attempt 1 failed: OpenAI API request failed: socket hang up
2349 | lastError = error instanceof Error ? error : new Error(errorMessage); // Store Error object if possible
2350 | progressReporter.log(`${provider.name} API Call: Attempt ${attempt} failed: ${errorMessage}`);
> 2351 | console.warn(`${provider.name} API Call: Attempt ${attempt} failed: ${errorMessage}`);
| ^
2352 |
2353 | // Handle cancellation specifically
2354 | if ((error instanceof Error && error.name === 'AbortError') || errorMessage.includes("cancelled by user")) {
at src/llmUtils.ts:2351:21
at Generator.throw (<anonymous>)
at rejected (node_modules/tslib/tslib.js:116:69)
console.error
OpenAI API Call: All configured attempts failed.
2407 | }
2408 |
> 2409 | console.error(`${provider.name} API Call: All configured attempts failed.`);
| ^
2410 | throw lastError || new Error(`${provider.name} API call failed after multiple retries.`);
2411 | }
2412 |
at src/llmUtils.ts:2409:13
at Generator.throw (<anonymous>)
at rejected (node_modules/tslib/tslib.js:116:69)
console.warn
Anthropic API Call: Attempt 1 failed: Anthropic API request failed: socket hang up
2349 | lastError = error instanceof Error ? error : new Error(errorMessage); // Store Error object if possible
2350 | progressReporter.log(`${provider.name} API Call: Attempt ${attempt} failed: ${errorMessage}`);
> 2351 | console.warn(`${provider.name} API Call: Attempt ${attempt} failed: ${errorMessage}`);
| ^
2352 |
2353 | // Handle cancellation specifically
2354 | if ((error instanceof Error && error.name === 'AbortError') || errorMessage.includes("cancelled by user")) {
at src/llmUtils.ts:2351:21
at Generator.throw (<anonymous>)
at rejected (node_modules/tslib/tslib.js:116:69)
console.error
Anthropic API Call: All configured attempts failed.
2407 | }
2408 |
> 2409 | console.error(`${provider.name} API Call: All configured attempts failed.`);
| ^
2410 | throw lastError || new Error(`${provider.name} API call failed after multiple retries.`);
2411 | }
2412 |
at src/llmUtils.ts:2409:13
at Generator.throw (<anonymous>)
at rejected (node_modules/tslib/tslib.js:116:69)
console.warn
Ollama API Call: Attempt 1 failed: Ollama API request failed: socket hang up
2349 | lastError = error instanceof Error ? error : new Error(errorMessage); // Store Error object if possible
2350 | progressReporter.log(`${provider.name} API Call: Attempt ${attempt} failed: ${errorMessage}`);
> 2351 | console.warn(`${provider.name} API Call: Attempt ${attempt} failed: ${errorMessage}`);
| ^
2352 |
2353 | // Handle cancellation specifically
2354 | if ((error instanceof Error && error.name === 'AbortError') || errorMessage.includes("cancelled by user")) {
at src/llmUtils.ts:2351:21
at Generator.throw (<anonymous>)
at rejected (node_modules/tslib/tslib.js:116:69)
console.error
Ollama API Call: All configured attempts failed.
2407 | }
2408 |
> 2409 | console.error(`${provider.name} API Call: All configured attempts failed.`);
| ^
2410 | throw lastError || new Error(`${provider.name} API call failed after multiple retries.`);
2411 | }
2412 |
at src/llmUtils.ts:2409:13
at Generator.throw (<anonymous>)
at rejected (node_modules/tslib/tslib.js:116:69)
PASS src/tests/sidebarDomButtonClicks.test.ts
● Console
console.log
Potential duplicates in Active.txt: [ 'duplicate' ]
at NotemdSidebarView.<anonymous> (src/ui/NotemdSidebarView.ts:566:29)
PASS src/tests/providerDiagnostics.test.ts
PASS src/tests/workflowButtons.test.ts
Test Suites: 4 passed, 4 total
Tests: 80 passed, 80 total
Snapshots: 0 total
Time: 0.782 s, estimated 1 s
Ran all test suites matching /src\/tests\/workflowButtons.test.ts|src\/tests\/sidebarDomButtonClicks.test.ts|src\/tests\/llmUtilsProviderSupport.test.ts|src\/tests\/providerDiagnostics.test.ts/i.

View file

@ -0,0 +1,293 @@
> notemd@1.7.14 test
> jest --runInBand src/tests/workflowButtons.test.ts src/tests/sidebarDomButtonClicks.test.ts src/tests/llmUtilsProviderSupport.test.ts src/tests/providerDiagnostics.test.ts
PASS src/tests/llmUtilsProviderSupport.test.ts
● Console
console.warn
OpenAI API Call: Attempt 1 failed: OpenAI API request failed: net::ERR_CONNECTION_CLOSED
2349 | lastError = error instanceof Error ? error : new Error(errorMessage); // Store Error object if possible
2350 | progressReporter.log(`${provider.name} API Call: Attempt ${attempt} failed: ${errorMessage}`);
> 2351 | console.warn(`${provider.name} API Call: Attempt ${attempt} failed: ${errorMessage}`);
| ^
2352 |
2353 | // Handle cancellation specifically
2354 | if ((error instanceof Error && error.name === 'AbortError') || errorMessage.includes("cancelled by user")) {
at src/llmUtils.ts:2351:21
at Generator.throw (<anonymous>)
at rejected (node_modules/tslib/tslib.js:116:69)
console.warn
OpenAI API Call: Attempt 1 failed: OpenAI API request failed: net::ERR_CONNECTION_CLOSED
2349 | lastError = error instanceof Error ? error : new Error(errorMessage); // Store Error object if possible
2350 | progressReporter.log(`${provider.name} API Call: Attempt ${attempt} failed: ${errorMessage}`);
> 2351 | console.warn(`${provider.name} API Call: Attempt ${attempt} failed: ${errorMessage}`);
| ^
2352 |
2353 | // Handle cancellation specifically
2354 | if ((error instanceof Error && error.name === 'AbortError') || errorMessage.includes("cancelled by user")) {
at src/llmUtils.ts:2351:21
at Generator.throw (<anonymous>)
at rejected (node_modules/tslib/tslib.js:116:69)
console.warn
OpenAI API Call: Attempt 2 failed: OpenAI API request failed: net::ERR_CONNECTION_CLOSED
2349 | lastError = error instanceof Error ? error : new Error(errorMessage); // Store Error object if possible
2350 | progressReporter.log(`${provider.name} API Call: Attempt ${attempt} failed: ${errorMessage}`);
> 2351 | console.warn(`${provider.name} API Call: Attempt ${attempt} failed: ${errorMessage}`);
| ^
2352 |
2353 | // Handle cancellation specifically
2354 | if ((error instanceof Error && error.name === 'AbortError') || errorMessage.includes("cancelled by user")) {
at src/llmUtils.ts:2351:21
at Generator.throw (<anonymous>)
at rejected (node_modules/tslib/tslib.js:116:69)
console.warn
Anthropic API Call: Attempt 1 failed: Anthropic API request failed: net::ERR_CONNECTION_CLOSED
2349 | lastError = error instanceof Error ? error : new Error(errorMessage); // Store Error object if possible
2350 | progressReporter.log(`${provider.name} API Call: Attempt ${attempt} failed: ${errorMessage}`);
> 2351 | console.warn(`${provider.name} API Call: Attempt ${attempt} failed: ${errorMessage}`);
| ^
2352 |
2353 | // Handle cancellation specifically
2354 | if ((error instanceof Error && error.name === 'AbortError') || errorMessage.includes("cancelled by user")) {
at src/llmUtils.ts:2351:21
at Generator.throw (<anonymous>)
at rejected (node_modules/tslib/tslib.js:116:69)
console.warn
Google API Call: Attempt 1 failed: Google API request failed: net::ERR_CONNECTION_CLOSED
2349 | lastError = error instanceof Error ? error : new Error(errorMessage); // Store Error object if possible
2350 | progressReporter.log(`${provider.name} API Call: Attempt ${attempt} failed: ${errorMessage}`);
> 2351 | console.warn(`${provider.name} API Call: Attempt ${attempt} failed: ${errorMessage}`);
| ^
2352 |
2353 | // Handle cancellation specifically
2354 | if ((error instanceof Error && error.name === 'AbortError') || errorMessage.includes("cancelled by user")) {
at src/llmUtils.ts:2351:21
at Generator.throw (<anonymous>)
at rejected (node_modules/tslib/tslib.js:116:69)
console.warn
Azure OpenAI API Call: Attempt 1 failed: Azure OpenAI API request failed: net::ERR_CONNECTION_CLOSED
2349 | lastError = error instanceof Error ? error : new Error(errorMessage); // Store Error object if possible
2350 | progressReporter.log(`${provider.name} API Call: Attempt ${attempt} failed: ${errorMessage}`);
> 2351 | console.warn(`${provider.name} API Call: Attempt ${attempt} failed: ${errorMessage}`);
| ^
2352 |
2353 | // Handle cancellation specifically
2354 | if ((error instanceof Error && error.name === 'AbortError') || errorMessage.includes("cancelled by user")) {
at src/llmUtils.ts:2351:21
at Generator.throw (<anonymous>)
at rejected (node_modules/tslib/tslib.js:116:69)
console.warn
Ollama API Call: Attempt 1 failed: Ollama API request failed: net::ERR_CONNECTION_CLOSED
2349 | lastError = error instanceof Error ? error : new Error(errorMessage); // Store Error object if possible
2350 | progressReporter.log(`${provider.name} API Call: Attempt ${attempt} failed: ${errorMessage}`);
> 2351 | console.warn(`${provider.name} API Call: Attempt ${attempt} failed: ${errorMessage}`);
| ^
2352 |
2353 | // Handle cancellation specifically
2354 | if ((error instanceof Error && error.name === 'AbortError') || errorMessage.includes("cancelled by user")) {
at src/llmUtils.ts:2351:21
at Generator.throw (<anonymous>)
at rejected (node_modules/tslib/tslib.js:116:69)
console.warn
OpenAI API Call: Attempt 1 failed: OpenAI API error: 400 - Bad request
2349 | lastError = error instanceof Error ? error : new Error(errorMessage); // Store Error object if possible
2350 | progressReporter.log(`${provider.name} API Call: Attempt ${attempt} failed: ${errorMessage}`);
> 2351 | console.warn(`${provider.name} API Call: Attempt ${attempt} failed: ${errorMessage}`);
| ^
2352 |
2353 | // Handle cancellation specifically
2354 | if ((error instanceof Error && error.name === 'AbortError') || errorMessage.includes("cancelled by user")) {
at src/llmUtils.ts:2351:21
at Generator.throw (<anonymous>)
at rejected (node_modules/tslib/tslib.js:116:69)
console.warn
OpenAI API Call: Attempt 1 failed: OpenAI API request failed: socket hang up
2349 | lastError = error instanceof Error ? error : new Error(errorMessage); // Store Error object if possible
2350 | progressReporter.log(`${provider.name} API Call: Attempt ${attempt} failed: ${errorMessage}`);
> 2351 | console.warn(`${provider.name} API Call: Attempt ${attempt} failed: ${errorMessage}`);
| ^
2352 |
2353 | // Handle cancellation specifically
2354 | if ((error instanceof Error && error.name === 'AbortError') || errorMessage.includes("cancelled by user")) {
at src/llmUtils.ts:2351:21
at Generator.throw (<anonymous>)
at rejected (node_modules/tslib/tslib.js:116:69)
console.error
OpenAI API Call: All configured attempts failed.
2407 | }
2408 |
> 2409 | console.error(`${provider.name} API Call: All configured attempts failed.`);
| ^
2410 | throw lastError || new Error(`${provider.name} API call failed after multiple retries.`);
2411 | }
2412 |
at src/llmUtils.ts:2409:13
at Generator.throw (<anonymous>)
at rejected (node_modules/tslib/tslib.js:116:69)
console.warn
OpenAI API Call: Attempt 1 failed: OpenAI API request failed: The operation was aborted.
2349 | lastError = error instanceof Error ? error : new Error(errorMessage); // Store Error object if possible
2350 | progressReporter.log(`${provider.name} API Call: Attempt ${attempt} failed: ${errorMessage}`);
> 2351 | console.warn(`${provider.name} API Call: Attempt ${attempt} failed: ${errorMessage}`);
| ^
2352 |
2353 | // Handle cancellation specifically
2354 | if ((error instanceof Error && error.name === 'AbortError') || errorMessage.includes("cancelled by user")) {
at src/llmUtils.ts:2351:21
at Generator.throw (<anonymous>)
at rejected (node_modules/tslib/tslib.js:116:69)
console.error
OpenAI API Call: All configured attempts failed.
2407 | }
2408 |
> 2409 | console.error(`${provider.name} API Call: All configured attempts failed.`);
| ^
2410 | throw lastError || new Error(`${provider.name} API call failed after multiple retries.`);
2411 | }
2412 |
at src/llmUtils.ts:2409:13
at Generator.throw (<anonymous>)
at rejected (node_modules/tslib/tslib.js:116:69)
console.warn
OpenAI API Call: Attempt 1 failed: OpenAI API request failed: socket hang up
2349 | lastError = error instanceof Error ? error : new Error(errorMessage); // Store Error object if possible
2350 | progressReporter.log(`${provider.name} API Call: Attempt ${attempt} failed: ${errorMessage}`);
> 2351 | console.warn(`${provider.name} API Call: Attempt ${attempt} failed: ${errorMessage}`);
| ^
2352 |
2353 | // Handle cancellation specifically
2354 | if ((error instanceof Error && error.name === 'AbortError') || errorMessage.includes("cancelled by user")) {
at src/llmUtils.ts:2351:21
at Generator.throw (<anonymous>)
at rejected (node_modules/tslib/tslib.js:116:69)
console.error
OpenAI API Call: All configured attempts failed.
2407 | }
2408 |
> 2409 | console.error(`${provider.name} API Call: All configured attempts failed.`);
| ^
2410 | throw lastError || new Error(`${provider.name} API call failed after multiple retries.`);
2411 | }
2412 |
at src/llmUtils.ts:2409:13
at Generator.throw (<anonymous>)
at rejected (node_modules/tslib/tslib.js:116:69)
console.warn
Anthropic API Call: Attempt 1 failed: Anthropic API request failed: socket hang up
2349 | lastError = error instanceof Error ? error : new Error(errorMessage); // Store Error object if possible
2350 | progressReporter.log(`${provider.name} API Call: Attempt ${attempt} failed: ${errorMessage}`);
> 2351 | console.warn(`${provider.name} API Call: Attempt ${attempt} failed: ${errorMessage}`);
| ^
2352 |
2353 | // Handle cancellation specifically
2354 | if ((error instanceof Error && error.name === 'AbortError') || errorMessage.includes("cancelled by user")) {
at src/llmUtils.ts:2351:21
at Generator.throw (<anonymous>)
at rejected (node_modules/tslib/tslib.js:116:69)
console.error
Anthropic API Call: All configured attempts failed.
2407 | }
2408 |
> 2409 | console.error(`${provider.name} API Call: All configured attempts failed.`);
| ^
2410 | throw lastError || new Error(`${provider.name} API call failed after multiple retries.`);
2411 | }
2412 |
at src/llmUtils.ts:2409:13
at Generator.throw (<anonymous>)
at rejected (node_modules/tslib/tslib.js:116:69)
console.warn
Ollama API Call: Attempt 1 failed: Ollama API request failed: socket hang up
2349 | lastError = error instanceof Error ? error : new Error(errorMessage); // Store Error object if possible
2350 | progressReporter.log(`${provider.name} API Call: Attempt ${attempt} failed: ${errorMessage}`);
> 2351 | console.warn(`${provider.name} API Call: Attempt ${attempt} failed: ${errorMessage}`);
| ^
2352 |
2353 | // Handle cancellation specifically
2354 | if ((error instanceof Error && error.name === 'AbortError') || errorMessage.includes("cancelled by user")) {
at src/llmUtils.ts:2351:21
at Generator.throw (<anonymous>)
at rejected (node_modules/tslib/tslib.js:116:69)
console.error
Ollama API Call: All configured attempts failed.
2407 | }
2408 |
> 2409 | console.error(`${provider.name} API Call: All configured attempts failed.`);
| ^
2410 | throw lastError || new Error(`${provider.name} API call failed after multiple retries.`);
2411 | }
2412 |
at src/llmUtils.ts:2409:13
at Generator.throw (<anonymous>)
at rejected (node_modules/tslib/tslib.js:116:69)
PASS src/tests/sidebarDomButtonClicks.test.ts
● Console
console.log
Potential duplicates in Active.txt: [ 'duplicate' ]
at NotemdSidebarView.<anonymous> (src/ui/NotemdSidebarView.ts:576:29)
PASS src/tests/providerDiagnostics.test.ts
PASS src/tests/workflowButtons.test.ts
Test Suites: 4 passed, 4 total
Tests: 80 passed, 80 total
Snapshots: 0 total
Time: 0.379 s, estimated 1 s
Ran all test suites matching /src\/tests\/workflowButtons.test.ts|src\/tests\/sidebarDomButtonClicks.test.ts|src\/tests\/llmUtilsProviderSupport.test.ts|src\/tests\/providerDiagnostics.test.ts/i.

View file

@ -0,0 +1,293 @@
> notemd@1.7.14 test
> jest --runInBand src/tests/workflowButtons.test.ts src/tests/sidebarDomButtonClicks.test.ts src/tests/llmUtilsProviderSupport.test.ts src/tests/providerDiagnostics.test.ts
PASS src/tests/llmUtilsProviderSupport.test.ts
● Console
console.warn
OpenAI API Call: Attempt 1 failed: OpenAI API request failed: net::ERR_CONNECTION_CLOSED
2349 | lastError = error instanceof Error ? error : new Error(errorMessage); // Store Error object if possible
2350 | progressReporter.log(`${provider.name} API Call: Attempt ${attempt} failed: ${errorMessage}`);
> 2351 | console.warn(`${provider.name} API Call: Attempt ${attempt} failed: ${errorMessage}`);
| ^
2352 |
2353 | // Handle cancellation specifically
2354 | if ((error instanceof Error && error.name === 'AbortError') || errorMessage.includes("cancelled by user")) {
at src/llmUtils.ts:2351:21
at Generator.throw (<anonymous>)
at rejected (node_modules/tslib/tslib.js:116:69)
console.warn
OpenAI API Call: Attempt 1 failed: OpenAI API request failed: net::ERR_CONNECTION_CLOSED
2349 | lastError = error instanceof Error ? error : new Error(errorMessage); // Store Error object if possible
2350 | progressReporter.log(`${provider.name} API Call: Attempt ${attempt} failed: ${errorMessage}`);
> 2351 | console.warn(`${provider.name} API Call: Attempt ${attempt} failed: ${errorMessage}`);
| ^
2352 |
2353 | // Handle cancellation specifically
2354 | if ((error instanceof Error && error.name === 'AbortError') || errorMessage.includes("cancelled by user")) {
at src/llmUtils.ts:2351:21
at Generator.throw (<anonymous>)
at rejected (node_modules/tslib/tslib.js:116:69)
console.warn
OpenAI API Call: Attempt 2 failed: OpenAI API request failed: net::ERR_CONNECTION_CLOSED
2349 | lastError = error instanceof Error ? error : new Error(errorMessage); // Store Error object if possible
2350 | progressReporter.log(`${provider.name} API Call: Attempt ${attempt} failed: ${errorMessage}`);
> 2351 | console.warn(`${provider.name} API Call: Attempt ${attempt} failed: ${errorMessage}`);
| ^
2352 |
2353 | // Handle cancellation specifically
2354 | if ((error instanceof Error && error.name === 'AbortError') || errorMessage.includes("cancelled by user")) {
at src/llmUtils.ts:2351:21
at Generator.throw (<anonymous>)
at rejected (node_modules/tslib/tslib.js:116:69)
console.warn
Anthropic API Call: Attempt 1 failed: Anthropic API request failed: net::ERR_CONNECTION_CLOSED
2349 | lastError = error instanceof Error ? error : new Error(errorMessage); // Store Error object if possible
2350 | progressReporter.log(`${provider.name} API Call: Attempt ${attempt} failed: ${errorMessage}`);
> 2351 | console.warn(`${provider.name} API Call: Attempt ${attempt} failed: ${errorMessage}`);
| ^
2352 |
2353 | // Handle cancellation specifically
2354 | if ((error instanceof Error && error.name === 'AbortError') || errorMessage.includes("cancelled by user")) {
at src/llmUtils.ts:2351:21
at Generator.throw (<anonymous>)
at rejected (node_modules/tslib/tslib.js:116:69)
console.warn
Google API Call: Attempt 1 failed: Google API request failed: net::ERR_CONNECTION_CLOSED
2349 | lastError = error instanceof Error ? error : new Error(errorMessage); // Store Error object if possible
2350 | progressReporter.log(`${provider.name} API Call: Attempt ${attempt} failed: ${errorMessage}`);
> 2351 | console.warn(`${provider.name} API Call: Attempt ${attempt} failed: ${errorMessage}`);
| ^
2352 |
2353 | // Handle cancellation specifically
2354 | if ((error instanceof Error && error.name === 'AbortError') || errorMessage.includes("cancelled by user")) {
at src/llmUtils.ts:2351:21
at Generator.throw (<anonymous>)
at rejected (node_modules/tslib/tslib.js:116:69)
console.warn
Azure OpenAI API Call: Attempt 1 failed: Azure OpenAI API request failed: net::ERR_CONNECTION_CLOSED
2349 | lastError = error instanceof Error ? error : new Error(errorMessage); // Store Error object if possible
2350 | progressReporter.log(`${provider.name} API Call: Attempt ${attempt} failed: ${errorMessage}`);
> 2351 | console.warn(`${provider.name} API Call: Attempt ${attempt} failed: ${errorMessage}`);
| ^
2352 |
2353 | // Handle cancellation specifically
2354 | if ((error instanceof Error && error.name === 'AbortError') || errorMessage.includes("cancelled by user")) {
at src/llmUtils.ts:2351:21
at Generator.throw (<anonymous>)
at rejected (node_modules/tslib/tslib.js:116:69)
console.warn
Ollama API Call: Attempt 1 failed: Ollama API request failed: net::ERR_CONNECTION_CLOSED
2349 | lastError = error instanceof Error ? error : new Error(errorMessage); // Store Error object if possible
2350 | progressReporter.log(`${provider.name} API Call: Attempt ${attempt} failed: ${errorMessage}`);
> 2351 | console.warn(`${provider.name} API Call: Attempt ${attempt} failed: ${errorMessage}`);
| ^
2352 |
2353 | // Handle cancellation specifically
2354 | if ((error instanceof Error && error.name === 'AbortError') || errorMessage.includes("cancelled by user")) {
at src/llmUtils.ts:2351:21
at Generator.throw (<anonymous>)
at rejected (node_modules/tslib/tslib.js:116:69)
console.warn
OpenAI API Call: Attempt 1 failed: OpenAI API error: 400 - Bad request
2349 | lastError = error instanceof Error ? error : new Error(errorMessage); // Store Error object if possible
2350 | progressReporter.log(`${provider.name} API Call: Attempt ${attempt} failed: ${errorMessage}`);
> 2351 | console.warn(`${provider.name} API Call: Attempt ${attempt} failed: ${errorMessage}`);
| ^
2352 |
2353 | // Handle cancellation specifically
2354 | if ((error instanceof Error && error.name === 'AbortError') || errorMessage.includes("cancelled by user")) {
at src/llmUtils.ts:2351:21
at Generator.throw (<anonymous>)
at rejected (node_modules/tslib/tslib.js:116:69)
console.warn
OpenAI API Call: Attempt 1 failed: OpenAI API request failed: socket hang up
2349 | lastError = error instanceof Error ? error : new Error(errorMessage); // Store Error object if possible
2350 | progressReporter.log(`${provider.name} API Call: Attempt ${attempt} failed: ${errorMessage}`);
> 2351 | console.warn(`${provider.name} API Call: Attempt ${attempt} failed: ${errorMessage}`);
| ^
2352 |
2353 | // Handle cancellation specifically
2354 | if ((error instanceof Error && error.name === 'AbortError') || errorMessage.includes("cancelled by user")) {
at src/llmUtils.ts:2351:21
at Generator.throw (<anonymous>)
at rejected (node_modules/tslib/tslib.js:116:69)
console.error
OpenAI API Call: All configured attempts failed.
2407 | }
2408 |
> 2409 | console.error(`${provider.name} API Call: All configured attempts failed.`);
| ^
2410 | throw lastError || new Error(`${provider.name} API call failed after multiple retries.`);
2411 | }
2412 |
at src/llmUtils.ts:2409:13
at Generator.throw (<anonymous>)
at rejected (node_modules/tslib/tslib.js:116:69)
console.warn
OpenAI API Call: Attempt 1 failed: OpenAI API request failed: The operation was aborted.
2349 | lastError = error instanceof Error ? error : new Error(errorMessage); // Store Error object if possible
2350 | progressReporter.log(`${provider.name} API Call: Attempt ${attempt} failed: ${errorMessage}`);
> 2351 | console.warn(`${provider.name} API Call: Attempt ${attempt} failed: ${errorMessage}`);
| ^
2352 |
2353 | // Handle cancellation specifically
2354 | if ((error instanceof Error && error.name === 'AbortError') || errorMessage.includes("cancelled by user")) {
at src/llmUtils.ts:2351:21
at Generator.throw (<anonymous>)
at rejected (node_modules/tslib/tslib.js:116:69)
console.error
OpenAI API Call: All configured attempts failed.
2407 | }
2408 |
> 2409 | console.error(`${provider.name} API Call: All configured attempts failed.`);
| ^
2410 | throw lastError || new Error(`${provider.name} API call failed after multiple retries.`);
2411 | }
2412 |
at src/llmUtils.ts:2409:13
at Generator.throw (<anonymous>)
at rejected (node_modules/tslib/tslib.js:116:69)
console.warn
OpenAI API Call: Attempt 1 failed: OpenAI API request failed: socket hang up
2349 | lastError = error instanceof Error ? error : new Error(errorMessage); // Store Error object if possible
2350 | progressReporter.log(`${provider.name} API Call: Attempt ${attempt} failed: ${errorMessage}`);
> 2351 | console.warn(`${provider.name} API Call: Attempt ${attempt} failed: ${errorMessage}`);
| ^
2352 |
2353 | // Handle cancellation specifically
2354 | if ((error instanceof Error && error.name === 'AbortError') || errorMessage.includes("cancelled by user")) {
at src/llmUtils.ts:2351:21
at Generator.throw (<anonymous>)
at rejected (node_modules/tslib/tslib.js:116:69)
console.error
OpenAI API Call: All configured attempts failed.
2407 | }
2408 |
> 2409 | console.error(`${provider.name} API Call: All configured attempts failed.`);
| ^
2410 | throw lastError || new Error(`${provider.name} API call failed after multiple retries.`);
2411 | }
2412 |
at src/llmUtils.ts:2409:13
at Generator.throw (<anonymous>)
at rejected (node_modules/tslib/tslib.js:116:69)
console.warn
Anthropic API Call: Attempt 1 failed: Anthropic API request failed: socket hang up
2349 | lastError = error instanceof Error ? error : new Error(errorMessage); // Store Error object if possible
2350 | progressReporter.log(`${provider.name} API Call: Attempt ${attempt} failed: ${errorMessage}`);
> 2351 | console.warn(`${provider.name} API Call: Attempt ${attempt} failed: ${errorMessage}`);
| ^
2352 |
2353 | // Handle cancellation specifically
2354 | if ((error instanceof Error && error.name === 'AbortError') || errorMessage.includes("cancelled by user")) {
at src/llmUtils.ts:2351:21
at Generator.throw (<anonymous>)
at rejected (node_modules/tslib/tslib.js:116:69)
console.error
Anthropic API Call: All configured attempts failed.
2407 | }
2408 |
> 2409 | console.error(`${provider.name} API Call: All configured attempts failed.`);
| ^
2410 | throw lastError || new Error(`${provider.name} API call failed after multiple retries.`);
2411 | }
2412 |
at src/llmUtils.ts:2409:13
at Generator.throw (<anonymous>)
at rejected (node_modules/tslib/tslib.js:116:69)
console.warn
Ollama API Call: Attempt 1 failed: Ollama API request failed: socket hang up
2349 | lastError = error instanceof Error ? error : new Error(errorMessage); // Store Error object if possible
2350 | progressReporter.log(`${provider.name} API Call: Attempt ${attempt} failed: ${errorMessage}`);
> 2351 | console.warn(`${provider.name} API Call: Attempt ${attempt} failed: ${errorMessage}`);
| ^
2352 |
2353 | // Handle cancellation specifically
2354 | if ((error instanceof Error && error.name === 'AbortError') || errorMessage.includes("cancelled by user")) {
at src/llmUtils.ts:2351:21
at Generator.throw (<anonymous>)
at rejected (node_modules/tslib/tslib.js:116:69)
console.error
Ollama API Call: All configured attempts failed.
2407 | }
2408 |
> 2409 | console.error(`${provider.name} API Call: All configured attempts failed.`);
| ^
2410 | throw lastError || new Error(`${provider.name} API call failed after multiple retries.`);
2411 | }
2412 |
at src/llmUtils.ts:2409:13
at Generator.throw (<anonymous>)
at rejected (node_modules/tslib/tslib.js:116:69)
PASS src/tests/sidebarDomButtonClicks.test.ts
● Console
console.log
Potential duplicates in Active.txt: [ 'duplicate' ]
at NotemdSidebarView.<anonymous> (src/ui/NotemdSidebarView.ts:576:29)
PASS src/tests/providerDiagnostics.test.ts
PASS src/tests/workflowButtons.test.ts
Test Suites: 4 passed, 4 total
Tests: 80 passed, 80 total
Snapshots: 0 total
Time: 0.369 s, estimated 1 s
Ran all test suites matching /src\/tests\/workflowButtons.test.ts|src\/tests\/sidebarDomButtonClicks.test.ts|src\/tests\/llmUtilsProviderSupport.test.ts|src\/tests\/providerDiagnostics.test.ts/i.

View file

@ -0,0 +1,23 @@
> notemd@1.7.14 test
> jest --runInBand src/tests/languagePolicy.test.ts
PASS src/tests/languagePolicy.test.ts
task language policy
✓ uses global language when per-task language is disabled (1 ms)
✓ uses task-specific language when enabled
✓ uses extractOriginalText language for merged mode
✓ translate task always uses global translation target
✓ resolves language display name from configured language list
✓ disableAutoTranslation is ignored for explicit translate task
✓ disableAutoTranslation disables auto-translation for non-translate tasks
ui locale resolution
✓ uses manual locale when configured and supported
✓ uses obsidian locale when ui locale is auto
✓ falls back to en when unsupported
Test Suites: 1 passed, 1 total
Tests: 10 passed, 10 total
Snapshots: 0 total
Time: 0.222 s
Ran all test suites matching /src\/tests\/languagePolicy.test.ts/i.

View file

@ -0,0 +1,12 @@
> notemd@1.7.14 test
> jest --runInBand src/tests/languagePolicy.test.ts
No tests found, exiting with code 1
Run with `--passWithNoTests` to exit with code 0
In /home/jacob/obsidian-NotEMD
631 files checked.
testMatch: **/tests/**/*.test.ts - 149 matches
testPathIgnorePatterns: /node_modules/, /ref/ - 84 matches
testRegex: - 0 matches
Pattern: src/tests/languagePolicy.test.ts - 0 matches

View file

@ -0,0 +1,19 @@
> notemd@1.7.14 test
> jest --runInBand src/tests/i18nFallback.test.ts
PASS src/tests/i18nFallback.test.ts
i18n fallback and cache
✓ falls back to English when locale is unsupported (1 ms)
✓ supports zh locale alias resolution
✓ fills missing localized keys from English base catalog (1 ms)
✓ returns cached object for identical locale
✓ resolves strings using Obsidian language when uiLocale is auto
✓ manual ui locale override has priority over Obsidian language
✓ formats placeholders
Test Suites: 1 passed, 1 total
Tests: 7 passed, 7 total
Snapshots: 0 total
Time: 0.234 s
Ran all test suites matching /src\/tests\/i18nFallback.test.ts/i.

View file

@ -0,0 +1,293 @@
> notemd@1.7.14 test
> jest --runInBand src/tests/workflowButtons.test.ts src/tests/sidebarDomButtonClicks.test.ts src/tests/llmUtilsProviderSupport.test.ts src/tests/providerDiagnostics.test.ts
PASS src/tests/llmUtilsProviderSupport.test.ts
● Console
console.warn
OpenAI API Call: Attempt 1 failed: OpenAI API request failed: net::ERR_CONNECTION_CLOSED
2349 | lastError = error instanceof Error ? error : new Error(errorMessage); // Store Error object if possible
2350 | progressReporter.log(`${provider.name} API Call: Attempt ${attempt} failed: ${errorMessage}`);
> 2351 | console.warn(`${provider.name} API Call: Attempt ${attempt} failed: ${errorMessage}`);
| ^
2352 |
2353 | // Handle cancellation specifically
2354 | if ((error instanceof Error && error.name === 'AbortError') || errorMessage.includes("cancelled by user")) {
at src/llmUtils.ts:2351:21
at Generator.throw (<anonymous>)
at rejected (node_modules/tslib/tslib.js:116:69)
console.warn
OpenAI API Call: Attempt 1 failed: OpenAI API request failed: net::ERR_CONNECTION_CLOSED
2349 | lastError = error instanceof Error ? error : new Error(errorMessage); // Store Error object if possible
2350 | progressReporter.log(`${provider.name} API Call: Attempt ${attempt} failed: ${errorMessage}`);
> 2351 | console.warn(`${provider.name} API Call: Attempt ${attempt} failed: ${errorMessage}`);
| ^
2352 |
2353 | // Handle cancellation specifically
2354 | if ((error instanceof Error && error.name === 'AbortError') || errorMessage.includes("cancelled by user")) {
at src/llmUtils.ts:2351:21
at Generator.throw (<anonymous>)
at rejected (node_modules/tslib/tslib.js:116:69)
console.warn
OpenAI API Call: Attempt 2 failed: OpenAI API request failed: net::ERR_CONNECTION_CLOSED
2349 | lastError = error instanceof Error ? error : new Error(errorMessage); // Store Error object if possible
2350 | progressReporter.log(`${provider.name} API Call: Attempt ${attempt} failed: ${errorMessage}`);
> 2351 | console.warn(`${provider.name} API Call: Attempt ${attempt} failed: ${errorMessage}`);
| ^
2352 |
2353 | // Handle cancellation specifically
2354 | if ((error instanceof Error && error.name === 'AbortError') || errorMessage.includes("cancelled by user")) {
at src/llmUtils.ts:2351:21
at Generator.throw (<anonymous>)
at rejected (node_modules/tslib/tslib.js:116:69)
console.warn
Anthropic API Call: Attempt 1 failed: Anthropic API request failed: net::ERR_CONNECTION_CLOSED
2349 | lastError = error instanceof Error ? error : new Error(errorMessage); // Store Error object if possible
2350 | progressReporter.log(`${provider.name} API Call: Attempt ${attempt} failed: ${errorMessage}`);
> 2351 | console.warn(`${provider.name} API Call: Attempt ${attempt} failed: ${errorMessage}`);
| ^
2352 |
2353 | // Handle cancellation specifically
2354 | if ((error instanceof Error && error.name === 'AbortError') || errorMessage.includes("cancelled by user")) {
at src/llmUtils.ts:2351:21
at Generator.throw (<anonymous>)
at rejected (node_modules/tslib/tslib.js:116:69)
console.warn
Google API Call: Attempt 1 failed: Google API request failed: net::ERR_CONNECTION_CLOSED
2349 | lastError = error instanceof Error ? error : new Error(errorMessage); // Store Error object if possible
2350 | progressReporter.log(`${provider.name} API Call: Attempt ${attempt} failed: ${errorMessage}`);
> 2351 | console.warn(`${provider.name} API Call: Attempt ${attempt} failed: ${errorMessage}`);
| ^
2352 |
2353 | // Handle cancellation specifically
2354 | if ((error instanceof Error && error.name === 'AbortError') || errorMessage.includes("cancelled by user")) {
at src/llmUtils.ts:2351:21
at Generator.throw (<anonymous>)
at rejected (node_modules/tslib/tslib.js:116:69)
console.warn
Azure OpenAI API Call: Attempt 1 failed: Azure OpenAI API request failed: net::ERR_CONNECTION_CLOSED
2349 | lastError = error instanceof Error ? error : new Error(errorMessage); // Store Error object if possible
2350 | progressReporter.log(`${provider.name} API Call: Attempt ${attempt} failed: ${errorMessage}`);
> 2351 | console.warn(`${provider.name} API Call: Attempt ${attempt} failed: ${errorMessage}`);
| ^
2352 |
2353 | // Handle cancellation specifically
2354 | if ((error instanceof Error && error.name === 'AbortError') || errorMessage.includes("cancelled by user")) {
at src/llmUtils.ts:2351:21
at Generator.throw (<anonymous>)
at rejected (node_modules/tslib/tslib.js:116:69)
console.warn
Ollama API Call: Attempt 1 failed: Ollama API request failed: net::ERR_CONNECTION_CLOSED
2349 | lastError = error instanceof Error ? error : new Error(errorMessage); // Store Error object if possible
2350 | progressReporter.log(`${provider.name} API Call: Attempt ${attempt} failed: ${errorMessage}`);
> 2351 | console.warn(`${provider.name} API Call: Attempt ${attempt} failed: ${errorMessage}`);
| ^
2352 |
2353 | // Handle cancellation specifically
2354 | if ((error instanceof Error && error.name === 'AbortError') || errorMessage.includes("cancelled by user")) {
at src/llmUtils.ts:2351:21
at Generator.throw (<anonymous>)
at rejected (node_modules/tslib/tslib.js:116:69)
console.warn
OpenAI API Call: Attempt 1 failed: OpenAI API error: 400 - Bad request
2349 | lastError = error instanceof Error ? error : new Error(errorMessage); // Store Error object if possible
2350 | progressReporter.log(`${provider.name} API Call: Attempt ${attempt} failed: ${errorMessage}`);
> 2351 | console.warn(`${provider.name} API Call: Attempt ${attempt} failed: ${errorMessage}`);
| ^
2352 |
2353 | // Handle cancellation specifically
2354 | if ((error instanceof Error && error.name === 'AbortError') || errorMessage.includes("cancelled by user")) {
at src/llmUtils.ts:2351:21
at Generator.throw (<anonymous>)
at rejected (node_modules/tslib/tslib.js:116:69)
console.warn
OpenAI API Call: Attempt 1 failed: OpenAI API request failed: socket hang up
2349 | lastError = error instanceof Error ? error : new Error(errorMessage); // Store Error object if possible
2350 | progressReporter.log(`${provider.name} API Call: Attempt ${attempt} failed: ${errorMessage}`);
> 2351 | console.warn(`${provider.name} API Call: Attempt ${attempt} failed: ${errorMessage}`);
| ^
2352 |
2353 | // Handle cancellation specifically
2354 | if ((error instanceof Error && error.name === 'AbortError') || errorMessage.includes("cancelled by user")) {
at src/llmUtils.ts:2351:21
at Generator.throw (<anonymous>)
at rejected (node_modules/tslib/tslib.js:116:69)
console.error
OpenAI API Call: All configured attempts failed.
2407 | }
2408 |
> 2409 | console.error(`${provider.name} API Call: All configured attempts failed.`);
| ^
2410 | throw lastError || new Error(`${provider.name} API call failed after multiple retries.`);
2411 | }
2412 |
at src/llmUtils.ts:2409:13
at Generator.throw (<anonymous>)
at rejected (node_modules/tslib/tslib.js:116:69)
console.warn
OpenAI API Call: Attempt 1 failed: OpenAI API request failed: The operation was aborted.
2349 | lastError = error instanceof Error ? error : new Error(errorMessage); // Store Error object if possible
2350 | progressReporter.log(`${provider.name} API Call: Attempt ${attempt} failed: ${errorMessage}`);
> 2351 | console.warn(`${provider.name} API Call: Attempt ${attempt} failed: ${errorMessage}`);
| ^
2352 |
2353 | // Handle cancellation specifically
2354 | if ((error instanceof Error && error.name === 'AbortError') || errorMessage.includes("cancelled by user")) {
at src/llmUtils.ts:2351:21
at Generator.throw (<anonymous>)
at rejected (node_modules/tslib/tslib.js:116:69)
console.error
OpenAI API Call: All configured attempts failed.
2407 | }
2408 |
> 2409 | console.error(`${provider.name} API Call: All configured attempts failed.`);
| ^
2410 | throw lastError || new Error(`${provider.name} API call failed after multiple retries.`);
2411 | }
2412 |
at src/llmUtils.ts:2409:13
at Generator.throw (<anonymous>)
at rejected (node_modules/tslib/tslib.js:116:69)
console.warn
OpenAI API Call: Attempt 1 failed: OpenAI API request failed: socket hang up
2349 | lastError = error instanceof Error ? error : new Error(errorMessage); // Store Error object if possible
2350 | progressReporter.log(`${provider.name} API Call: Attempt ${attempt} failed: ${errorMessage}`);
> 2351 | console.warn(`${provider.name} API Call: Attempt ${attempt} failed: ${errorMessage}`);
| ^
2352 |
2353 | // Handle cancellation specifically
2354 | if ((error instanceof Error && error.name === 'AbortError') || errorMessage.includes("cancelled by user")) {
at src/llmUtils.ts:2351:21
at Generator.throw (<anonymous>)
at rejected (node_modules/tslib/tslib.js:116:69)
console.error
OpenAI API Call: All configured attempts failed.
2407 | }
2408 |
> 2409 | console.error(`${provider.name} API Call: All configured attempts failed.`);
| ^
2410 | throw lastError || new Error(`${provider.name} API call failed after multiple retries.`);
2411 | }
2412 |
at src/llmUtils.ts:2409:13
at Generator.throw (<anonymous>)
at rejected (node_modules/tslib/tslib.js:116:69)
console.warn
Anthropic API Call: Attempt 1 failed: Anthropic API request failed: socket hang up
2349 | lastError = error instanceof Error ? error : new Error(errorMessage); // Store Error object if possible
2350 | progressReporter.log(`${provider.name} API Call: Attempt ${attempt} failed: ${errorMessage}`);
> 2351 | console.warn(`${provider.name} API Call: Attempt ${attempt} failed: ${errorMessage}`);
| ^
2352 |
2353 | // Handle cancellation specifically
2354 | if ((error instanceof Error && error.name === 'AbortError') || errorMessage.includes("cancelled by user")) {
at src/llmUtils.ts:2351:21
at Generator.throw (<anonymous>)
at rejected (node_modules/tslib/tslib.js:116:69)
console.error
Anthropic API Call: All configured attempts failed.
2407 | }
2408 |
> 2409 | console.error(`${provider.name} API Call: All configured attempts failed.`);
| ^
2410 | throw lastError || new Error(`${provider.name} API call failed after multiple retries.`);
2411 | }
2412 |
at src/llmUtils.ts:2409:13
at Generator.throw (<anonymous>)
at rejected (node_modules/tslib/tslib.js:116:69)
console.warn
Ollama API Call: Attempt 1 failed: Ollama API request failed: socket hang up
2349 | lastError = error instanceof Error ? error : new Error(errorMessage); // Store Error object if possible
2350 | progressReporter.log(`${provider.name} API Call: Attempt ${attempt} failed: ${errorMessage}`);
> 2351 | console.warn(`${provider.name} API Call: Attempt ${attempt} failed: ${errorMessage}`);
| ^
2352 |
2353 | // Handle cancellation specifically
2354 | if ((error instanceof Error && error.name === 'AbortError') || errorMessage.includes("cancelled by user")) {
at src/llmUtils.ts:2351:21
at Generator.throw (<anonymous>)
at rejected (node_modules/tslib/tslib.js:116:69)
console.error
Ollama API Call: All configured attempts failed.
2407 | }
2408 |
> 2409 | console.error(`${provider.name} API Call: All configured attempts failed.`);
| ^
2410 | throw lastError || new Error(`${provider.name} API call failed after multiple retries.`);
2411 | }
2412 |
at src/llmUtils.ts:2409:13
at Generator.throw (<anonymous>)
at rejected (node_modules/tslib/tslib.js:116:69)
PASS src/tests/sidebarDomButtonClicks.test.ts
● Console
console.log
Potential duplicates in Active.txt: [ 'duplicate' ]
at NotemdSidebarView.<anonymous> (src/ui/NotemdSidebarView.ts:566:29)
PASS src/tests/providerDiagnostics.test.ts
PASS src/tests/workflowButtons.test.ts
Test Suites: 4 passed, 4 total
Tests: 80 passed, 80 total
Snapshots: 0 total
Time: 0.384 s, estimated 1 s
Ran all test suites matching /src\/tests\/workflowButtons.test.ts|src\/tests\/sidebarDomButtonClicks.test.ts|src\/tests\/llmUtilsProviderSupport.test.ts|src\/tests\/providerDiagnostics.test.ts/i.

View file

@ -0,0 +1,19 @@
> notemd@1.7.14 test
> jest --runInBand src/tests/providerDiagnostics.test.ts src/tests/sidebarDomButtonClicks.test.ts
PASS src/tests/sidebarDomButtonClicks.test.ts
● Console
console.log
Potential duplicates in Active.txt: [ 'duplicate' ]
at NotemdSidebarView.<anonymous> (src/ui/NotemdSidebarView.ts:574:29)
PASS src/tests/providerDiagnostics.test.ts
Test Suites: 2 passed, 2 total
Tests: 14 passed, 14 total
Snapshots: 0 total
Time: 0.326 s, estimated 1 s
Ran all test suites matching /src\/tests\/providerDiagnostics.test.ts|src\/tests\/sidebarDomButtonClicks.test.ts/i.

View file

@ -0,0 +1,293 @@
> notemd@1.7.14 test
> jest --runInBand src/tests/workflowButtons.test.ts src/tests/sidebarDomButtonClicks.test.ts src/tests/llmUtilsProviderSupport.test.ts src/tests/providerDiagnostics.test.ts
PASS src/tests/llmUtilsProviderSupport.test.ts
● Console
console.warn
OpenAI API Call: Attempt 1 failed: OpenAI API request failed: net::ERR_CONNECTION_CLOSED
2349 | lastError = error instanceof Error ? error : new Error(errorMessage); // Store Error object if possible
2350 | progressReporter.log(`${provider.name} API Call: Attempt ${attempt} failed: ${errorMessage}`);
> 2351 | console.warn(`${provider.name} API Call: Attempt ${attempt} failed: ${errorMessage}`);
| ^
2352 |
2353 | // Handle cancellation specifically
2354 | if ((error instanceof Error && error.name === 'AbortError') || errorMessage.includes("cancelled by user")) {
at src/llmUtils.ts:2351:21
at Generator.throw (<anonymous>)
at rejected (node_modules/tslib/tslib.js:116:69)
console.warn
OpenAI API Call: Attempt 1 failed: OpenAI API request failed: net::ERR_CONNECTION_CLOSED
2349 | lastError = error instanceof Error ? error : new Error(errorMessage); // Store Error object if possible
2350 | progressReporter.log(`${provider.name} API Call: Attempt ${attempt} failed: ${errorMessage}`);
> 2351 | console.warn(`${provider.name} API Call: Attempt ${attempt} failed: ${errorMessage}`);
| ^
2352 |
2353 | // Handle cancellation specifically
2354 | if ((error instanceof Error && error.name === 'AbortError') || errorMessage.includes("cancelled by user")) {
at src/llmUtils.ts:2351:21
at Generator.throw (<anonymous>)
at rejected (node_modules/tslib/tslib.js:116:69)
console.warn
OpenAI API Call: Attempt 2 failed: OpenAI API request failed: net::ERR_CONNECTION_CLOSED
2349 | lastError = error instanceof Error ? error : new Error(errorMessage); // Store Error object if possible
2350 | progressReporter.log(`${provider.name} API Call: Attempt ${attempt} failed: ${errorMessage}`);
> 2351 | console.warn(`${provider.name} API Call: Attempt ${attempt} failed: ${errorMessage}`);
| ^
2352 |
2353 | // Handle cancellation specifically
2354 | if ((error instanceof Error && error.name === 'AbortError') || errorMessage.includes("cancelled by user")) {
at src/llmUtils.ts:2351:21
at Generator.throw (<anonymous>)
at rejected (node_modules/tslib/tslib.js:116:69)
console.warn
Anthropic API Call: Attempt 1 failed: Anthropic API request failed: net::ERR_CONNECTION_CLOSED
2349 | lastError = error instanceof Error ? error : new Error(errorMessage); // Store Error object if possible
2350 | progressReporter.log(`${provider.name} API Call: Attempt ${attempt} failed: ${errorMessage}`);
> 2351 | console.warn(`${provider.name} API Call: Attempt ${attempt} failed: ${errorMessage}`);
| ^
2352 |
2353 | // Handle cancellation specifically
2354 | if ((error instanceof Error && error.name === 'AbortError') || errorMessage.includes("cancelled by user")) {
at src/llmUtils.ts:2351:21
at Generator.throw (<anonymous>)
at rejected (node_modules/tslib/tslib.js:116:69)
console.warn
Google API Call: Attempt 1 failed: Google API request failed: net::ERR_CONNECTION_CLOSED
2349 | lastError = error instanceof Error ? error : new Error(errorMessage); // Store Error object if possible
2350 | progressReporter.log(`${provider.name} API Call: Attempt ${attempt} failed: ${errorMessage}`);
> 2351 | console.warn(`${provider.name} API Call: Attempt ${attempt} failed: ${errorMessage}`);
| ^
2352 |
2353 | // Handle cancellation specifically
2354 | if ((error instanceof Error && error.name === 'AbortError') || errorMessage.includes("cancelled by user")) {
at src/llmUtils.ts:2351:21
at Generator.throw (<anonymous>)
at rejected (node_modules/tslib/tslib.js:116:69)
console.warn
Azure OpenAI API Call: Attempt 1 failed: Azure OpenAI API request failed: net::ERR_CONNECTION_CLOSED
2349 | lastError = error instanceof Error ? error : new Error(errorMessage); // Store Error object if possible
2350 | progressReporter.log(`${provider.name} API Call: Attempt ${attempt} failed: ${errorMessage}`);
> 2351 | console.warn(`${provider.name} API Call: Attempt ${attempt} failed: ${errorMessage}`);
| ^
2352 |
2353 | // Handle cancellation specifically
2354 | if ((error instanceof Error && error.name === 'AbortError') || errorMessage.includes("cancelled by user")) {
at src/llmUtils.ts:2351:21
at Generator.throw (<anonymous>)
at rejected (node_modules/tslib/tslib.js:116:69)
console.warn
Ollama API Call: Attempt 1 failed: Ollama API request failed: net::ERR_CONNECTION_CLOSED
2349 | lastError = error instanceof Error ? error : new Error(errorMessage); // Store Error object if possible
2350 | progressReporter.log(`${provider.name} API Call: Attempt ${attempt} failed: ${errorMessage}`);
> 2351 | console.warn(`${provider.name} API Call: Attempt ${attempt} failed: ${errorMessage}`);
| ^
2352 |
2353 | // Handle cancellation specifically
2354 | if ((error instanceof Error && error.name === 'AbortError') || errorMessage.includes("cancelled by user")) {
at src/llmUtils.ts:2351:21
at Generator.throw (<anonymous>)
at rejected (node_modules/tslib/tslib.js:116:69)
console.warn
OpenAI API Call: Attempt 1 failed: OpenAI API error: 400 - Bad request
2349 | lastError = error instanceof Error ? error : new Error(errorMessage); // Store Error object if possible
2350 | progressReporter.log(`${provider.name} API Call: Attempt ${attempt} failed: ${errorMessage}`);
> 2351 | console.warn(`${provider.name} API Call: Attempt ${attempt} failed: ${errorMessage}`);
| ^
2352 |
2353 | // Handle cancellation specifically
2354 | if ((error instanceof Error && error.name === 'AbortError') || errorMessage.includes("cancelled by user")) {
at src/llmUtils.ts:2351:21
at Generator.throw (<anonymous>)
at rejected (node_modules/tslib/tslib.js:116:69)
console.warn
OpenAI API Call: Attempt 1 failed: OpenAI API request failed: socket hang up
2349 | lastError = error instanceof Error ? error : new Error(errorMessage); // Store Error object if possible
2350 | progressReporter.log(`${provider.name} API Call: Attempt ${attempt} failed: ${errorMessage}`);
> 2351 | console.warn(`${provider.name} API Call: Attempt ${attempt} failed: ${errorMessage}`);
| ^
2352 |
2353 | // Handle cancellation specifically
2354 | if ((error instanceof Error && error.name === 'AbortError') || errorMessage.includes("cancelled by user")) {
at src/llmUtils.ts:2351:21
at Generator.throw (<anonymous>)
at rejected (node_modules/tslib/tslib.js:116:69)
console.error
OpenAI API Call: All configured attempts failed.
2407 | }
2408 |
> 2409 | console.error(`${provider.name} API Call: All configured attempts failed.`);
| ^
2410 | throw lastError || new Error(`${provider.name} API call failed after multiple retries.`);
2411 | }
2412 |
at src/llmUtils.ts:2409:13
at Generator.throw (<anonymous>)
at rejected (node_modules/tslib/tslib.js:116:69)
console.warn
OpenAI API Call: Attempt 1 failed: OpenAI API request failed: The operation was aborted.
2349 | lastError = error instanceof Error ? error : new Error(errorMessage); // Store Error object if possible
2350 | progressReporter.log(`${provider.name} API Call: Attempt ${attempt} failed: ${errorMessage}`);
> 2351 | console.warn(`${provider.name} API Call: Attempt ${attempt} failed: ${errorMessage}`);
| ^
2352 |
2353 | // Handle cancellation specifically
2354 | if ((error instanceof Error && error.name === 'AbortError') || errorMessage.includes("cancelled by user")) {
at src/llmUtils.ts:2351:21
at Generator.throw (<anonymous>)
at rejected (node_modules/tslib/tslib.js:116:69)
console.error
OpenAI API Call: All configured attempts failed.
2407 | }
2408 |
> 2409 | console.error(`${provider.name} API Call: All configured attempts failed.`);
| ^
2410 | throw lastError || new Error(`${provider.name} API call failed after multiple retries.`);
2411 | }
2412 |
at src/llmUtils.ts:2409:13
at Generator.throw (<anonymous>)
at rejected (node_modules/tslib/tslib.js:116:69)
console.warn
OpenAI API Call: Attempt 1 failed: OpenAI API request failed: socket hang up
2349 | lastError = error instanceof Error ? error : new Error(errorMessage); // Store Error object if possible
2350 | progressReporter.log(`${provider.name} API Call: Attempt ${attempt} failed: ${errorMessage}`);
> 2351 | console.warn(`${provider.name} API Call: Attempt ${attempt} failed: ${errorMessage}`);
| ^
2352 |
2353 | // Handle cancellation specifically
2354 | if ((error instanceof Error && error.name === 'AbortError') || errorMessage.includes("cancelled by user")) {
at src/llmUtils.ts:2351:21
at Generator.throw (<anonymous>)
at rejected (node_modules/tslib/tslib.js:116:69)
console.error
OpenAI API Call: All configured attempts failed.
2407 | }
2408 |
> 2409 | console.error(`${provider.name} API Call: All configured attempts failed.`);
| ^
2410 | throw lastError || new Error(`${provider.name} API call failed after multiple retries.`);
2411 | }
2412 |
at src/llmUtils.ts:2409:13
at Generator.throw (<anonymous>)
at rejected (node_modules/tslib/tslib.js:116:69)
console.warn
Anthropic API Call: Attempt 1 failed: Anthropic API request failed: socket hang up
2349 | lastError = error instanceof Error ? error : new Error(errorMessage); // Store Error object if possible
2350 | progressReporter.log(`${provider.name} API Call: Attempt ${attempt} failed: ${errorMessage}`);
> 2351 | console.warn(`${provider.name} API Call: Attempt ${attempt} failed: ${errorMessage}`);
| ^
2352 |
2353 | // Handle cancellation specifically
2354 | if ((error instanceof Error && error.name === 'AbortError') || errorMessage.includes("cancelled by user")) {
at src/llmUtils.ts:2351:21
at Generator.throw (<anonymous>)
at rejected (node_modules/tslib/tslib.js:116:69)
console.error
Anthropic API Call: All configured attempts failed.
2407 | }
2408 |
> 2409 | console.error(`${provider.name} API Call: All configured attempts failed.`);
| ^
2410 | throw lastError || new Error(`${provider.name} API call failed after multiple retries.`);
2411 | }
2412 |
at src/llmUtils.ts:2409:13
at Generator.throw (<anonymous>)
at rejected (node_modules/tslib/tslib.js:116:69)
console.warn
Ollama API Call: Attempt 1 failed: Ollama API request failed: socket hang up
2349 | lastError = error instanceof Error ? error : new Error(errorMessage); // Store Error object if possible
2350 | progressReporter.log(`${provider.name} API Call: Attempt ${attempt} failed: ${errorMessage}`);
> 2351 | console.warn(`${provider.name} API Call: Attempt ${attempt} failed: ${errorMessage}`);
| ^
2352 |
2353 | // Handle cancellation specifically
2354 | if ((error instanceof Error && error.name === 'AbortError') || errorMessage.includes("cancelled by user")) {
at src/llmUtils.ts:2351:21
at Generator.throw (<anonymous>)
at rejected (node_modules/tslib/tslib.js:116:69)
console.error
Ollama API Call: All configured attempts failed.
2407 | }
2408 |
> 2409 | console.error(`${provider.name} API Call: All configured attempts failed.`);
| ^
2410 | throw lastError || new Error(`${provider.name} API call failed after multiple retries.`);
2411 | }
2412 |
at src/llmUtils.ts:2409:13
at Generator.throw (<anonymous>)
at rejected (node_modules/tslib/tslib.js:116:69)
PASS src/tests/sidebarDomButtonClicks.test.ts
● Console
console.log
Potential duplicates in Active.txt: [ 'duplicate' ]
at NotemdSidebarView.<anonymous> (src/ui/NotemdSidebarView.ts:566:29)
PASS src/tests/providerDiagnostics.test.ts
PASS src/tests/workflowButtons.test.ts
Test Suites: 4 passed, 4 total
Tests: 80 passed, 80 total
Snapshots: 0 total
Time: 0.384 s, estimated 1 s
Ran all test suites matching /src\/tests\/workflowButtons.test.ts|src\/tests\/sidebarDomButtonClicks.test.ts|src\/tests\/llmUtilsProviderSupport.test.ts|src\/tests\/providerDiagnostics.test.ts/i.

View file

@ -0,0 +1,26 @@
> notemd@1.7.14 test
> jest --runInBand src/tests/sidebarDomButtonClicks.test.ts src/tests/sidebarButtonTriggerChains.test.ts
PASS src/tests/sidebarButtonTriggerChains.test.ts
● Console
console.log
Potential duplicates in Current.md: [ 'duplicate' ]
at NotemdSidebarView.<anonymous> (src/ui/NotemdSidebarView.ts:574:29)
PASS src/tests/sidebarDomButtonClicks.test.ts
● Console
console.log
Potential duplicates in Active.txt: [ 'duplicate' ]
at NotemdSidebarView.<anonymous> (src/ui/NotemdSidebarView.ts:574:29)
Test Suites: 2 passed, 2 total
Tests: 23 passed, 23 total
Snapshots: 0 total
Time: 0.293 s, estimated 1 s
Ran all test suites matching /src\/tests\/sidebarDomButtonClicks.test.ts|src\/tests\/sidebarButtonTriggerChains.test.ts/i.

View file

@ -0,0 +1,27 @@
> notemd@1.7.14 test
> jest --runInBand src/tests/providerDiagnostics.test.ts src/tests/sidebarDomButtonClicks.test.ts src/tests/sidebarButtonTriggerChains.test.ts
PASS src/tests/sidebarButtonTriggerChains.test.ts
● Console
console.log
Potential duplicates in Current.md: [ 'duplicate' ]
at NotemdSidebarView.<anonymous> (src/ui/NotemdSidebarView.ts:574:29)
PASS src/tests/sidebarDomButtonClicks.test.ts
● Console
console.log
Potential duplicates in Active.txt: [ 'duplicate' ]
at NotemdSidebarView.<anonymous> (src/ui/NotemdSidebarView.ts:574:29)
PASS src/tests/providerDiagnostics.test.ts
Test Suites: 3 passed, 3 total
Tests: 32 passed, 32 total
Snapshots: 0 total
Time: 0.423 s, estimated 1 s
Ran all test suites matching /src\/tests\/providerDiagnostics.test.ts|src\/tests\/sidebarDomButtonClicks.test.ts|src\/tests\/sidebarButtonTriggerChains.test.ts/i.

View file

@ -0,0 +1,13 @@
> notemd@1.7.14 test
> jest --runInBand src/tests/languagePolicy.test.ts src/tests/processFile.test.ts src/tests/workflowButtons.test.ts
PASS src/tests/processFile.test.ts
PASS src/tests/languagePolicy.test.ts
PASS src/tests/workflowButtons.test.ts
Test Suites: 3 passed, 3 total
Tests: 17 passed, 17 total
Snapshots: 0 total
Time: 0.374 s, estimated 1 s
Ran all test suites matching /src\/tests\/languagePolicy.test.ts|src\/tests\/processFile.test.ts|src\/tests\/workflowButtons.test.ts/i.

View file

@ -0,0 +1,4 @@
> notemd@1.7.14 build
> tsc -noEmit -skipLibCheck && node esbuild.config.mjs production

View file

@ -0,0 +1,4 @@
> notemd@1.7.14 test
> jest --runInBand

View file

@ -0,0 +1,2 @@
Ignored: Error: ENOENT: no such file or directory, open '/root/.config/obsidian/obsidian.json'
2026-04-09 11:15:42 Loaded main app package /opt/obsidian-desktop/1.12.7/resources/obsidian.asar

View file

@ -0,0 +1,7 @@
> notemd@1.7.14 regression:language-baseline
> bash scripts/regression/language-support-baseline.sh
[baseline] writing into docs/superpowers/baselines/2026-04-09-language-support
[baseline] done
BASE_DIR=docs/superpowers/baselines/2026-04-09-language-support

View file

@ -0,0 +1,6 @@
> notemd@1.7.14 regression:language-compare
> bash scripts/regression/language-support-compare.sh
[compare] using baseline docs/superpowers/baselines/2026-04-09-language-support
[compare] PASS: build + targeted regressions are stable

View file

@ -0,0 +1,4 @@
> notemd@1.7.14 test
> jest --runInBand src/tests/workflowButtons.test.ts src/tests/sidebarDomButtonClicks.test.ts src/tests/llmUtilsProviderSupport.test.ts src/tests/providerDiagnostics.test.ts src/tests/languagePolicy.test.ts src/tests/i18nFallback.test.ts src/tests/localeFormat.test.ts

View file

@ -0,0 +1,25 @@
# Regression Baselines
This directory stores reproducible before/after evidence for high-risk changes.
## Standard workflow
1. Capture baseline:
```bash
npm run regression:language-baseline
```
2. Implement changes.
3. Compare against latest baseline:
```bash
npm run regression:language-compare
```
4. If compare fails, fix regressions before merging.
## Conventions
- Use dated folders: `YYYY-MM-DD-<topic>`.
- Keep both command output and exit codes when relevant.
- Never overwrite historical baseline logs; append new files or create a new dated folder.

View file

@ -3,6 +3,7 @@ module.exports = {
testEnvironment: 'node',
moduleFileExtensions: ['ts', 'js', 'json'],
testMatch: ['**/tests/**/*.test.ts'],
testPathIgnorePatterns: ['/node_modules/', '/ref/'],
moduleNameMapper: {
'^obsidian$': '<rootDir>/src/__mocks__/obsidian.ts',
'^mermaid$': '<rootDir>/src/__mocks__/mermaid.ts'

View file

@ -9,7 +9,9 @@
"version": "node version-bump.mjs && git add manifest.json versions.json",
"test": "jest",
"lint": "eslint . --ext .ts",
"diagnose:llm": "node scripts/diagnose-llm-provider.js"
"diagnose:llm": "node scripts/diagnose-llm-provider.js",
"regression:language-baseline": "bash scripts/regression/language-support-baseline.sh",
"regression:language-compare": "bash scripts/regression/language-support-compare.sh"
},
"keywords": [
"obsidian",

View file

@ -0,0 +1,42 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "$0")/../.." && pwd)"
cd "$ROOT_DIR"
STAMP="${1:-$(date +%F)}"
BASE_DIR="docs/superpowers/baselines/${STAMP}-language-support"
mkdir -p "$BASE_DIR"
echo "[baseline] writing into $BASE_DIR"
{
echo "=== DATE ==="
date -Is
echo
echo "=== NODE/NPM ==="
node -v
npm -v
echo
echo "=== OBSIDIAN CLI ==="
obsidian help || true
echo
obsidian-cli help || true
} > "$BASE_DIR/environment-baseline.txt"
set +e
npm run build > "$BASE_DIR/build-baseline.txt" 2>&1
BUILD_EXIT=$?
set -e
echo "$BUILD_EXIT" > "$BASE_DIR/build-baseline.exitcode"
npm test -- --runInBand \
src/tests/workflowButtons.test.ts \
src/tests/sidebarDomButtonClicks.test.ts \
src/tests/llmUtilsProviderSupport.test.ts \
src/tests/providerDiagnostics.test.ts \
> "$BASE_DIR/targeted-tests-baseline.txt" 2>&1
echo "[baseline] done"
echo "BASE_DIR=$BASE_DIR"

View file

@ -0,0 +1,54 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "$0")/../.." && pwd)"
cd "$ROOT_DIR"
BASE_DIR="${1:-}"
if [[ -z "$BASE_DIR" ]]; then
BASE_DIR="$(ls -td docs/superpowers/baselines/*-language-support 2>/dev/null | head -n1 || true)"
fi
if [[ -z "$BASE_DIR" || ! -d "$BASE_DIR" ]]; then
echo "[compare] baseline directory not found"
echo "usage: bash scripts/regression/language-support-compare.sh <baseline-dir>"
exit 1
fi
echo "[compare] using baseline $BASE_DIR"
CURRENT_BUILD="$BASE_DIR/build-compare-current.txt"
CURRENT_TEST="$BASE_DIR/targeted-tests-compare-current.txt"
set +e
npm run build > "$CURRENT_BUILD" 2>&1
CURRENT_BUILD_EXIT=$?
set -e
echo "$CURRENT_BUILD_EXIT" > "$BASE_DIR/build-compare-current.exitcode"
npm test -- --runInBand \
src/tests/workflowButtons.test.ts \
src/tests/sidebarDomButtonClicks.test.ts \
src/tests/llmUtilsProviderSupport.test.ts \
src/tests/providerDiagnostics.test.ts \
> "$CURRENT_TEST" 2>&1
if grep -q "error TS6059" "$CURRENT_BUILD"; then
echo "[compare] FAIL: TS6059 still present in current build output"
exit 1
fi
if grep -E "Test Suites:\s*[1-9][0-9]* failed|\bFAIL\b" "$CURRENT_TEST" >/dev/null; then
echo "[compare] FAIL: targeted regression tests reported failures"
exit 1
fi
if grep -E "ERR_CONNECTION_CLOSED|socket hang up" "$CURRENT_TEST" >/dev/null; then
if ! grep -q "llmUtilsProviderSupport.test.ts" "$CURRENT_TEST"; then
echo "[compare] FAIL: network-close signatures found outside expected mock test context"
exit 1
fi
fi
echo "[compare] PASS: build + targeted regressions are stable"

View file

@ -6,6 +6,7 @@ export const Notice = jest.fn();
export const Plugin = jest.fn();
export const PluginSettingTab = jest.fn();
export const requestUrl = jest.fn();
export const getLanguage = jest.fn(() => 'en');
export const Setting = jest.fn().mockImplementation(() => ({
setName: jest.fn().mockReturnThis(),
setDesc: jest.fn().mockReturnThis(),

View file

@ -78,6 +78,7 @@ export const DEFAULT_SETTINGS: NotemdSettings = {
// Add Links Post-Processing Defaults
removeCodeFencesOnAddLinks: false, // Default to NOT removing code fences
// Language Settings Defaults
uiLocale: 'auto',
language: 'en', // Default to English
availableLanguages: [ // Default list of available languages
{ code: 'en', name: 'English' },

View file

@ -9,6 +9,7 @@ import { refineMermaidBlocks, cleanupLatexDelimiters, deepDebugMermaid, applyDee
import { _performResearch } from './searchUtils'; // Assuming this will be moved or imported correctly later
import { showDeletionConfirmationModal } from './ui/modals'; // Assuming this will be moved or imported correctly later
import mermaid from 'mermaid';
import { resolveTaskLanguageName, shouldApplyAutoTranslation } from './i18n/taskLanguagePolicy';
// --- Backlink and Note Management ---
@ -639,8 +640,8 @@ export async function generateContentForTitle(app: App, settings: NotemdSettings
);
// Language-specific instruction
const targetLanguageName = settings.availableLanguages.find(lang => lang.code === settings.language)?.name || settings.language;
if (settings.language && settings.language !== 'en') {
const targetLanguageName = resolveTaskLanguageName(settings, 'generateTitle');
if (shouldApplyAutoTranslation(settings, 'generateTitle') && targetLanguageName && targetLanguageName !== 'English') {
generationPrompt += `\n\nIMPORTANT: Process the request and perform all reasoning in English. However, the final output MUST be written in ${targetLanguageName}.In mermaid diagrams, it is necessary to translate into ${targetLanguageName} while retaining the English.`;
}

86
src/i18n/index.ts Normal file
View file

@ -0,0 +1,86 @@
import { getLanguage } from 'obsidian';
import { normalizeLocaleCode, resolveUiLocale } from './languageContext';
import { STRINGS_EN, NotemdEnglishStrings } from './locales/en';
import { STRINGS_ZH_CN } from './locales/zh_cn';
import { STRINGS_ZH_TW } from './locales/zh_tw';
type TranslationStrings = NotemdEnglishStrings;
type DeepPartial<T> = T extends object ? { [K in keyof T]?: DeepPartial<T[K]> } : T;
const LANGUAGE_MAP: Record<string, DeepPartial<TranslationStrings>> = {
en: STRINGS_EN,
zh: STRINGS_ZH_CN,
'zh-CN': STRINGS_ZH_CN,
'zh-TW': STRINGS_ZH_TW
};
const resolvedLanguageCache = new Map<string, TranslationStrings>();
function isPlainObject(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value);
}
function mergeTranslationValues(base: unknown, override: unknown): unknown {
if (override === undefined) {
return base;
}
if (Array.isArray(base)) {
return Array.isArray(override) ? override : base;
}
if (isPlainObject(base)) {
if (!isPlainObject(override)) {
return base;
}
const result: Record<string, unknown> = {};
for (const key of Object.keys(base)) {
result[key] = mergeTranslationValues(base[key], override[key]);
}
return result;
}
return typeof override === typeof base ? override : base;
}
export function getResolvedStrings(locale: string): TranslationStrings {
const normalizedLocale = normalizeLocaleCode(locale);
if (normalizedLocale === 'en') {
return STRINGS_EN;
}
const cached = resolvedLanguageCache.get(normalizedLocale);
if (cached) {
return cached;
}
const merged = mergeTranslationValues(STRINGS_EN, LANGUAGE_MAP[normalizedLocale] || LANGUAGE_MAP.en) as TranslationStrings;
resolvedLanguageCache.set(normalizedLocale, merged);
return merged;
}
export function clearI18nCache(): void {
resolvedLanguageCache.clear();
}
export function getCurrentObsidianLocale(): string {
return normalizeLocaleCode(getLanguage());
}
export function getI18nStrings(settings?: { uiLocale: string }): TranslationStrings {
if (settings) {
const locale = resolveUiLocale(settings, getCurrentObsidianLocale(), Object.keys(LANGUAGE_MAP));
return getResolvedStrings(locale);
}
return getResolvedStrings(getCurrentObsidianLocale());
}
export function formatI18n(template: string, variables: Record<string, string | number>): string {
return template.replace(/{(\w+)}/g, (_, key: string) => {
const value = variables[key];
return value === undefined ? `{${key}}` : String(value);
});
}
export const strings: TranslationStrings = getI18nStrings();

View file

@ -0,0 +1,63 @@
import { NotemdSettings } from '../types';
export const UI_LOCALE_AUTO = 'auto';
function normalizeChineseLocale(locale: string): string {
const lower = locale.toLowerCase();
if (lower === 'zh' || lower === 'zh-cn' || lower === 'zh_cn') {
return 'zh-CN';
}
if (lower === 'zh-tw' || lower === 'zh_tw' || lower === 'zh-hant') {
return 'zh-TW';
}
return locale;
}
export function normalizeLocaleCode(locale: string | undefined | null): string {
if (!locale) {
return 'en';
}
const trimmed = locale.trim();
if (!trimmed) {
return 'en';
}
const unified = normalizeChineseLocale(trimmed.replace(/_/g, '-'));
const parts = unified.split('-');
if (parts.length === 1) {
return parts[0].toLowerCase();
}
return `${parts[0].toLowerCase()}-${parts[1].toUpperCase()}`;
}
export function languageCodesEqual(left: string | undefined | null, right: string | undefined | null): boolean {
return normalizeLocaleCode(left) === normalizeLocaleCode(right);
}
export function resolveLanguageDisplayName(settings: NotemdSettings, languageCode: string): string {
const normalizedTarget = normalizeLocaleCode(languageCode);
const language = settings.availableLanguages.find(lang => languageCodesEqual(lang.code, normalizedTarget));
return language?.name || normalizedTarget;
}
export function resolveUiLocale(
settings: { uiLocale: string },
obsidianLocale: string | undefined | null,
supportedLocales: readonly string[] = ['en', 'zh-CN', 'zh-TW']
): string {
const normalizedSupported = supportedLocales.map(normalizeLocaleCode);
const configured = normalizeLocaleCode(settings.uiLocale);
if (configured !== UI_LOCALE_AUTO && normalizedSupported.includes(configured)) {
return configured;
}
const detected = normalizeLocaleCode(obsidianLocale);
if (normalizedSupported.includes(detected)) {
return detected;
}
return 'en';
}

27
src/i18n/localeFormat.ts Normal file
View file

@ -0,0 +1,27 @@
import { normalizeLocaleCode } from './languageContext';
const RTL_LANGUAGE_PREFIXES = ['ar', 'he', 'fa', 'ur'];
export function isRtlLocale(locale: string | undefined | null): boolean {
const normalized = normalizeLocaleCode(locale);
return RTL_LANGUAGE_PREFIXES.some(prefix => normalized.startsWith(prefix));
}
export function formatTimeForLocale(date: Date, locale: string | undefined | null): string {
const normalized = normalizeLocaleCode(locale);
try {
return date.toLocaleTimeString(normalized);
} catch (_error) {
return date.toLocaleTimeString();
}
}
export function formatDateForLocale(date: Date, locale: string | undefined | null): string {
const normalized = normalizeLocaleCode(locale);
try {
return date.toLocaleDateString(normalized);
} catch (_error) {
return date.toLocaleDateString();
}
}

60
src/i18n/locales/en.ts Normal file
View file

@ -0,0 +1,60 @@
export const STRINGS_EN = {
common: {
language: 'Language',
cancel: 'Cancel',
close: 'Close',
copy: 'Copy',
ready: 'Ready',
standby: 'Standby',
unknownError: 'Unknown error'
},
settings: {
language: {
heading: 'Language settings',
outputName: 'Output language',
outputDesc: 'Select the desired output language for LLM responses.',
perTaskName: 'Select different languages for different tasks.',
perTaskDesc: 'On: Select a specific language for each task below. Off: Use the single "Output language".',
disableAutoTranslationName: 'Disable auto translation (except for "Translate" task)',
disableAutoTranslationDesc:
'On: Non-Translate tasks do not force a target language or auto-translate outputs. The explicit "Translate" task still performs translation as configured.',
taskLanguageLabel: '{task} language',
taskLanguageDesc: 'Select the output language for "{task}".'
},
developer: {
modeName: 'Developer mode',
modeDesc: 'On: Show dedicated developer diagnostic tools in settings. Off: Hide developer-only controls.',
heading: 'Developer diagnostics',
runDiagnostic: 'Run diagnostic',
runStability: 'Run stability test'
}
},
sidebar: {
heroTitle: 'Notemd Workbench',
heroDesc: 'Run single actions or custom one-click workflows with live progress and logs.',
quickWorkflowTitle: 'Quick Workflows',
quickWorkflowDesc: 'Custom buttons assembled from built-in actions.',
builtInActionsPrefix: 'Built-in {category} actions.',
logOutputTitle: 'Log output',
copyLog: 'Copy log',
copyLogSuccess: 'Log copied!',
copyLogFailed: 'Failed to copy log.',
logEmpty: 'Log is empty.',
cancelProcessing: 'Cancel processing',
workflowFallbackWarning: 'Workflow DSL has {count} issue(s). Sidebar is using default fallback.',
languageChangedNotice: 'Language changed to {language}'
},
notices: {
processingAlreadyRunning: 'Processing already in progress.',
anotherProcessRunning: 'Another process is running. Please wait.',
notemdBusy: 'Notemd is busy.'
},
errorModal: {
copyDetails: 'Copy Error Details',
copied: 'Copied!',
copySuccessNotice: 'Error details copied to clipboard!',
copyFailedNotice: 'Failed to copy error details. See console.'
}
};
export type NotemdEnglishStrings = typeof STRINGS_EN;

59
src/i18n/locales/zh_cn.ts Normal file
View file

@ -0,0 +1,59 @@
import type { NotemdEnglishStrings } from './en';
export const STRINGS_ZH_CN: Partial<NotemdEnglishStrings> = {
common: {
language: '语言',
cancel: '取消',
close: '关闭',
copy: '复制',
ready: '就绪',
standby: '待命',
unknownError: '未知错误'
},
settings: {
language: {
heading: '语言设置',
outputName: '输出语言',
outputDesc: '选择 LLM 输出内容使用的语言。',
perTaskName: '为不同任务选择不同语言',
perTaskDesc: '开:为每个任务分别设置语言。关:统一使用“输出语言”。',
disableAutoTranslationName: '关闭自动翻译“Translate”任务除外',
disableAutoTranslationDesc: '开:非 Translate 任务不强制翻译到目标语言。Translate 任务仍按配置执行翻译。',
taskLanguageLabel: '{task} 语言',
taskLanguageDesc: '为“{task}”选择输出语言。'
},
developer: {
modeName: '开发者模式',
modeDesc: '开:显示开发者诊断工具。关:隐藏开发者专用控件。',
heading: '开发者诊断',
runDiagnostic: '运行诊断',
runStability: '运行稳定性测试'
}
},
sidebar: {
heroTitle: 'Notemd 工作台',
heroDesc: '执行单项任务或自定义一键工作流,并查看实时进度与日志。',
quickWorkflowTitle: '快速工作流',
quickWorkflowDesc: '由内置动作组装的自定义按钮。',
builtInActionsPrefix: '内置{category}动作。',
logOutputTitle: '日志输出',
copyLog: '复制日志',
copyLogSuccess: '日志已复制!',
copyLogFailed: '复制日志失败。',
logEmpty: '日志为空。',
cancelProcessing: '取消处理',
workflowFallbackWarning: '工作流 DSL 有 {count} 个问题,侧边栏已使用默认回退配置。',
languageChangedNotice: '语言已切换为 {language}'
},
notices: {
processingAlreadyRunning: '已有任务正在处理中。',
anotherProcessRunning: '另一个任务正在运行,请稍候。',
notemdBusy: 'Notemd 正在忙碌中。'
},
errorModal: {
copyDetails: '复制错误详情',
copied: '已复制!',
copySuccessNotice: '错误详情已复制到剪贴板!',
copyFailedNotice: '复制错误详情失败,请查看控制台。'
}
};

59
src/i18n/locales/zh_tw.ts Normal file
View file

@ -0,0 +1,59 @@
import type { NotemdEnglishStrings } from './en';
export const STRINGS_ZH_TW: Partial<NotemdEnglishStrings> = {
common: {
language: '語言',
cancel: '取消',
close: '關閉',
copy: '複製',
ready: '就緒',
standby: '待命',
unknownError: '未知錯誤'
},
settings: {
language: {
heading: '語言設定',
outputName: '輸出語言',
outputDesc: '選擇 LLM 輸出內容使用的語言。',
perTaskName: '為不同任務選擇不同語言',
perTaskDesc: '開:可為每個任務設定語言。關:統一使用「輸出語言」。',
disableAutoTranslationName: '停用自動翻譯「Translate」任務除外',
disableAutoTranslationDesc: '開:非 Translate 任務不強制翻譯到目標語言。Translate 任務仍依設定執行。',
taskLanguageLabel: '{task} 語言',
taskLanguageDesc: '為「{task}」選擇輸出語言。'
},
developer: {
modeName: '開發者模式',
modeDesc: '開:顯示開發者診斷工具。關:隱藏開發者專用控制項。',
heading: '開發者診斷',
runDiagnostic: '執行診斷',
runStability: '執行穩定性測試'
}
},
sidebar: {
heroTitle: 'Notemd 工作台',
heroDesc: '執行單一動作或自訂一鍵工作流程,並查看即時進度與日誌。',
quickWorkflowTitle: '快速工作流程',
quickWorkflowDesc: '由內建動作組裝的自訂按鈕。',
builtInActionsPrefix: '內建 {category} 動作。',
logOutputTitle: '日誌輸出',
copyLog: '複製日誌',
copyLogSuccess: '日誌已複製!',
copyLogFailed: '複製日誌失敗。',
logEmpty: '日誌為空。',
cancelProcessing: '取消處理',
workflowFallbackWarning: '工作流程 DSL 有 {count} 個問題,側欄已使用預設回退設定。',
languageChangedNotice: '語言已切換為 {language}'
},
notices: {
processingAlreadyRunning: '已有任務正在處理中。',
anotherProcessRunning: '另一個任務正在執行,請稍候。',
notemdBusy: 'Notemd 忙碌中。'
},
errorModal: {
copyDetails: '複製錯誤詳情',
copied: '已複製!',
copySuccessNotice: '錯誤詳情已複製到剪貼簿!',
copyFailedNotice: '複製錯誤詳情失敗,請查看主控台。'
}
};

View file

@ -0,0 +1,50 @@
import { NotemdSettings, TaskKey } from '../types';
import { resolveLanguageDisplayName } from './languageContext';
type LanguageSettingKey =
| 'generateTitleLanguage'
| 'researchSummarizeLanguage'
| 'addLinksLanguage'
| 'summarizeToMermaidLanguage'
| 'extractConceptsLanguage'
| 'extractOriginalTextLanguage';
const TASK_LANGUAGE_KEY_MAP: Partial<Record<TaskKey, LanguageSettingKey>> = {
addLinks: 'addLinksLanguage',
generateTitle: 'generateTitleLanguage',
researchSummarize: 'researchSummarizeLanguage',
summarizeToMermaid: 'summarizeToMermaidLanguage',
extractConcepts: 'extractConceptsLanguage',
extractOriginalText: 'extractOriginalTextLanguage',
extractOriginalTextMerged: 'extractOriginalTextLanguage'
};
export function shouldApplyAutoTranslation(settings: NotemdSettings, taskKey: TaskKey): boolean {
if (taskKey === 'translate') {
return true;
}
return !settings.disableAutoTranslation;
}
export function resolveTaskLanguageCode(settings: NotemdSettings, taskKey: TaskKey): string {
if (taskKey === 'translate') {
return settings.language || 'en';
}
if (settings.useDifferentLanguagesForTasks) {
const key = TASK_LANGUAGE_KEY_MAP[taskKey];
if (key) {
const taskLanguage = settings[key];
if (typeof taskLanguage === 'string' && taskLanguage.trim().length > 0) {
return taskLanguage;
}
}
}
return settings.language || 'en';
}
export function resolveTaskLanguageName(settings: NotemdSettings, taskKey: TaskKey): string {
return resolveLanguageDisplayName(settings, resolveTaskLanguageCode(settings, taskKey));
}

View file

@ -28,6 +28,8 @@ import { NotemdSidebarView } from './ui/NotemdSidebarView';
import { translateFile, batchTranslateFolder } from './translate';
import { getSystemPrompt } from './promptUtils';
import { extractOriginalText } from './extractOriginalText';
import { getI18nStrings } from './i18n';
import { resolveTaskLanguageCode } from './i18n/taskLanguagePolicy';
export default class NotemdPlugin extends Plugin {
settings: NotemdSettings;
@ -47,6 +49,10 @@ export default class NotemdPlugin extends Plugin {
// this.updateStatusBar(busy ? 'Busy...' : 'Ready');
}
private getUiStrings() {
return getI18nStrings({ uiLocale: this.settings?.uiLocale || 'auto' });
}
async onload() {
await this.loadSettings();
@ -652,7 +658,7 @@ export default class NotemdPlugin extends Plugin {
/** Command: Process Current File (Add Links) */
async processWithNotemdCommand(reporter?: ProgressReporter) {
if (this.isBusy) { new Notice("Notemd is busy."); return; }
if (this.isBusy) { new Notice(this.getUiStrings().notices.notemdBusy); return; }
this.isBusy = true;
const useReporter = reporter || this.getReporter();
@ -719,7 +725,7 @@ export default class NotemdPlugin extends Plugin {
/** Command: Process Folder (Add Links) */
async processFolderWithNotemdCommand(reporter?: ProgressReporter, folderPathOverride?: string) {
if (this.isBusy) { new Notice("Notemd is busy."); return; }
if (this.isBusy) { new Notice(this.getUiStrings().notices.notemdBusy); return; }
this.isBusy = true;
const useReporter = reporter || this.getReporter();
@ -944,7 +950,7 @@ export default class NotemdPlugin extends Plugin {
/** Command: Generate Content from Title */
async generateContentForTitleCommand(file: TFile, reporter?: ProgressReporter) {
if (this.isBusy) { new Notice("Notemd is busy."); return; }
if (this.isBusy) { new Notice(this.getUiStrings().notices.notemdBusy); return; }
this.isBusy = true;
const useReporter = reporter || this.getReporter();
@ -996,7 +1002,7 @@ export default class NotemdPlugin extends Plugin {
/** Command: Research and Summarize Topic */
async researchAndSummarizeCommand(editor: Editor, view: MarkdownView, reporter?: ProgressReporter) {
if (this.isBusy) { new Notice("Notemd is busy."); return; }
if (this.isBusy) { new Notice(this.getUiStrings().notices.notemdBusy); return; }
this.isBusy = true;
const useReporter = reporter || this.getReporter();
@ -1065,7 +1071,7 @@ export default class NotemdPlugin extends Plugin {
reporter?: ProgressReporter,
folderPathOverride?: string
): Promise<{ sourceFolderPath: string; completeFolderPath: string } | null> {
if (this.isBusy) { new Notice("Notemd is busy."); return null; }
if (this.isBusy) { new Notice(this.getUiStrings().notices.notemdBusy); return null; }
this.isBusy = true;
const useReporter = reporter || this.getReporter();
@ -1145,7 +1151,7 @@ export default class NotemdPlugin extends Plugin {
/** Command: Check and Remove Duplicate Concept Notes */
async checkAndRemoveDuplicateConceptNotesCommand(reporter?: ProgressReporter) {
if (this.isBusy) { new Notice("Notemd is busy."); return; }
if (this.isBusy) { new Notice(this.getUiStrings().notices.notemdBusy); return; }
this.isBusy = true;
const useReporter = reporter || this.getReporter();
@ -1189,7 +1195,7 @@ export default class NotemdPlugin extends Plugin {
reporter?: ProgressReporter,
folderPathOverride?: string
): Promise<{ folderPath: string; modifiedCount: number } | null> {
if (this.isBusy) { new Notice("Notemd is busy."); return null; }
if (this.isBusy) { new Notice(this.getUiStrings().notices.notemdBusy); return null; }
this.isBusy = true;
const useReporter = reporter || this.getReporter();
@ -1253,7 +1259,7 @@ export default class NotemdPlugin extends Plugin {
}
async fixFormulaFormatsCommand(file: TFile, reporter?: ProgressReporter) {
if (this.isBusy) { new Notice("Notemd is busy."); return; }
if (this.isBusy) { new Notice(this.getUiStrings().notices.notemdBusy); return; }
this.isBusy = true;
const useReporter = reporter || this.getReporter();
@ -1297,7 +1303,7 @@ export default class NotemdPlugin extends Plugin {
}
async batchFixFormulaFormatsCommand(reporter?: ProgressReporter) {
if (this.isBusy) { new Notice("Notemd is busy."); return; }
if (this.isBusy) { new Notice(this.getUiStrings().notices.notemdBusy); return; }
this.isBusy = true;
const useReporter = reporter || this.getReporter();
@ -1349,7 +1355,7 @@ export default class NotemdPlugin extends Plugin {
async batchTranslateFolderCommand(folder?: TFolder, reporter?: ProgressReporter) {
if (this.isBusy) {
new Notice("Notemd is busy.");
new Notice(this.getUiStrings().notices.notemdBusy);
return;
}
this.isBusy = true;
@ -1381,7 +1387,8 @@ export default class NotemdPlugin extends Plugin {
}
const resolvedTargetFolder = targetFolder as TFolder;
await batchTranslateFolder(this.app, this.settings, resolvedTargetFolder, this.settings.language);
const translateLanguage = resolveTaskLanguageCode(this.settings, 'translate');
await batchTranslateFolder(this.app, this.settings, resolvedTargetFolder, translateLanguage);
if (!useReporter.cancelled) {
const mermaidFixTarget = (this.settings.useCustomTranslationSavePath && this.settings.translationSavePath)
? this.settings.translationSavePath
@ -1414,7 +1421,7 @@ export default class NotemdPlugin extends Plugin {
async translateFileCommand(file: TFile, signal?: AbortSignal, reporter?: ProgressReporter) {
if (this.isBusy) {
new Notice("Notemd is busy.");
new Notice(this.getUiStrings().notices.notemdBusy);
return;
}
this.isBusy = true;
@ -1432,7 +1439,8 @@ export default class NotemdPlugin extends Plugin {
try {
await this.loadSettings();
const outputPath = await translateFile(this.app, this.settings, file, this.settings.language, useReporter, true, signal);
const translateLanguage = resolveTaskLanguageCode(this.settings, 'translate');
const outputPath = await translateFile(this.app, this.settings, file, translateLanguage, useReporter, true, signal);
if (outputPath && this.settings.autoMermaidFixAfterGenerate) {
const outputFile = this.app.vault.getAbstractFileByPath(outputPath);
if (outputFile instanceof TFile) {
@ -1473,7 +1481,7 @@ export default class NotemdPlugin extends Plugin {
async summarizeToMermaidCommand(file: TFile, reporter: ProgressReporter) {
if (this.isBusy) {
new Notice('Another process is running. Please wait.');
new Notice(this.getUiStrings().notices.anotherProcessRunning);
return;
}
this.isBusy = true;
@ -1542,7 +1550,7 @@ export default class NotemdPlugin extends Plugin {
}
async extractConceptsCommand(reporter?: ProgressReporter) {
if (this.isBusy) { new Notice("Notemd is busy."); return; }
if (this.isBusy) { new Notice(this.getUiStrings().notices.notemdBusy); return; }
this.isBusy = true;
const useReporter = reporter || this.getReporter();
@ -1610,7 +1618,7 @@ export default class NotemdPlugin extends Plugin {
}
async batchExtractConceptsForFolderCommand(reporter?: ProgressReporter) {
if (this.isBusy) { new Notice("Notemd is busy."); return; }
if (this.isBusy) { new Notice(this.getUiStrings().notices.notemdBusy); return; }
this.isBusy = true;
const useReporter = reporter || this.getReporter();
@ -1791,7 +1799,7 @@ export default class NotemdPlugin extends Plugin {
async extractConceptsAndGenerateTitlesCommand(reporter?: ProgressReporter) {
if (this.isBusy) {
new Notice("Notemd is busy.");
new Notice(this.getUiStrings().notices.notemdBusy);
return;
}
this.isBusy = true;
@ -1844,7 +1852,7 @@ export default class NotemdPlugin extends Plugin {
async extractOriginalTextCommand(reporter?: ProgressReporter) {
if (this.isBusy) {
new Notice("Notemd is busy.");
new Notice(this.getUiStrings().notices.notemdBusy);
return;
}
this.isBusy = true;

View file

@ -1,4 +1,9 @@
import { NotemdSettings, TaskKey } from './types';
import {
resolveTaskLanguageName,
shouldApplyAutoTranslation
} from './i18n/taskLanguagePolicy';
import { resolveLanguageDisplayName } from './i18n/languageContext';
export const DEFAULT_PROMPTS: Record<TaskKey, string> = {
extractConcepts: `You are an AI assistant specializing in knowledge extraction. Your task is to Completely decompose and structure the knowledge points in this markdown document, analyze the markdown document and identify all core concepts and keywords.
@ -416,54 +421,42 @@ export function getSystemPrompt(settings: NotemdSettings, taskKey: TaskKey, repl
prompt = `${domainText}\n\n${prompt}`;
}
const autoTranslationEnabled = shouldApplyAutoTranslation(settings, taskKey);
const resolvedTaskLanguageName = resolveTaskLanguageName(settings, taskKey);
const finalReplacements: Record<string, string> = { ...replacements };
if (finalReplacements.LANGUAGE) {
finalReplacements.LANGUAGE = resolveLanguageDisplayName(settings, finalReplacements.LANGUAGE);
} else if (autoTranslationEnabled || taskKey === 'translate') {
finalReplacements.LANGUAGE = resolvedTaskLanguageName;
} else {
finalReplacements.LANGUAGE = 'the same language as the source content';
}
// Perform replacements
for (const key in replacements) {
prompt = prompt.replace(new RegExp(`{${key}}`, 'g'), replacements[key]);
for (const key in finalReplacements) {
prompt = prompt.replace(new RegExp(`{${key}}`, 'g'), finalReplacements[key]);
}
// Add translation instruction for Mermaid summarization if enabled
if (taskKey === 'summarizeToMermaid' && settings.translateSummarizeToMermaidOutput && !settings.disableAutoTranslation) {
const targetLanguageName = settings.availableLanguages.find(lang => lang.code === settings.language)?.name || settings.language;
if (taskKey === 'summarizeToMermaid' && settings.translateSummarizeToMermaidOutput && autoTranslationEnabled) {
prompt += `
IMPORTANT: The entire Mermaid diagram, including all node text, MUST be translated into ${targetLanguageName}.`;
IMPORTANT: The entire Mermaid diagram, including all node text, MUST be translated into ${resolvedTaskLanguageName}.`;
}
// Add translation instruction for extractOriginalText (and Merged variant) if enabled
if ((taskKey === 'extractOriginalText' || taskKey === 'extractOriginalTextMerged') && settings.translateExtractOriginalTextOutput && !settings.disableAutoTranslation) {
// Modify the output format instruction for the multi-line format
// We look for the generic bullet structure pattern
const translationInstruction = `\n\nIMPORTANT: For each matching excerpt, you MUST append the translation in {LANGUAGE}.
if ((taskKey === 'extractOriginalText' || taskKey === 'extractOriginalTextMerged') && settings.translateExtractOriginalTextOutput && autoTranslationEnabled) {
const translationInstruction = `\n\nIMPORTANT: For each matching excerpt, you MUST append the translation in ${resolvedTaskLanguageName}.
The format for each bullet point must be strictly:
- [Exact Excerpt] - [Excerpt translated into {LANGUAGE}]`;
- [Exact Excerpt] - [Excerpt translated into ${resolvedTaskLanguageName}]`;
prompt += translationInstruction;
prompt += translationInstruction;
}
// Add language instruction for extractConcepts if a specific language is set
if (taskKey === 'extractConcepts' && !settings.disableAutoTranslation) {
const languageCode = settings.useDifferentLanguagesForTasks ? settings.extractConceptsLanguage : settings.language;
const targetLanguageName = settings.availableLanguages.find(lang => lang.code === languageCode)?.name || languageCode;
if (targetLanguageName) {
prompt += `\n\nThe output concepts MUST be in ${targetLanguageName}.`;
}
}
// For other tasks that might have language settings, ensure they are also skipped if disableAutoTranslation is true
if (taskKey !== 'translate' && taskKey !== 'summarizeToMermaid' && taskKey !== 'extractConcepts' && !settings.disableAutoTranslation) {
const languageCode = settings.useDifferentLanguagesForTasks
? (settings as any)[`${taskKey}Language`] // e.g., settings.addLinksLanguage
: settings.language;
const targetLanguageName = settings.availableLanguages.find(lang => lang.code === languageCode)?.name || languageCode;
if (targetLanguageName && prompt.includes('{LANGUAGE}')) {
prompt = prompt.replace(/{LANGUAGE}/g, targetLanguageName);
} else if (targetLanguageName) {
// A more generic way to add language instruction if not already in prompt
// This part might need to be adjusted based on how other prompts are structured
}
if (taskKey === 'extractConcepts' && autoTranslationEnabled) {
prompt += `\n\nThe output concepts MUST be in ${resolvedTaskLanguageName}.`;
}
return prompt;

View file

@ -6,6 +6,7 @@ import { cleanupLatexDelimiters, refineMermaidBlocks } from './mermaidProcessor'
import { ErrorModal } from './ui/ErrorModal';
import { getSystemPrompt } from './promptUtils';
import { SearchManager } from './search/SearchManager';
import { resolveTaskLanguageCode } from './i18n/taskLanguagePolicy';
/**
* Fetches content from a URL and extracts basic text.
@ -244,7 +245,7 @@ export async function researchAndSummarize(app: App, settings: NotemdSettings, e
progressReporter.log(`Calling ${provider.name} (Model: ${modelName}) for summarization...`);
const language = settings.useDifferentLanguagesForTasks ? settings.researchSummarizeLanguage : settings.language;
const language = resolveTaskLanguageCode(settings, 'researchSummarize');
const finalPrompt = getSystemPrompt(settings, 'researchSummarize', {
TOPIC: topic,

View file

@ -44,6 +44,7 @@ export const mockSettings: NotemdSettings = {
duplicateCheckScopeMode: 'vault',
duplicateCheckScopePaths: '',
removeCodeFencesOnAddLinks: false,
uiLocale: 'auto',
language: 'en',
availableLanguages: [{ code: 'en', name: 'English' }],
useDifferentLanguagesForTasks: false,

View file

@ -0,0 +1,52 @@
jest.mock('obsidian');
import { getLanguage } from 'obsidian';
import { clearI18nCache, formatI18n, getI18nStrings, getResolvedStrings } from '../i18n';
describe('i18n fallback and cache', () => {
beforeEach(() => {
clearI18nCache();
(getLanguage as jest.Mock).mockReset();
});
test('falls back to English when locale is unsupported', () => {
const locale = getResolvedStrings('fr');
expect(locale.common.cancel).toBe('Cancel');
expect(locale.sidebar.logOutputTitle).toBe('Log output');
});
test('supports zh locale alias resolution', () => {
const zh = getResolvedStrings('zh');
expect(zh.common.cancel).toBe('取消');
});
test('fills missing localized keys from English base catalog', () => {
const zhTw = getResolvedStrings('zh-TW');
expect(zhTw.notices.notemdBusy).toBe('Notemd 忙碌中。');
// This key exists in English and should always be available even if future locales omit it.
expect(zhTw.common.unknownError).toBeDefined();
});
test('returns cached object for identical locale', () => {
const first = getResolvedStrings('zh-CN');
const second = getResolvedStrings('zh_cn');
expect(first).toBe(second);
});
test('resolves strings using Obsidian language when uiLocale is auto', () => {
(getLanguage as jest.Mock).mockReturnValue('zh-cn');
const resolved = getI18nStrings({ uiLocale: 'auto' });
expect(resolved.common.language).toBe('语言');
});
test('manual ui locale override has priority over Obsidian language', () => {
(getLanguage as jest.Mock).mockReturnValue('en');
const resolved = getI18nStrings({ uiLocale: 'zh-TW' });
expect(resolved.common.language).toBe('語言');
});
test('formats placeholders', () => {
expect(formatI18n('Language changed to {language}', { language: 'zh-CN' })).toBe('Language changed to zh-CN');
});
});

View file

@ -0,0 +1,97 @@
import { DEFAULT_SETTINGS } from '../constants';
import { resolveUiLocale } from '../i18n/languageContext';
import {
resolveTaskLanguageCode,
resolveTaskLanguageName,
shouldApplyAutoTranslation
} from '../i18n/taskLanguagePolicy';
import { NotemdSettings, TaskKey } from '../types';
function createSettings(overrides: Partial<NotemdSettings> = {}): NotemdSettings {
return {
...DEFAULT_SETTINGS,
...overrides
};
}
describe('task language policy', () => {
test('uses global language when per-task language is disabled', () => {
const settings = createSettings({
language: 'zh-CN',
useDifferentLanguagesForTasks: false,
generateTitleLanguage: 'de'
});
expect(resolveTaskLanguageCode(settings, 'generateTitle')).toBe('zh-CN');
});
test('uses task-specific language when enabled', () => {
const settings = createSettings({
language: 'en',
useDifferentLanguagesForTasks: true,
summarizeToMermaidLanguage: 'ja'
});
expect(resolveTaskLanguageCode(settings, 'summarizeToMermaid')).toBe('ja');
});
test('uses extractOriginalText language for merged mode', () => {
const settings = createSettings({
language: 'en',
useDifferentLanguagesForTasks: true,
extractOriginalTextLanguage: 'fr'
});
expect(resolveTaskLanguageCode(settings, 'extractOriginalTextMerged')).toBe('fr');
});
test('translate task always uses global translation target', () => {
const settings = createSettings({
language: 'es',
useDifferentLanguagesForTasks: true
});
expect(resolveTaskLanguageCode(settings, 'translate')).toBe('es');
});
test('resolves language display name from configured language list', () => {
const settings = createSettings({
language: 'zh-CN',
useDifferentLanguagesForTasks: false
});
expect(resolveTaskLanguageName(settings, 'addLinks')).toBe('简体中文');
});
test('disableAutoTranslation is ignored for explicit translate task', () => {
const settings = createSettings({ disableAutoTranslation: true });
expect(shouldApplyAutoTranslation(settings, 'translate')).toBe(true);
});
test('disableAutoTranslation disables auto-translation for non-translate tasks', () => {
const settings = createSettings({ disableAutoTranslation: true });
const cases: TaskKey[] = ['addLinks', 'generateTitle', 'researchSummarize', 'summarizeToMermaid', 'extractConcepts'];
cases.forEach(taskKey => {
expect(shouldApplyAutoTranslation(settings, taskKey)).toBe(false);
});
});
});
describe('ui locale resolution', () => {
test('uses manual locale when configured and supported', () => {
const settings = createSettings({ uiLocale: 'zh-CN' });
expect(resolveUiLocale(settings, 'en')).toBe('zh-CN');
});
test('uses obsidian locale when ui locale is auto', () => {
const settings = createSettings({ uiLocale: 'auto' });
expect(resolveUiLocale(settings, 'zh_cn')).toBe('zh-CN');
});
test('falls back to en when unsupported', () => {
const settings = createSettings({ uiLocale: 'auto' });
expect(resolveUiLocale(settings, 'fr')).toBe('en');
});
});

View file

@ -0,0 +1,33 @@
import { formatDateForLocale, formatTimeForLocale, isRtlLocale } from '../i18n/localeFormat';
describe('localeFormat utilities', () => {
test('detects RTL locales by prefix', () => {
expect(isRtlLocale('ar')).toBe(true);
expect(isRtlLocale('he-IL')).toBe(true);
expect(isRtlLocale('fa_IR')).toBe(true);
expect(isRtlLocale('en')).toBe(false);
});
test('formats time with provided locale and falls back safely', () => {
const date = new Date('2026-01-02T03:04:05.000Z');
const formatted = formatTimeForLocale(date, 'en-US');
expect(typeof formatted).toBe('string');
expect(formatted.length).toBeGreaterThan(0);
const fallback = formatTimeForLocale(date, 'invalid-locale-name');
expect(typeof fallback).toBe('string');
expect(fallback.length).toBeGreaterThan(0);
});
test('formats date with provided locale and falls back safely', () => {
const date = new Date('2026-01-02T03:04:05.000Z');
const formatted = formatDateForLocale(date, 'zh-CN');
expect(typeof formatted).toBe('string');
expect(formatted.length).toBeGreaterThan(0);
const fallback = formatDateForLocale(date, 'invalid-locale-name');
expect(typeof fallback).toBe('string');
expect(fallback.length).toBeGreaterThan(0);
});
});

View file

@ -4,6 +4,7 @@ import { getProviderForTask, getModelForTask, splitContent, createConcurrentProc
import { callLLM, handleApiError } from './llmUtils';
import { getSystemPrompt } from './promptUtils';
import { ProgressModal } from './ui/ProgressModal';
import { resolveLanguageDisplayName } from './i18n/languageContext';
export async function batchTranslateFolder(
@ -76,8 +77,9 @@ export async function translateFile(
}
const model = getModelForTask('translate', provider, settings);
const promptLanguageName = resolveLanguageDisplayName(settings, targetLanguage);
const prompt = getSystemPrompt(settings, 'translate', {
LANGUAGE: targetLanguage,
LANGUAGE: promptLanguageName,
TEXT: fileContent,
});

View file

@ -89,6 +89,7 @@ export interface NotemdSettings {
// Add Links Post-Processing
removeCodeFencesOnAddLinks: boolean; // New: Option to remove ```markdown and ``` fences
// Language Settings
uiLocale: string; // 'auto' uses Obsidian locale; otherwise a specific UI locale code
language: string; // Stores the selected language code (e.g., 'en', 'es', 'fr')
availableLanguages: Array<{ code: string; name: string }>; // List of available languages
useDifferentLanguagesForTasks: boolean;

View file

@ -1,4 +1,5 @@
import { App, Modal, Notice } from 'obsidian';
import { getI18nStrings } from '../i18n';
// --- Error Modal for Copyable Messages ---
export class ErrorModal extends Modal {
@ -12,6 +13,7 @@ export class ErrorModal extends Modal {
}
onOpen() {
const i18n = getI18nStrings({ uiLocale: 'auto' });
const { contentEl } = this;
contentEl.addClass('notemd-error-modal');
@ -23,19 +25,19 @@ export class ErrorModal extends Modal {
// Add a copy button
const buttonContainer = contentEl.createDiv({ cls: 'modal-button-container' });
const copyButton = buttonContainer.createEl('button', { text: 'Copy Error Details', cls: 'mod-cta' });
const copyButton = buttonContainer.createEl('button', { text: i18n.errorModal.copyDetails, cls: 'mod-cta' });
copyButton.onclick = () => {
navigator.clipboard.writeText(this.errorMessage).then(() => {
new Notice('Error details copied to clipboard!');
copyButton.setText('Copied!');
setTimeout(() => copyButton.setText('Copy Error Details'), 2000);
new Notice(i18n.errorModal.copySuccessNotice);
copyButton.setText(i18n.errorModal.copied);
setTimeout(() => copyButton.setText(i18n.errorModal.copyDetails), 2000);
}).catch(err => {
new Notice('Failed to copy error details. See console.');
new Notice(i18n.errorModal.copyFailedNotice);
console.error('Failed to copy error to clipboard:', err);
});
};
const closeButton = buttonContainer.createEl('button', { text: 'Close' });
const closeButton = buttonContainer.createEl('button', { text: i18n.common.close });
closeButton.onclick = () => {
this.close();
};

View file

@ -27,6 +27,7 @@ import {
SIDEBAR_ACTION_DEFINITIONS,
SidebarActionId
} from '../workflowButtons';
import { formatI18n, getI18nStrings } from '../i18n';
// Define specific key types for settings accessed dynamically
type ProviderSettingKey = 'addLinksProvider' | 'researchProvider' | 'generateTitleProvider' | 'translateProvider';
@ -95,6 +96,7 @@ export class NotemdSettingTab extends PluginSettingTab {
buttonControl: ButtonComponent,
callMode: ProviderDiagnosticCallMode
): Promise<void> {
const i18n = getI18nStrings({ uiLocale: this.plugin.settings.uiLocale });
const blockingIssues = getProviderValidationIssues(provider)
.filter(issue => issue.level === 'error')
.map(issue => issue.message);
@ -104,7 +106,7 @@ export class NotemdSettingTab extends PluginSettingTab {
return;
}
buttonControl.setDisabled(true).setButtonText('Running...');
buttonControl.setDisabled(true).setButtonText(i18n.settings.developer.runDiagnostic);
const runningNotice = new Notice(`Running developer diagnostic for ${provider.name}...`, 0);
const timeoutMs = this.sanitizeDeveloperDiagnosticTimeoutMs(this.plugin.settings.developerDiagnosticTimeoutMs);
@ -127,7 +129,7 @@ export class NotemdSettingTab extends PluginSettingTab {
new Notice(`Developer diagnostic failed before report generation: ${message}`, 12000);
console.error('Developer provider diagnostic failed:', error);
} finally {
buttonControl.setDisabled(false).setButtonText('Run diagnostic');
buttonControl.setDisabled(false).setButtonText(i18n.settings.developer.runDiagnostic);
}
}
@ -136,6 +138,7 @@ export class NotemdSettingTab extends PluginSettingTab {
buttonControl: ButtonComponent,
callMode: ProviderDiagnosticCallMode
): Promise<void> {
const i18n = getI18nStrings({ uiLocale: this.plugin.settings.uiLocale });
const blockingIssues = getProviderValidationIssues(provider)
.filter(issue => issue.level === 'error')
.map(issue => issue.message);
@ -147,7 +150,7 @@ export class NotemdSettingTab extends PluginSettingTab {
const runs = this.sanitizeDeveloperDiagnosticRuns(this.plugin.settings.developerDiagnosticStabilityRuns);
const timeoutMs = this.sanitizeDeveloperDiagnosticTimeoutMs(this.plugin.settings.developerDiagnosticTimeoutMs);
buttonControl.setDisabled(true).setButtonText('Running...');
buttonControl.setDisabled(true).setButtonText(i18n.settings.developer.runStability);
const runningNotice = new Notice(
`Running developer stability diagnostic for ${provider.name} (${runs} runs)...`,
0
@ -171,7 +174,7 @@ export class NotemdSettingTab extends PluginSettingTab {
new Notice(`Developer stability diagnostic failed before report generation: ${message}`, 12000);
console.error('Developer provider stability diagnostic failed:', error);
} finally {
buttonControl.setDisabled(false).setButtonText('Run stability test');
buttonControl.setDisabled(false).setButtonText(i18n.settings.developer.runStability);
}
}
@ -455,6 +458,7 @@ export class NotemdSettingTab extends PluginSettingTab {
display(): void {
const { containerEl } = this;
containerEl.empty();
const i18n = getI18nStrings({ uiLocale: this.plugin.settings.uiLocale });
// --- Provider Configuration ---
new Setting(containerEl).setName('LLM providers').setHeading();
@ -793,8 +797,8 @@ export class NotemdSettingTab extends PluginSettingTab {
}));
new Setting(containerEl)
.setName('Developer mode')
.setDesc('On: Show dedicated developer diagnostic tools in settings. Off: Hide developer-only controls.')
.setName(i18n.settings.developer.modeName)
.setDesc(i18n.settings.developer.modeDesc)
.addToggle(toggle => toggle
.setValue(this.plugin.settings.enableDeveloperMode)
.onChange(async (value) => {
@ -804,7 +808,7 @@ export class NotemdSettingTab extends PluginSettingTab {
}));
if (this.plugin.settings.enableDeveloperMode && activeProvider) {
new Setting(containerEl).setName('Developer diagnostics').setHeading();
new Setting(containerEl).setName(i18n.settings.developer.heading).setHeading();
const diagnosticModeOptions = getProviderDiagnosticCallModeOptions(activeProvider);
const modeSet = new Set(diagnosticModeOptions.map(option => option.value));
@ -873,12 +877,12 @@ export class NotemdSettingTab extends PluginSettingTab {
.setName('Developer provider diagnostic (long request)')
.setDesc('Run one long-request diagnostic with the selected call mode and save a full report to vault root.')
.addButton(button => button
.setButtonText('Run diagnostic')
.setButtonText(i18n.settings.developer.runDiagnostic)
.onClick(async () => {
await this.runDeveloperProviderDiagnostic(activeProvider, button, effectiveCallMode);
}))
.addButton(button => button
.setButtonText('Run stability test')
.setButtonText(i18n.settings.developer.runStability)
.onClick(async () => {
await this.runDeveloperProviderStabilityDiagnostic(activeProvider, button, effectiveCallMode);
}));
@ -1094,10 +1098,10 @@ export class NotemdSettingTab extends PluginSettingTab {
}));
new Setting(containerEl).setName('Language settings').setHeading();
new Setting(containerEl).setName(i18n.settings.language.heading).setHeading();
new Setting(containerEl)
.setName('Output language')
.setDesc('Select the desired output language for LLM responses.')
.setName(i18n.settings.language.outputName)
.setDesc(i18n.settings.language.outputDesc)
.addDropdown(dropdown => {
(this.plugin.settings.availableLanguages || DEFAULT_SETTINGS.availableLanguages).forEach(lang => {
dropdown.addOption(lang.code, lang.name);
@ -1111,8 +1115,8 @@ export class NotemdSettingTab extends PluginSettingTab {
});
new Setting(containerEl)
.setName('Select different languages for different tasks.')
.setDesc('On: Select a specific language for each task below. Off: Use the single "Output language".')
.setName(i18n.settings.language.perTaskName)
.setDesc(i18n.settings.language.perTaskDesc)
.addToggle(toggle => toggle
.setValue(this.plugin.settings.useDifferentLanguagesForTasks)
.onChange(async (value) => {
@ -1122,11 +1126,8 @@ export class NotemdSettingTab extends PluginSettingTab {
}));
new Setting(containerEl)
.setName('Disable auto translation (except for "Translate" task)')
.setDesc(
'On: Non-Translate tasks do not force a target language or auto-translate outputs. ' +
'The explicit "Translate" task still performs translation as configured.'
)
.setName(i18n.settings.language.disableAutoTranslationName)
.setDesc(i18n.settings.language.disableAutoTranslationDesc)
.addToggle(toggle =>
toggle
.setValue(this.plugin.settings.disableAutoTranslation)
@ -1141,8 +1142,8 @@ export class NotemdSettingTab extends PluginSettingTab {
const createTaskLanguageSettings = (languageSettingName: keyof NotemdSettings, taskDesc: string) => {
new Setting(containerEl)
.setName(`${taskDesc} language`)
.setDesc(`Select the output language for "${taskDesc}".`)
.setName(formatI18n(i18n.settings.language.taskLanguageLabel, { task: taskDesc }))
.setDesc(formatI18n(i18n.settings.language.taskLanguageDesc, { task: taskDesc }))
.addDropdown(dropdown => {
availableLanguages.forEach(lang => {
dropdown.addOption(lang.code, lang.name);

View file

@ -9,6 +9,8 @@ import {
SIDEBAR_ACTION_DEFINITIONS,
SidebarActionId
} from '../workflowButtons';
import { formatI18n, getI18nStrings } from '../i18n';
import { formatTimeForLocale } from '../i18n/localeFormat';
type ActionCategory = typeof SIDEBAR_ACTION_DEFINITIONS[number]['category'];
@ -88,6 +90,10 @@ export class NotemdSidebarView extends ItemView implements ProgressReporter {
private actionButtons = new Map<string, HTMLButtonElement>();
private workflowButtons: HTMLButtonElement[] = [];
private getStrings() {
return getI18nStrings({ uiLocale: this.plugin.settings.uiLocale });
}
constructor(leaf: WorkspaceLeaf, plugin: NotemdPlugin) {
super(leaf);
this.plugin = plugin;
@ -121,9 +127,10 @@ export class NotemdSidebarView extends ItemView implements ProgressReporter {
}
clearDisplay() {
const i18n = this.getStrings();
this.logContent = [];
if (this.logEl) this.logEl.empty();
if (this.statusEl) this.statusEl.setText('Ready');
if (this.statusEl) this.statusEl.setText(i18n.common.ready);
if (this.progressAreaEl) this.progressAreaEl.addClass('is-idle');
if (this.progressEl) {
this.progressEl.dataset.progress = '0';
@ -136,11 +143,11 @@ export class NotemdSidebarView extends ItemView implements ProgressReporter {
this.progressBarContainerEl.addClass('is-idle');
}
if (this.progressValueEl) {
this.progressValueEl.setText('Ready');
this.progressValueEl.setText(i18n.common.ready);
this.progressValueEl.addClass('is-idle');
this.progressValueEl.removeClass('is-error');
}
if (this.timeRemainingEl) this.timeRemainingEl.setText('Standby');
if (this.timeRemainingEl) this.timeRemainingEl.setText(i18n.common.standby);
if (this.cancelButton) {
this.cancelButton.disabled = true;
this.cancelButton.removeClass('is-active');
@ -206,7 +213,8 @@ export class NotemdSidebarView extends ItemView implements ProgressReporter {
if (!this.logEl) {
return;
}
const timestamp = `[${new Date().toLocaleTimeString()}]`;
const uiLocale = this.plugin.settings.uiLocale === 'auto' ? undefined : this.plugin.settings.uiLocale;
const timestamp = `[${formatTimeForLocale(new Date(), uiLocale)}]`;
const fullMessage = `${timestamp} ${message}`;
this.logContent.push(fullMessage);
@ -357,8 +365,9 @@ export class NotemdSidebarView extends ItemView implements ProgressReporter {
}
private async runSingleAction(actionId: SidebarActionId): Promise<void> {
const i18n = this.getStrings();
if (this.isProcessing || this.plugin.getIsBusy()) {
new Notice('Processing already in progress.');
new Notice(i18n.notices.processingAlreadyRunning);
return;
}
const actionLabel = SIDEBAR_ACTION_DEFINITIONS.find(def => def.id === actionId)?.label || actionId;
@ -380,8 +389,9 @@ export class NotemdSidebarView extends ItemView implements ProgressReporter {
}
private async runCustomWorkflow(button: CustomWorkflowButton): Promise<void> {
const i18n = this.getStrings();
if (this.isProcessing || this.plugin.getIsBusy()) {
new Notice('Processing already in progress.');
new Notice(i18n.notices.processingAlreadyRunning);
return;
}
this.startProcessing(`Workflow: ${button.name}`);
@ -582,8 +592,9 @@ export class NotemdSidebarView extends ItemView implements ProgressReporter {
}
private buildLanguageSelector(parent: HTMLElement) {
const i18n = this.getStrings();
const row = parent.createDiv({ cls: 'notemd-inline-control' });
row.createEl('label', { text: 'Language', cls: 'notemd-inline-label' });
row.createEl('label', { text: i18n.common.language, cls: 'notemd-inline-label' });
this.languageSelector = row.createEl('select', { cls: 'notemd-language-select' });
const selector = this.languageSelector;
this.plugin.settings.availableLanguages.forEach(lang => {
@ -593,11 +604,12 @@ export class NotemdSidebarView extends ItemView implements ProgressReporter {
selector.onchange = async () => {
this.plugin.settings.language = selector.value;
await this.plugin.saveSettings();
new Notice(`Language changed to ${selector.value}`);
new Notice(formatI18n(i18n.sidebar.languageChangedNotice, { language: selector.value }));
};
}
async onOpen() {
const i18n = this.getStrings();
const container = this.containerEl.children[1] as HTMLElement;
container.empty();
container.addClass('notemd-sidebar-container');
@ -606,14 +618,14 @@ export class NotemdSidebarView extends ItemView implements ProgressReporter {
const footer = shell.createDiv({ cls: 'notemd-sidebar-footer mod-docked' });
const hero = scrollArea.createDiv({ cls: 'notemd-hero-card' });
hero.createEl('h3', { text: 'Notemd Workbench' });
hero.createEl('p', { text: 'Run single actions or custom one-click workflows with live progress and logs.' });
hero.createEl('h3', { text: i18n.sidebar.heroTitle });
hero.createEl('p', { text: i18n.sidebar.heroDesc });
const workflowResolution = resolveCustomWorkflowButtons(this.plugin.settings.customWorkflowButtonsDsl);
const quickBody = this.createSection(
scrollArea,
'Quick Workflows',
'Custom buttons assembled from built-in actions.',
i18n.sidebar.quickWorkflowTitle,
i18n.sidebar.quickWorkflowDesc,
true
);
@ -636,7 +648,7 @@ export class NotemdSidebarView extends ItemView implements ProgressReporter {
if (workflowResolution.usedFallback && workflowResolution.errors.length > 0) {
const warn = quickBody.createDiv({ cls: 'notemd-workflow-warning' });
warn.setText(`Workflow DSL has ${workflowResolution.errors.length} issue(s). Sidebar is using default fallback.`);
warn.setText(formatI18n(i18n.sidebar.workflowFallbackWarning, { count: workflowResolution.errors.length }));
}
const actionsByCategory = new Map<ActionCategory, Array<typeof SIDEBAR_ACTION_DEFINITIONS[number]>>();
@ -655,7 +667,7 @@ export class NotemdSidebarView extends ItemView implements ProgressReporter {
const body = this.createSection(
scrollArea,
ACTION_CATEGORY_LABEL[category].title,
`Built-in ${ACTION_CATEGORY_LABEL[category].title.toLowerCase()} actions.`,
formatI18n(i18n.sidebar.builtInActionsPrefix, { category: ACTION_CATEGORY_LABEL[category].title.toLowerCase() }),
ACTION_CATEGORY_LABEL[category].openByDefault
);
@ -671,26 +683,26 @@ export class NotemdSidebarView extends ItemView implements ProgressReporter {
const progressArea = footer.createDiv({ cls: 'notemd-progress-area is-idle' });
this.progressAreaEl = progressArea;
const progressMeta = progressArea.createDiv({ cls: 'notemd-progress-meta' });
this.statusEl = progressMeta.createEl('p', { text: 'Ready', cls: 'notemd-status-text' });
this.progressValueEl = progressMeta.createEl('span', { text: 'Ready', cls: 'notemd-progress-value is-idle' });
this.statusEl = progressMeta.createEl('p', { text: i18n.common.ready, cls: 'notemd-status-text' });
this.progressValueEl = progressMeta.createEl('span', { text: i18n.common.ready, cls: 'notemd-progress-value is-idle' });
this.progressBarContainerEl = progressArea.createEl('div', { cls: 'notemd-progress-bar-container mod-sidebar is-idle' });
this.progressEl = this.progressBarContainerEl.createEl('div', { cls: 'notemd-progress-bar-fill' });
this.timeRemainingEl = progressArea.createEl('p', { text: 'Standby', cls: 'notemd-time-remaining' });
this.timeRemainingEl = progressArea.createEl('p', { text: i18n.common.standby, cls: 'notemd-time-remaining' });
this.cancelButton = progressArea.createEl('button', { text: 'Cancel processing', cls: 'notemd-cancel-button' });
this.cancelButton = progressArea.createEl('button', { text: i18n.sidebar.cancelProcessing, cls: 'notemd-cancel-button' });
this.cancelButton.onclick = () => this.requestCancel();
const logCard = footer.createDiv({ cls: 'notemd-log-card mod-persistent' });
const logHeader = logCard.createDiv({ cls: 'notemd-log-header' });
logHeader.createEl('h5', { text: 'Log output' });
const copyLogButton = logHeader.createEl('button', { text: 'Copy log', cls: 'notemd-copy-log-button' });
logHeader.createEl('h5', { text: i18n.sidebar.logOutputTitle });
const copyLogButton = logHeader.createEl('button', { text: i18n.sidebar.copyLog, cls: 'notemd-copy-log-button' });
copyLogButton.onclick = () => {
if (this.logContent.length > 0) {
navigator.clipboard
.writeText(this.logContent.join('\n'))
.then(() => new Notice('Log copied!'), () => new Notice('Failed to copy log.'));
.then(() => new Notice(i18n.sidebar.copyLogSuccess), () => new Notice(i18n.sidebar.copyLogFailed));
} else {
new Notice('Log is empty.');
new Notice(i18n.sidebar.logEmpty);
}
};
this.logEl = logCard.createEl('div', { cls: 'notemd-log-output is-selectable mod-sidebar' });

View file

@ -746,3 +746,19 @@
transition: none !important;
}
}
/* RTL safety: keep Notemd panel readable in right-to-left locales. */
.mod-rtl .notemd-sidebar-container,
.mod-rtl .notemd-log-output,
.mod-rtl .notemd-status-text,
.mod-rtl .notemd-time-remaining {
direction: rtl;
text-align: right;
}
.notemd-log-time,
.notemd-log-message,
.notemd-status-text,
.notemd-time-remaining {
unicode-bidi: plaintext;
}

View file

@ -23,5 +23,10 @@
},
"include": [
"**/*.ts"
],
"exclude": [
"node_modules",
"dist",
"ref"
]
}