Joplin 桌面版 TinyMCE 语言包机制:从 Assets/TinyMCE/langs 到富文本编辑器本地化的完整构建与加载流程
【免费下载链接】joplinJoplin - the privacy-focused note taking app with sync capabilities for Windows, macOS, Linux, Android and iOS.项目地址: https://gitcode.com/GitHub_Trending/jo/joplin
Joplin 桌面版(Electron 应用)内置了基于 TinyMCE 的富文本编辑器,其界面文案(工具栏按钮、菜单、对话框)的多语言支持依赖一套独立的 TinyMCE 语言包(language packs)。本文以仓库中的 Assets/TinyMCE/langs/README.md 为核心,结合桌面版构建脚本与编辑器初始化源码,讲清这些语言包的存放位置、构建期拷贝流程(gulp 任务copyApplicationAssets)以及运行时的按需加载机制,读完后可完整掌握 Joplin 富文本编辑器本地化的原理与实操方式。
语言包是什么,为什么单独维护
Assets/TinyMCE/langs/README.md 的原文只有三行,但它描述了 Joplin 富文本编辑器本地化链路的起点:
- 语言包来自 TinyMCE 官方的语言包下载页面(Tiny Language Packages);
- 语言包需要被放置到
node_modules/tinymce目录才能被编辑器识别; - 这一步由 Electron 客户端(桌面版)的 gulpfile 自动完成。
这里的“语言包”指 TinyMCE 编辑器自身的 UI 翻译文件——每个语言对应一个独立的.js文件(例如de.js之于德语、ja.js之于日语),文件内以tinymce.addI18n('de', {...})的形式注册该语言的词条。注意它和 Joplin 应用本身的翻译体系是分开的:应用级翻译由 packages/tools/locales 下的.po文件管理,而 TinyMCE 编辑器内部的工具栏提示、对话框文案则使用这一套语言包。
当前仓库中Assets/TinyMCE/langs/目录下实际存放了约 50 个语言包文件,覆盖范围包括:ar.js、de.js、fr_FR.js、ja.js、ko_KR.js、ru.js、es_MX.js、zh_CN.js、zh_TW.js等(完整列表可直接查看 Assets/TinyMCE/langs/ 目录)。也就是说,仓库把语言包“vendoring”到了Assets/目录中,保证构建时不再依赖外部网络下载。
构建期:gulp 任务 copyApplicationAssets 的拷贝流程
README 提到“由 ElectronClient 的 gulpfile 完成拷贝”。在代码库演进的现状中,这一职责由桌面版的 gulp 任务copyApplicationAssets承担。任务定义见 packages/app-desktop/gulpfile.ts:
copyApplicationAssets: { fn: require('./tools/copyApplicationAssets.js'), ... }其实现位于 packages/app-desktop/tools/copyApplicationAssets.js,核心逻辑分三步:
1. 定位语言包源目录与目标目录
const langSourceDir = resolve(__dirname, '../../../Assets/TinyMCE/langs'); const buildLibDir = resolve(__dirname, '../vendor/lib');拷贝清单dirs中显式声明了语言包这一项(copyApplicationAssets.js#L67-L80):
const dirs = [ 'tinymce', // node_modules/tinymce -> vendor/lib/tinymce '@fortawesome/fontawesome-free/webfonts', 'roboto-fontface/fonts', 'codemirror/theme', { src: langSourceDir, // Assets/TinyMCE/langs dest: `${buildLibDir}/tinymce/langs`, // packages/app-desktop/vendor/lib/tinymce/langs }, { src: `${nodeModulesDir}/tesseract.js-core`, dest: `${buildDir}/tesseract.js-core`, }, ];可以看到,node_modules/tinymce整个目录(包含tinymce.min.js核心)被拷贝到vendor/lib/tinymce,而仓库Assets/TinyMCE/langs/中的语言包则被合并拷贝到vendor/lib/tinymce/langs/——这正是 README 所说“拷贝到 node_modules/tinymce 目录”这一意图在当前构建体系中的落地形态(桌面版最终把vendor/打包进应用分发的vendorDir(),而非运行时直接读node_modules)。
2. 针对 CI 不稳定的重试机制
值得注意的是,源码注释记录了该脚本在 CI 上出现过随机性失败(ENOENT: copyfile、ENOTEMPTY: directory not empty等看似不可能的错误,见 copyApplicationAssets.js#L22-L44)。为此脚本为每个文件操作实现了指数退避重试(最多 5 次):
const withRetry = async (fn) => { for (let i = 0; i < 5; i++) { try { await fn(); return; } catch (error) { console.warn(`withRetry: Failed calling function - will retry (${i})`, error); await msleep(1000 + i * 1000); } } throw new Error('withRetry: Could not run function after multiple attempts'); };此外,脚本先统一删除所有目标目录、再统一执行拷贝(for (const action of ['delete', 'copy']),见 copyApplicationAssets.js#L109-L131),注释说明这样做是为了规避“删除后立即拷贝”时的竞态条件。
3. 自动生成 supportedLocales.js 清单
拷贝完成后,脚本扫描语言包目录生成一份“受支持语言清单”文件(copyApplicationAssets.js#L150-L159):
const supportedLocales = glob.sync(`${langSourceDir}/*.js`).map(s => { s = basename(s).split('.'); return s[0]; }); supportedLocales.sort(); const content = `module.exports = ${JSON.stringify(supportedLocales, null, 2)}`; await writeFile(`${__dirname}/../gui/NoteEditor/NoteBody/TinyMCE/supportedLocales.js`, content, 'utf8');即:Assets/TinyMCE/langs/里放了哪些语言文件,supportedLocales数组就包含哪些语言代码,输出产物是 packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/supportedLocales.js(构建时生成的文件)。这意味着“新增一个语言包”不需要改任何代码——把对应.js放进Assets/TinyMCE/langs/重新构建即可被运行时识别。
运行期:编辑器如何按需加载语言包
桌面版富文本编辑器的初始化入口在 packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/TinyMCE.tsx。运行时加载语言包的完整调用链如下:
第一步:从受支持清单中解析出最接近的语言代码
const supportedLocales = require('./supportedLocales'); ... const language = closestSupportedLocale(props.locale, true, supportedLocales);closestSupportedLocale定义在 packages/lib/locale.ts,它接受用户当前的应用 locale、defaultToEnglish = true标志以及 TinyMCE 语言包清单,返回一个 TinyMCE 实际具备的语言代码。其单测 packages/lib/locale.test.ts 覆盖了多种 locale 归并场景(如区域变体回退到语言主干)。
第二步:非英语语言才动态注入语言包脚本
const isDefaultEnglishLocale = ['en_US', 'en_GB'].includes(language); if (!isDefaultEnglishLocale) { await loadScript({ id: `tinyMceLang_${language}`, src: `${bridge().vendorDir()}/lib/tinymce/langs/${language}.js`, }, editorContainerDom); }(见 TinyMCE.tsx#L710-L717)
这里有两个设计要点:
- 脚本从
bridge().vendorDir()下的lib/tinymce/langs/加载,路径与构建期copyApplicationAssets的拷贝目标一一对应; - 英语(
en_US/en_GB)是 TinyMCE 的默认语言,无需加载额外脚本,直接跳过注入。
第三步:把语言传给 tinymce.init
language: isDefaultEnglishLocale ? undefined : language,(见 TinyMCE.tsx#L749)
tinymce.init的完整配置中还有若干与本地化/编辑器行为相关的选项,例如localization_function: _(接入 Joplin 自身的翻译函数)、icons: 'Joplin'与icons_url(自定义图标集,对应 packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/icons.js)、branding: false与promotion: false(去掉 TinyMCE 品牌标识与付费推广)。当前锁定的编辑器版本为tinymce: 6.8.5(见 packages/app-desktop/package.json),语言包格式与该版本的tinymce.addI18n约定匹配。
实操:如何查看、新增或更新一个语言包
基于上述源码链路,围绕语言包的实际操作路径如下(均以查看仓库为准,仓库为只读参考):
- 查看现有语言支持范围:直接浏览 Assets/TinyMCE/langs/,文件名即 TinyMCE 语言代码(
bg_BG.js、he_IL.js、ta_IN.js等)。 - 新增语言:从 TinyMCE 官方语言包页面下载对应的
<lang>.js,放入Assets/TinyMCE/langs/目录。构建时copyApplicationAssets会自动将其拷入vendor/lib/tinymce/langs/,并自动更新supportedLocales.js清单;当用户应用语言对应该 locale 时,closestSupportedLocale即可命中并加载。 - 更新 TinyMCE 或语言包版本:
Assets/TinyMCE/langs/中的语言包必须与node_modules中的 tinymce 版本配套(见 packages/app-desktop/package.json 中锁定的tinymce版本),更换大版本时应整包重新获取,避免旧格式文件与新版addI18n约定不兼容。 - 验证构建产物:构建后检查
packages/app-desktop/vendor/lib/tinymce/langs/是否包含预期文件(vendor/为构建输出,不入库)。
小结
Assets/TinyMCE/langs/README.md 虽仅三行,却勾勒出 Joplin 桌面版富文本编辑器本地化的完整契约:语言包由 TinyMCE 官方获取并 vendoring 至Assets/TinyMCE/langs/,构建期由 gulpfile.ts 中的copyApplicationAssets任务拷入vendor/lib/tinymce/langs/并顺带生成supportedLocales.js清单,运行期由 TinyMCE.tsx 通过closestSupportedLocale解析语言、非英语场景按需注入脚本、最终经tinymce.init的language选项生效。理解这条“资产 → 构建 → 运行时”的单向流水线,即可准确解释 Joplin 富文本编辑器各语言界面文案的来源与扩展方式。
【免费下载链接】joplinJoplin - the privacy-focused note taking app with sync capabilities for Windows, macOS, Linux, Android and iOS.项目地址: https://gitcode.com/GitHub_Trending/jo/joplin
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考