三股水
Obsidian RSS Dashboard 插件中文化完整教程
本文从 Obsidian 撰写发布

Obsidian RSS 仪表盘 插件中文化完整教程

一、背景与目标

RSS 仪表盘 是一个功能强大的 Obsidian 插件,支持 RSS、Atom、JSON Feed、YouTube、播客等多种订阅源。但官方没有提供中文语言包,对于中文用户来说使用体验不够友好。

本文档记录了完整的插件中文化过程,包括:翻译文本提取、语言包创建、代码修改、构建测试、以及后续维护策略。适合有一定动手能力的 Obsidian 用户参考。


二、准备工作

2.1 环境要求

工具说明
Node.js用于运行构建脚本(推荐 LTS 版本)
npmNode.js 自带包管理器
VS Code推荐用于代码编辑(非必须)
Git(可选)用于版本管理和后续更新
Obsidian测试环境

2.2 下载插件源码

# 从 GitHub 克隆或下载
git clone https://github.com/amatya-aditya/obsidian-rss-dashboard.git
cd obsidian-rss-dashboard

三、核心思路

3.1 总体方案

RSS 仪表盘 插件没有内置国际化(i 18 n)架构,所有界面文本都是硬编码在 类型Script 文件中的英文。为了实现中文化,我们需要:

  1. 新建 i 18 n 架构:创建翻译文件和翻译引擎
  2. 提取所有界面文本:从源代码中提取需要翻译的英文
  3. 翻译并整合:将中文翻译添加到语言包
  4. 修改主入口文件:在插件启动时加载翻译

3.2 技术选型

方案说明
DOM 替换翻译通过 MutationObserver 监听 DOM 变化,动态替换文本
t() 函数翻译对硬编码文本调用翻译函数(需要修改源代码)
混合方案(本教程采用)主界面用 t() 函数 + 设置页面用 DOM 替换

3.3 为什么不用纯 t() 函数方案?

纯 t() 函数方案需要修改几百个源代码文件,工作量巨大,而且作者更新插件时所有修改都会丢失。混合方案可以在最小修改量的前提下实现大部分翻译。


四、文本提取

4.1 提取脚本

在项目根目录创建 extract-strings.js,用于自动扫描源代码中的界面文本:

// extract-strings.js
const fs = require('fs');
const path = require('path');

const SRC_DIR = './src';
const OUTPUT_FILE = './strings-to-translate.json';

