news 2026/9/20 23:57:38

Sails 动态内容国际化(Translating Dynamic Content)实战指南:从 JSON stringfile 到数据库驱动的多语言方案

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Sails 动态内容国际化(Translating Dynamic Content)实战指南:从 JSON stringfile 到数据库驱动的多语言方案

Sails 动态内容国际化(Translating Dynamic Content)实战指南:从 JSON stringfile 到数据库驱动的多语言方案

【免费下载链接】sailsRealtime MVC Framework for Node.js项目地址: https://gitcode.com/gh_mirrors/sa/sails

导读

当你的 Sails 应用需要翻译的不只是页面上的固定文案,而是存储在数据库里的"跨语言业务数据"(例如通过 CMS 以多种语言录入的商品数据、文章正文、用户生成内容)时,传统的config/locales/*.json静态 stringfile 便不再够用。本篇基于 Sails 官方文档 TranslatingDynamicContent.md 展开,结合仓库中 i18n hook 源码 与 集成测试,系统讲解两条落地路线:以编程方式动态编辑 locale 翻译文件,以及将翻译内容存入数据库、按 locale id 建模读取。读完本文,你将掌握req.getLocale()req.setLocale()sails.__的正确用法,并能够为"CMS 多语言录入 + Sails 后端渲染"这类典型场景设计出与项目既有国际化约定保持一致的数据模型与查询方案。

一、先厘清边界:静态 stringfile 与动态内容的本质区别

Sails 内置的国际化和本地化支持(自 v1 起基于轻量级i18n-2包实现,见 Internationalization.md)面向的是静态词句:即那些预先确定、数量有限、可以在config/locales/目录下以 JSON 键值对形式维护的文案。i18n hook 在初始化时会把整个 locales 目录加载进内存,之后在每次请求中通过expressMiddleware把翻译能力注入res.locals(参见 i18n/index.js)。

然而,如果你的后端正在存储跨语言数据(interlingual data)——例如商品在多个语言下都有独立描述、用户在不同语言下提交了不同版本的内容——此时每个语种的字符串数量可能达到数万条,且会持续动态增长。继续依赖简单的 JSON locale 文件有两个明显障碍:

  1. 规模与维护成本失控:stringfile 会膨胀到难以人工维护,且每次新增内容都要同步修改、重新部署。
  2. 编辑时机错位:除非你有办法在运行时动态编辑 locale 翻译(并且愿意承担并发写文件的风险),否则静态文件方案难以承载 CMS 的多语言录入流程。

原文档给出的判断非常清晰:只有当你能以某种方式动态编辑locale 翻译时,JSON stringfile 才值得继续沿用;否则就应该把动态翻译字符串放进数据库。下面逐一展开这两条路线。

二、路线一:以编程方式动态编辑 locale 翻译(JSON stringfile)

2.1 适用前提:具备编程式编辑能力

原文档明确指出,如果你的后端存储的是跨语言数据,就不应依赖简单 JSON locale 文件——除非你计划以编程方式动态编辑这些翻译。也就是说,你可以在运行时:

  • 通过自研实现直接读写 stringfile(fs读写 JSON,注意加锁/原子写入);
  • 或对接第三方翻译服务,将服务返回的翻译批量写回 stringfile。

Sails / node-i18n 的 JSON stringfile 与 webtranslateit.com 等翻译平台使用的格式是兼容的,这意味着你可以把config/locales/下的文件直接交给这类翻译服务做人工或机器翻译后回写,无需转换格式。Sails 的 stringfile 就是最简单的 JSON 键值对,例如仓库测试夹具 config/locales/es.json:

{ "hello": "Hola" }

2.2 stringfile 的格式约束与注意点

要编程式编辑 stringfile,必须遵守 Locales.md 中定义的文件约定:

  • 每个文件对应一个 locale,文件名即语言标识,例如config/locales/es.jsonconfig/locales/de.json
  • 键与值都是字符串,键区分大小写且必须精确匹配'Hello''hello'是两个不同的键);
  • 支持占位符,例如"Hello %s, how are you today?",翻译时%s会被运行时传入的参数替换;
  • 需要表达嵌套结构时,在键中使用.,例如"editProfile.username.label": "Username"
  • 键的命名风格取决于谁来维护 stringfile:如果是人工手工编辑,统一小写短键(如hellohowAreYouToday)通常比长英文句子更利于维护。

2.3 编程式写入示例:一个可复用的 helper

下面是一个把新翻译写回 stringfile 的可运行示例(可作为 Sails 自定义 helper 使用,通过 action 触发后由 CMS 后台调用):

// api/helpers/update-locale-file.js const fs = require('fs'); const path = require('path'); module.exports = { friendlyName: 'Update locale file', description: 'Programmatically write a translated string into a Sails locale stringfile.', inputs: { locale: { type: 'string', required: true, description: 'e.g. "es"' }, key: { type: 'string', required: true, description: 'Translation key, e.g. "Welcome"' }, value: { type: 'string', required: true, description: 'Translated string' } }, fn: async function ({ locale, key, value }) { const filePath = path.resolve(sails.config.appPath, sails.config.i18n.localesDirectory, locale + '.json'); // 读入现有翻译(文件不存在则视为空对象) let translations = {}; if (fs.existsSync(filePath)) { translations = JSON.parse(fs.readFileSync(filePath, 'utf8')); } // 写入新键并原子回写 translations[key] = value; fs.writeFileSync(filePath, JSON.stringify(translations, null, 2) + '\n'); return { filePath, key, value }; } };

需要注意两点:

  1. 运行时缓存:i18n hook 在initialize阶段通过new i18nFactory({...})一次性加载 locales 目录(见 i18n/index.js),因此直接改文件后,当前进程内的翻译缓存不会自动刷新。写入后应重新读取文件(如新开请求用req.i18n实例已重新构造),或在写完后触发应用重启/热重载。
  2. i18n-2 的自动补写行为:仓库集成测试 hook.i18n.test.js 验证了一个容易被忽视的行为——调用sailsApp.__('Login')后,config/locales/de.json会被自动补写缺失的键('Login': 'Login')。这是 i18n-2 的 "update files" 特性,意味着 stringfile 可能在请求处理过程中被后台改动。如果你的数据模型不允许翻译文件被隐式修改,应当通过配置关闭该行为,或在文件系统层面做好并发保护。

三、路线二:把动态翻译内容存进数据库

这是原文档推荐的另一条路线,也是承载 CMS 多语言数据的主流方案。核心思路:不要把"翻译"和"被翻译的业务实体"混在同一个字段里,而是围绕 locale id 设计数据模型,让每条翻译记录都能按语言标识("en""es""de"…)被独立存储与检索。

3.1 按 locale id 建模:一对多翻译模型

以商品为例,业务实体与翻译分离,一个商品对应多语言的多条翻译记录:

// api/models/Product.js module.exports = { attributes: { sku: { type: 'string', required: true, unique: true }, translations: { collection: 'producttranslation', via: 'product' } } };
// api/models/ProductTranslation.js module.exports = { attributes: { // locale id 即语言标识,例如 'en' / 'es' / 'de' locale: { type: 'string', required: true, isIn: ['en', 'es', 'de', 'fr'] }, name: { type: 'string', required: true }, description: { type: 'string' }, product: { model: 'product' } } };

这种"实体 + 按 locale 展开的翻译子表"结构有两点收益:

  • 与 Sails 的国际化约定保持一致:locale id 直接复用 stringfile 的命名约定(小写、BCP 47 风格),前端与后端、静态文案与动态内容共享同一套语言标识,语义统一;
  • 检索直观:拿到当前请求的语言后,按locale字段过滤即可,必要时再补充一个默认语言回退(fallback)。

3.2 用 req.getLocale() 决定"返回哪份翻译"

原文档特别强调:借助req.getLocale()方法判断当前请求应使用哪份翻译内容,从而"keep consistent with the conventions used elsewhere in your app"(与应用中其他地方使用的约定保持一致)。

req.getLocale()由 i18n hook 的expressMiddleware在每个请求上绑定(源码见 i18n/index.js):hook 会基于请求头为当前请求新建一个i18n-2实例,并把getLocalesetLocale挂到req上。默认情况下,它读取的是请求的Accept-Language头——即用户浏览器/设备的语言设置。

仓库集成测试 hook.i18n.test.js 验证了这一行为:对/test_req_getlocale路由发起带Accept-language: es头的请求,req.getLocale()返回'es'

在 action 中组合使用:

// api/controllers/product/view-product.js const _ = require('@sailshq/lodash'); module.exports = { friendlyName: 'View product', exits: { notFound: { responseType: 'notFound' } }, fn: async function () { const product = await Product.findOne({ sku: this.req.param('sku') }) .populate('translations'); if (!product) { throw 'notFound'; } // 用当前请求的语言标识挑选翻译 const locale = this.req.getLocale(); let translation = _.find(product.translations, { locale: locale }); // 找不到时回退到默认语言(例如 'en'),保证任何语言请求都有兜底内容 if (!translation) { translation = _.find(product.translations, { locale: 'en' }) || {}; } return { product: product, translation: translation }; } };

在视图中,动态数据同样以响应 locals 的形式输出,而静态页面文案继续使用__()/i18n()走 stringfile——两者互不干扰:

<h1><%= translation.name %></h1> <p><%= translation.description %></p> <p><%= __('Add to cart') %></p> <!-- 静态文案仍走 stringfile -->

3.3 用户语言偏好:req.setLocale() 与动态内容的配合

默认的语言检测对"跟随用户偏好切换语言"的场景并不够用——用户可能希望手动选择语言,而这个偏好存在 session 或数据库账号里。此时用req.setLocale()覆盖自动检测结果即可。

官方文档 req.setLocale.md 给出的典型用法是:登录用户在 action 入口处,把其账号保存的语言偏好写进本次请求:

// 基于 actions2 的写法 if (this.req.me.preferredLocale) { this.req.setLocale(this.req.me.preferredLocale); } return exits.success(); // 传统写法(非 Web app 模板 / 非 actions2) var me = await User.findOne({ id: req.session.userId }); if (me.preferredLocale) { req.setLocale(me.preferredLocale); } return res.view('pages/homepage');

测试 hook.i18n.test.js 证明了它的生效链路:路由中执行req.setLocale('es')后,req.i18n.__('Welcome')返回'Bienvenido'。这意味着你在setLocale之后再调用req.getLocale()req.i18n.__(),都会以新 locale 为准——先覆盖语言偏好,再查询数据库翻译,两步配合即可实现"用户语言偏好 + 动态多语言内容"的完整闭环

fn: async function () { // 1. 用户偏好优先(session 或数据库账号字段) if (this.req.me && this.req.me.preferredLocale) { this.req.setLocale(this.req.me.preferredLocale); } // 2. 此刻 getLocale() 返回的已是最终生效的语言 const locale = this.req.getLocale(); // 3. 按该语言读取动态翻译…… }

四、配置与约束:sails.config.i18n 关键参数

无论走哪条路线,动态内容的语言标识体系都与 sails.config.i18n 的配置紧密相关。该配置按约定放在config/i18n.js,核心属性如下(对照 i18n hook 源码 defaults 与configure校验逻辑 L44-L60):

属性类型默认值说明
localesarray['en','es','fr','de']应用支持的 locale 列表。注意:配置值与对应翻译文件名必须全小写。hook 的configure阶段会强制校验其必须是字符串数组,否则直接抛错
localesDirectorystring'config/locales/'stringfile 所在目录的应用相对路径,也支持绝对路径;同样会被校验必须是字符串
defaultLocalestring'en'站点默认语言。对发送Accept-Language头的请求(绝大多数浏览器)会被覆盖;但对非浏览器客户端(移动设备、IoT、cURL、Postman 等)仍很有用

示例(仓库测试 hook.i18n.test.js 中真实使用的配置):

// config/i18n.js module.exports.i18n = { locales: ['en', 'de'], defaultLocale: 'de' };

两个值得注意的边界行为(均有源码/测试依据):

  • defaultLocale 与 locales 顺序:hook 在configure阶段会把defaultLocale移动到列表顶部,以规避 i18n-2 的一个已知问题(见 i18n/index.js);
  • 禁用 i18n hook 的兜底行为:当sails.config.i18n.locales为空数组时,hook 会自我禁用,并把sails.__sails.i18n替换为"原样返回输入字符串并打印警告"的透传函数(见 i18n/index.js)。如果应用走数据库翻译路线且不需要 stringfile,可通过loadHooks/hooks配置彻底移除 i18n hook(详见 Internationalization.md 的 "Disabling or customizing" 一节)。

另外,若采用路线一(编程式编辑 stringfile),defaultLocale还关系到sails.__在非请求上下文中(如 shell-scripts 命令行脚本)使用的语言:sails.__('Welcome')默认按defaultLocale翻译,例如测试中默认 locale 为'de'时返回'Willkommen'(见 hook.i18n.test.js)。

五、两条路线的选型建议

考量维度路线一:编程式编辑 stringfile路线二:数据库存储翻译
数据规模适合文案量有限、变更不频繁适合 CMS 多语言录入、海量动态内容
与既有约定的兼容直接复用__()/i18n()体系,零改造成本需自行建模,但 locale id 约定与 stringfile 一致
更新实时性受 i18n 内存缓存与自动补写行为影响,需处理刷新随数据库事务即时生效,天然适合多写并发
维护主体翻译服务 / 后台脚本批量回写业务用户通过 CMS 直接编辑
典型场景少数固定页面文案 + 外部翻译平台协作电商商品、多语言文章、UGC 内容

无论选择哪条路线,请始终遵循原文档的核心原则:让动态翻译数据与应用的国际化约定对齐——语言标识统一使用小写 locale id(enesde…),并借助req.getLocale()/req.setLocale()在请求生命周期内确定并覆盖生效语言。这样,静态 stringfile 与数据库动态内容可以无缝共存,前端渲染、后端接口、shell 脚本(sails.__)三条路径对"当前语言"的理解保持一致。更多关于 stringfile 编写规范与语言检测细节,可继续阅读 Locales.md;整体国际化机制与视图用法见 Internationalization.md。

【免费下载链接】sailsRealtime MVC Framework for Node.js项目地址: https://gitcode.com/gh_mirrors/sa/sails

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/20 23:56:54

C++和OpenCV车牌识别实战:从定位到识别的完整流程

简介&#xff1a;C结合OpenCV实现的车牌识别系统&#xff0c;是一份面向计算机视觉初学者与智能交通项目开发者的完整工程资料&#xff0c;适用于高速公路收费、停车场管理、城市交通监控等场景的学习与原型验证。系统贯穿图像采集、预处理、车牌定位、字符分割、字符识别全流程…

作者头像 李华
网站建设 2026/9/20 23:56:41

大华Java SDK迁移SpringBoot完整实践:从库加载到设备管理

1. 迁移前的整体判断与方案选型 1.1 大华Java SDK到底是个什么东西 先聊一个基本认知问题。大华官方提供的Java SDK&#xff0c;表面上看是一堆 .jar 包加几个 .dll 或 .so 文件&#xff0c;但它的核心底层其实是C实现的native库&#xff0c;Java层通过JNA技术去调用。…

作者头像 李华
网站建设 2026/9/20 23:56:33

PostHog TMDB 数据源 API 盘点:从认证、分页到限流的接入全解

PostHog TMDB 数据源 API 盘点&#xff1a;从认证、分页到限流的接入全解 【免费下载链接】posthog :hedgehog: PostHog is the leading platform for building self-driving products. Our developer tools – AI observability, analytics, session replay, flags, experimen…

作者头像 李华
网站建设 2026/9/20 23:55:46

Claude Code 不走 Anthropic API,改走 TaoToken 行不行

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/20 23:53:45

Zephyr 在 PHYTEC phyBOARD-Lyra AM62x A53 上的移植与实战指南

Zephyr 在 PHYTEC phyBOARD-Lyra AM62x A53 上的移植与实战指南 【免费下载链接】zephyr Primary Git Repository for the Zephyr Project. Zephyr is a new generation, scalable, optimized, secure RTOS for multiple hardware architectures. 项目地址: https://gitcode.…

作者头像 李华
网站建设 2026/9/20 23:53:35

Biome 与 Prettier 兼容性挑战报告深度解读:96%+ 相似度的背后

Biome 与 Prettier 兼容性挑战报告深度解读&#xff1a;96% 相似度的背后 【免费下载链接】biome A toolchain for web projects, aimed to provide functionalities to maintain them. Biome offers formatter and linter, usable via CLI and LSP. 项目地址: https://gitco…

作者头像 李华