Obsidian RSS 仪表盘 插件中文化完整教程
一、背景与目标
RSS 仪表盘 是一个功能强大的 Obsidian 插件,支持 RSS、Atom、JSON Feed、YouTube、播客等多种订阅源。但官方没有提供中文语言包,对于中文用户来说使用体验不够友好。
本文档记录了完整的插件中文化过程,包括:翻译文本提取、语言包创建、代码修改、构建测试、以及后续维护策略。适合有一定动手能力的 Obsidian 用户参考。
二、准备工作
2.1 环境要求
| 工具 | 说明 |
|---|---|
| Node.js | 用于运行构建脚本(推荐 LTS 版本) |
| npm | Node.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 文件中的英文。为了实现中文化,我们需要:
- 新建 i 18 n 架构:创建翻译文件和翻译引擎
- 提取所有界面文本:从源代码中提取需要翻译的英文
- 翻译并整合:将中文翻译添加到语言包
- 修改主入口文件:在插件启动时加载翻译
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.json5.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 install7.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 name | zh-CN.ts 中有重复的键 | 删除重复项,确保每个英文原文只出现一次 |
TS 2554: Expected 1-3 arguments, but got 4 | document.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 build8.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 中英文混合问题
如果出现同一个元素中英文混合显示,通常是:
- DOM 替换时部分匹配导致(
replace()只替换了部分) - 动态生成的文本没有被翻译函数覆盖
- 翻译函数执行时机早于 DOM 渲染完成
解决方法:在 applyTranslationsToDOM() 中优先使用精确匹配而非部分匹配。
十、给作者提 PR(推荐)
如果你希望官方支持中文,可以向作者提交 Pull Request:
- Fork 仓库到你的 GitHub
- 将
src/i 18 n/目录和修改后的main.ts推送到你的 Fork - 提交 Pull Request,描述中文化的工作
优点:
- 所有用户都能受益
- 以后官方更新自动包含中文
- 不需要自己维护翻译分支
十一、总结
本教程完整记录了 RSS 仪表盘 插件中文化的全过程:
- 提取文本:使用正则脚本扫描源代码
- 创建 i 18 n 架构:新建翻译引擎和语言包
- 修改主入口:在插件启动时加载翻译
- 构建测试:编译并在 Obsidian 中验证
- 后续维护:手动更新或提 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+