// 需要提取的文本模式
const patterns = [
  /{\s*text:\s*["']([^"']+)["']/g,
  /\.setText\(\s*["']([^"']+)["']\s*\)/g,
  /\.setName\(\s*["']([^"']+)["']\s*\)/g,
  /\.setPlaceholder\(\s*["']([^"']+)["']\s*\)/g,
  /\.setButtonText\(\s*["']([^"']+)["']\s*\)/g,
  /\.setDesc\(\s*["']([^"']+)["']\s*\)/g,
  /new\s+Notice\(\s*["']([^"']+)["']\s*[\),]/g,
  /createEl\s*\([^,]+,\s*{\s*text:\s*["']([^"']+)["']/g,
  /\.addOption\(\s*["'][^"']+["']\s*,\s*["']([^"']+)["']\s*\)/g,
];

const found = new Set();

function extractFromFile(filePath) {
  const 内容 = fs.readFileSync(filePath, 'utf8');
  for (const pattern of patterns) {
    let match;
    while ((match = pattern.exec(内容)) !== null) {
      const text = match[1];
      // 过滤掉代码片段和非界面文本
      if (text.length > 1 && 
          !text.startsWith('http') && 
          !text.match(/^[a-zA-Z 0-9_\-\.\/]+$/)) {
        found.add(text);
      }
    }
  }
}

function walkDir(dir) {
  const files = fs.readdirSync(dir);
  for (const file of files) {
    const fullPath = path.join(dir, file);
    if (fs.statSync(fullPath).isDirectory()) {
      if (!file.startsWith('.') && file !== 'node_modules') {
        walkDir(fullPath);
      }
    } else if (file.endsWith('.ts') || file.endsWith('.tsx')) {
      extractFromFile(fullPath);
    }
  }
}

console.log('🔍 开始扫描 src 目录...');
walkDir(SRC_DIR);
console.log(`✅ 找到 ${found.size} 条需要翻译的文本`);

const result = Array.from(found).sort();
fs.writeFileSync(OUTPUT_FILE, JSON.stringify(result, null, 2));
console.log(`📄 已保存到: ${OUTPUT_FILE}`);

4.2 运行提取

node extract-strings.js

生成的 strings-to-translate.json 包含所有待翻译文本。

4.3 提取结果分析

提取脚本可能提取出 5000+ 条候选文本,但其中约 90% 是代码片段(CSS 类名、HTML 标签、属性名等),需要人工筛选出真正的界面文本(约 200-500 条)。


五、翻译与语言包

5.1 项目结构

obsidian-rss-dashboard/
├── src/
│   ├── i 18 n/                    ← 新建目录
│   │   ├── index.ts             ← 翻译引擎
│   │   └── locales/
│   │       └── zh-CN.ts         ← 中文语言包
│   ├── settings/
│   ├── views/
│   └── ...
├── main.ts                      ← 修改(添加 i 18 n 初始化)
├── manifest.json
└── package.json

5.2 翻译引擎 src/i 18 n/index.ts

import { zhCN } from './locales/zh-CN';

const locales = {
  'zh-CN': zhCN,
} as const;

let currentLocale: keyof typeof locales = 'zh-CN';

export function t(key: string): string {
  const locale = locales[currentLocale];
  if (locale && key in locale) {
    return (locale as Record<string, string>)[key];
  }
  return key;
}

export function setLocale(locale: keyof typeof locales) {
  currentLocale = locale;
}

export function getLocale(): keyof typeof locales {
  return currentLocale;
}

export function applyTranslationsToDOM(root: Element = document.body) {
  const translations = locales['zh-CN'];
  
  const walker = document.createTreeWalker(
    root,
    NodeFilter.SHOW_TEXT,
    {
      acceptNode: (node) => {
        const parent = node.parentElement;
        if (parent) {
          const tag = parent.tagName;
          if (tag === 'SCRIPT' || tag === 'STYLE' || 
              tag === 'TEXTAREA' || tag === 'INPUT') {
            return NodeFilter.FILTER_REJECT;
          }
        }
        return NodeFilter.FILTER_ACCEPT;
      }
    }
  );
  
  const textNodes: Text[] = [];
  let node: Node | null;
  while ((node = walker.nextNode())) {
    textNodes.push(node as Text);
  }
  
  for (const textNode of textNodes) {
    const originalText = textNode.textContent || '';
    const trimmedText = originalText.trim();
    if (!trimmedText || /^[\d\s\W]+$/.test(trimmedText)) continue;
    
    let newText = originalText;
    let replaced = false;
    
    for (const [en, zh] of Object.entries(translations)) {
      if (!en || !zh) continue;
      
      if (originalText === en || trimmedText === en) {
        newText = originalText.replace(en, zh);
        replaced = true;
        break;
      }
      
      if (originalText.includes(en) && en.length > 2) {
        const escapedEn = en.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
        const regex = new RegExp(`(?<!<[^>]*)${escapedEn}(?![^<]*>)`, 'g');
        if (regex.test(originalText)) {
          newText = originalText.replace(regex, zh);
          replaced = true;
          break;
        }
      }
    }
    
    if (replaced && newText !== originalText) {
      textNode.textContent = newText;
    }
  }
  
  // 专门处理设置项描述
  root.querySelector全部('.setting-item-description, .setting-item-name').forEach((el) => {
    const text = el.textContent || '';
    const trimmed = text.trim();
    if (!trimmed) return;
    for (const [en, zh] of Object.entries(translations)) {
      if (!en || !zh) continue;
      if (trimmed === en || text === en) {
        el.textContent = text.replace(en, zh);
        break;
      }
    }
  });
}

export function forceTranslate(root: Element): void {
  if (!root) return;
  setTimeout(() => applyTranslationsToDOM(root), 100);
}

5.3 语言包格式 src/i 18 n/locales/zh-CN.ts

export const zhCN = {
  // ===== 按钮、标签等短文本 =====
  "取消": "取消",
  "保存": "保存",
  "删除": "删除",
  "添加订阅源": "添加订阅源",
  "刷新订阅源s": "刷新订阅源",
  "Settings": "设置",
  
  // ===== 设置页面 Tab 标签 =====
  "通用": "通用",
  "显示": "显示",
  "媒体": "媒体",
  "文章保存": "文章保存",
  "规则": "规则",
  "高亮": "高亮",
  "导入/导出": "导入/导出",
  "标签": "标签",
  "关于": "关于",
  
  // ===== 长文本描述 =====
  "在列表、卡片和订阅源视图之间选择文章显示方式": "在列表、卡片和订阅源视图之间选择文章显示方式",
  // ... 继续添加 400+ 条翻译
};

六、修改主入口文件

6.1 在 main.ts 中添加导入

// 在文件顶部添加
import { t, applyTranslationsToDOM, setLocale } from "./src/i 18 n";

6.2 在 onload() 方法中初始化翻译

async onload() {
  // ===== i 18 n: 初始化中文翻译 =====
  setLocale("zh-CN");

  // 监听 DOM 变化自动翻译
  this.i 18 nObserver = new MutationObserver(() => {
    applyTranslationsToDOM();
  });
  this.i 18 nObserver.observe(document.body, {
    childList: true,
    subtree: true,
    characterData: true,
  });

  // 初始应用翻译(等待界面渲染完成)
  this.app.workspace.onLayoutReady(() => {
    setTimeout(() => applyTranslationsToDOM(), 150);
  });
  // ===== i 18 n 初始化结束 =====

  // ... 原有代码继续
}

6.3 在 onunload() 中清理 observer

onunload() {
  // 清理 i 18 n observer
  if (this.i 18 nObserver) {
    this.i 18 nObserver.disconnect();
    this.i 18 nObserver = null;
  }
  // ... 原有代码继续
}

6.4 将硬编码文本替换为 t() 函数调用

// 修改前
new Notice("RSS Dashboard restored to factory defaults.");

// 修改后
new Notice(t("RSS Dashboard restored to factory defaults."));

七、构建与测试

7.1 安装依赖

npm install

7.2 构建插件

npm run build

成功输出:

CSS scoping check passed (18 file(s)).
Build complete: main.js and styles.css have been updated.

7.3 常见构建错误及解决

错误原因解决方案
TS 1117: An object literal cannot have multiple properties with the same namezh-CN.ts 中有重复的键删除重复项,确保每个英文原文只出现一次
TS 2554: Expected 1-3 arguments, but got 4document.createTreeWalker 参数过多删除第 4 个参数 false
Cannot find module './src/i 18 n'文件路径不正确确认 src/i 18 n/index.ts 存在
ESLint 检查 extract-strings.js临时脚本被 ESLint 检查删除 extract-strings.js 或添加到 .eslintignore

7.4 安装到 Obsidian

将整个项目文件夹复制到 Obsidian 插件目录:

你的仓库/.obsidian/plugins/obsidian-rss-dashboard/

然后在 Obsidian 设置中启用插件。


八、翻译更新维护

8.1 更新策略

更新方式翻译是否保留说明
Obsidian 市场更新❌ 丢失会删除整个文件夹再下载新文件
GitHub 手动下载覆盖✅ 保留只覆盖文件,不删除
Git pull✅ 保留合并更新,保留修改

8.2 推荐工作流

# 1. 从 GitHub 拉取最新代码(保留你的翻译)
git pull

# 2. 如果有新依赖
npm install

# 3. 重新构建
npm run build

8.3 合并冲突处理

你的修改集中在:

  • src/i 18 n/locales/zh-CN.ts(新建文件,原作者没有)
  • src/i 18 n/index.ts(新建文件,原作者没有)
  • main.ts(修改过,可能冲突)

如果 main.ts 有冲突,需要手动合并,但翻译文件本身不会冲突。


九、已知限制与注意事项

9.1 翻译覆盖范围

已翻译未翻译/部分翻译
所有按钮文字部分设置项详细描述
所有菜单项部分通知消息
所有弹窗标题动态生成的文本(如 ${count} items
所有 Tab 标签插件内部调试日志
设置项名称(标题)Obsidian 原生界面文本
仪表盘 主界面部分插件内部服务类输出

9.2 中英文混合问题

如果出现同一个元素中英文混合显示,通常是:

  1. DOM 替换时部分匹配导致(replace() 只替换了部分)
  2. 动态生成的文本没有被翻译函数覆盖
  3. 翻译函数执行时机早于 DOM 渲染完成

解决方法:在 applyTranslationsToDOM() 中优先使用精确匹配而非部分匹配。


十、给作者提 PR(推荐)

如果你希望官方支持中文,可以向作者提交 Pull Request:

  1. Fork 仓库到你的 GitHub
  2. src/i 18 n/ 目录和修改后的 main.ts 推送到你的 Fork
  3. 提交 Pull Request,描述中文化的工作

优点

  • 所有用户都能受益
  • 以后官方更新自动包含中文
  • 不需要自己维护翻译分支

十一、总结

本教程完整记录了 RSS 仪表盘 插件中文化的全过程:

  1. 提取文本:使用正则脚本扫描源代码
  2. 创建 i 18 n 架构:新建翻译引擎和语言包
  3. 修改主入口:在插件启动时加载翻译
  4. 构建测试:编译并在 Obsidian 中验证
  5. 后续维护:手动更新或提 PR

整个过程中最大的挑战是文本提取的完整性——由于代码分散在 200+ 个文件中,不可能一次性提取所有界面文本。建议在实际使用中发现遗漏即补充,逐步完善语言包。


十二、参考资源

资源链接
RSS 仪表盘 项目https://github.com/amatya-aditya/obsidian-rss-dashboard
Obsidian 插件开发文档https://docs.obsidian.md/
类型Script 中文手册https://www.typescriptlang.org/zh/
Node.js 官网https://nodejs.org/

本文档基于实际中文化操作经验整理,版本信息:RSS 仪表盘 v 2.3.0,Obsidian v 1.7.2+

😊
提交