functions-samples 实战:基于 Google Translate API 的 Realtime Database 自动消息翻译
functions-samples 实战:基于 Google Translate API 的 Realtime Database 自动消息翻译
导读
在 Firebase 应用中,用户产生的一条消息往往需要同时呈现在多个语言的客户端中。本示例(位于 Node-1st-gen/message-translation)演示了如何借助 Cloud Functions for Firebase 与 Google Translate API,在消息写入 Realtime Database 后自动将其翻译成多个目标语言,并以「语言代码 + 消息 ID」的扇出结构写回数据库。读完本文你将掌握:Realtime Database onWrite 触发器的监听路径设计、翻译结果的扇出存储模式、基于 translated 标志的幂等保护,以及该示例完整的部署与本地运行方式。
一、示例定位:一次写入,多语言自动扩散
该示例是 functions-samples 仓库中「解决其他常见场景」类目下的经典用例,仓库根目录 README.md 对它的定位是:
Integrates the Google Translate API to perform automatic text translation across any number of languages.
其核心思路可以用一句话概括:当任意语言的消息写入 Realtime Database 后,函数自动将其翻译为预定义的目标语言列表中的每一种语言,并写入对应的语言节点。它属于 Node 第 1 代(1st gen)示例,使用 Realtime Database 触发器,不依赖任何 HTTP 端点,完全由数据库写入事件驱动。
从仓库结构看,该示例由四个文件构成:
| 文件 | 作用 |
|---|---|
| functions/index.js | 触发器函数主体,完成翻译与写回逻辑 |
| functions/package.json | 依赖声明与 npm 脚本 |
| functions/eslint.config.js | ESLint 扁平配置 |
| firebase.json | Firebase 函数代码库(codebase)配置 |
二、核心设计:以语言代码为 key 的扇出结构
原 README 明确说明了数据的组织方式:翻译结果会被写入一个 以语言代码为 key 的扇出(fanned out)结构 中。所谓扇出,就是把同一份消息内容按语言维度复制到多个独立节点,使客户端只需按自己的语言读取对应路径,无需在服务端做语言筛选。
2.1 消息写入前的数据库结构
假设项目名为 functions-project-12345,示例采用的初始数据结构如下:
/functions-project-12345
/messages
/en
/key-123456
translated: false
text: "Hey Bob! How Are you?"
/key-123457
translated: false
text: "Hey Mat! How Are you?"
即 messages 下第一层是语言代码(en),第二层是消息 ID(key-123456),每条消息包含一个 translated 布尔标志与消息正文。
2.2 翻译完成后的数据库结构
当函数检测到新消息并完成翻译后,数据被扩散为多语言形态:
/functions-project-12345
/messages
/en
/key-123456
translated: true
text: "Hey Bob! How Are you?"
/key-123457
translated: false
text: "Hey Mat! How Are you?"
/fr
/key-123456
translated: true
text: "Salut Bob! Comment ca va?"
可以看到:
- 原始语言节点(
en)中的消息translated被置为true; - 新生成的法语节点(
fr)下出现了**与源消息相同 key(key-123456)**的翻译结果; - 尚未翻译的消息(
key-123457)保持不变,等待下一次写入触发。
这种「同一 key、多语言父节点」的结构,让客户端可以稳定地通过 messages/{lang}/{messageID} 取到正确语言的内容。
注意:README 中的数据示例使用
text字段,而实际代码(见下文第三节)读写的是message字段。两者存在细微出入,实战部署时请以源码实现为准,写入message字段即可被正确触发与翻译。
三、函数实现逐段解析
原 README 指出核心代码位于 functions/index.js。下面结合源码逐段拆解实现原理。
3.1 依赖引入与客户端初始化
源码 index.js#L18-L24:
const functions = require('firebase-functions/v1');
const { initializeApp } = require('firebase-admin/app');
const { getDatabase } = require('firebase-admin/database');
initializeApp();
const { TranslationServiceClient } = require('@google-cloud/translate');
const translate = new TranslationServiceClient();
关键点:
- 显式引入
firebase-functions/v1:这确认了本示例运行在 Cloud Functions 第 1 代运行时上; initializeApp()无参数调用:在 Firebase 托管环境中,管理员 SDK 会自动读取部署时注入的默认凭据与服务配置;TranslationServiceClient:来自@google-cloud/translate包,是 Cloud Translation API(v3 协议)的 Node.js 客户端,translate实例在模块顶层创建、全局复用。
3.2 目标语言列表
源码 index.js#L26-L27:
// List of output languages.
const LANGUAGES = ['en', 'es', 'de', 'fr', 'sv', 'ga', 'it', 'jp'];
示例预定义了 8 个目标语言:英语、西班牙语、德语、法语、瑞典语、爱尔兰语、意大利语、日语。这段数组即「翻译扇出」的目标维度——凡是写入的源语言不属于该数组,就会向数组内每一个语言节点扩散。
值得说明的是,仓库根 README 对该类用例的补充描述提到「Language codes can be stored in Firebase for on the fly changes」,即语言代码可存入数据库实现运行期动态调整;而当前示例采用硬编码数组,属于该思路的简化实现。你可以在此基础上将 LANGUAGES 改为从 Realtime Database 读取,即可在不重新部署的前提下增删目标语言。
3.3 触发器路径与幂等保护
源码 index.js#L30-L35:
exports.translate = functions.database.ref('/messages/{languageID}/{messageID}').onWrite(
(change, context) => {
const snapshot = change.after;
if (snapshot.val().translated) {
return null;
}
- 监听路径:
/messages/{languageID}/{messageID},通配符languageID与messageID会分别被解析为源语言代码和消息 ID,并可通过context.params获取; onWrite语义:在任何创建、更新、删除操作后触发,change.after为写入后的数据快照;- 幂等保护:
translated标志是防重复翻译的关键。如果消息已被标记为已翻译,函数直接返回null结束。这一点对 Realtime Database 触发器尤为重要——因为扇出写回本身会再次触发/messages/fr/{messageID}路径上的写入事件,若无此检查,翻译结果会被递归地再次翻译,造成无限循环。translated标志正是用来切断这一递归链条的。
3.4 并行翻译与结果写回
源码 index.js#L36-L53:
const promises = [];
for (let i = 0; i < LANGUAGES.length; i++) {
const language = LANGUAGES[i];
if (language !== context.params.languageID) {
promises.push(async () => {
const results = await translate.translateText({
contents: [snapshot.val().message],
sourceLanguageCode: context.params.languageID,
targetLanguageCode: language
});
return getDatabase().ref(`/messages/${language}/${snapshot.key}`).set({
message: results[0],
translated: true,
});
});
}
}
return Promise.all(promises);
这段代码的要点:
- 跳过源语言自身:
if (language !== context.params.languageID)避免把原文「翻译」到它自己的语言节点; - 逐语言发起翻译请求:
translateText接收contents(源文本数组)、sourceLanguageCode(源语言,来自路径通配符)、targetLanguageCode(目标语言); - Promise.all 并行执行:所有语言的翻译任务被收集为异步函数数组,最终
Promise.all(promises)保证 Cloud Functions 会等待全部翻译与写回完成后再结束实例,避免函数提前终止导致部分语言缺失; - 保持消息 key 一致:写回路径使用
snapshot.key,即源消息的messageID,从而保证同一逻辑消息在各语言节点下 key 相同; - 写回即置位:翻译结果连同
translated: true一起写入目标语言节点,该写入会再次触发本函数,但因translated已被置位而被幂等拦截。
细节提示:从代码结构看,
translateText的返回值被整体赋给results,随后以results[0]作为翻译结果写入数据库。由于@google-cloud/translate不同版本对返回结构(数组解构 vsresponse.translations数组)的约定存在差异,实际部署时建议结合package.json锁定的版本(见下节)核对返回值的具体形态,必要时改为results[0].translations[0].translatedText。
四、依赖与工程配置
原 README 明确指出依赖清单位于 functions/package.json,展开如下:
{
"name": "message-translation-functions",
"dependencies": {
"@google-cloud/translate": "^9.4.2",
"firebase-admin": "^14.5.0",
"firebase-functions": "^7.4.0"
},
"devDependencies": {
"eslint": "^8.57.1",
"eslint-plugin-promise": "^7.3.0"
},
"scripts": {
"lint": "./node_modules/.bin/eslint --max-warnings=0 .",
"serve": "firebase emulators:start --only functions",
"shell": "firebase functions:shell",
"start": "npm run shell",
"deploy": "firebase deploy --only functions",
"logs": "firebase functions:log",
"compile": "cp ../../../tsconfig.template.json ./tsconfig-compile.json && tsc --project tsconfig-compile.json"
},
"engines": {
"node": "22"
},
"private": true
}
各依赖的作用:
| 依赖 | 用途 |
|---|---|
@google-cloud/translate |
Cloud Translation API 官方客户端,提供 TranslationServiceClient |
firebase-admin |
访问 Realtime Database(getDatabase().ref(...))与初始化服务账号 |
firebase-functions |
声明触发器(此处通过 /v1 入口使用第 1 代 API) |
eslint / eslint-plugin-promise |
代码检查,保证异步 Promise 写法规范 |
运行环境方面,engines.node 指定为 22,意味着该函数需部署在 Node.js 22 运行时之上。
此外 firebase.json 为本示例声明了独立代码库名:
{
"functions": {
"codebase": "message-translation"
}
}
当同一 Firebase 项目中存在多个函数代码库时,codebase 用于区分各自的部署单元,避免互相覆盖。配套的 eslint.config.js 使用 ESLint 扁平配置(flat config),对示例代码放宽了 no-console、no-unused-vars 等规则,便于教学演示。
五、本地运行与部署
5.1 前置条件
根据仓库根 README.md 的说明,所有 functions-samples 示例均需要:
- Firebase Blaze(按量付费) 计费套餐才能部署 Cloud Functions;
- 在 Google Cloud 项目中启用 Cloud Translation API 并为该项目关联结算账户(翻译服务属于付费 API);
- 本示例基于 Realtime Database,因此还需在 Firebase 控制台创建一个 Realtime Database 实例。
5.2 安装依赖
进入示例的 functions 目录安装依赖(仓库使用 pnpm 工作区,亦可使用 npm):
cd Node-1st-gen/message-translation/functions
npm install
5.3 常用命令
package.json 中预置的 npm 脚本覆盖了开发全流程:
| 命令 | 功能 |
|---|---|
npm run lint |
以零警告为标准运行 ESLint 检查 |
npm run serve |
启动 Firebase Emulator 套件中的函数模拟器,便于本地调试触发器 |
npm run shell(或 npm start) |
进入 firebase functions:shell 交互式 shell,可手动模拟数据库写入事件 |
npm run deploy |
仅部署函数到 Firebase(等价于 firebase deploy --only functions) |
npm run logs |
查看云端函数日志(firebase functions:log) |
5.4 端到端验证流程
- 在 Realtime Database 中写入一条消息,例如在
/messages/en/key-123456下写入{ "message": "Hey Bob! How Are you?", "translated": false }; - 触发器随即启动,调用 Translation API 将该消息翻译为目标语言列表中的其他语言;
- 观察数据库,应出现
/messages/fr/key-123456、/messages/es/key-123456等节点,内容为对应语言的译文且translated: true; - 若需调整目标语言,直接修改 index.js#L27 中的
LANGUAGES数组并重新部署。
六、扩展思路与设计启示
从实现事实出发,该示例可进一步演进的点包括:
- 动态语言列表:将
LANGUAGES数组改为从 Realtime Database 读取,实现不重新部署即可增删语言(仓库根 README 对该用例的说明即点明了这一思路); - 删除语义:当前
onWrite对删除操作(change.after为空)未做处理,如业务需要「删除源消息同时清理各语言副本」,可在触发器内补充对snapshot.val()为空的分支; - 错误补偿:
Promise.all中任一语言翻译失败会导致整个函数调用失败,可引入按语言独立捕获错误与重试机制; - 成本控制:每次消息写入都会产生最多 7 次(8 个目标语言减去源语言)翻译请求,可按业务量评估翻译配额与预算。
整体而言,本示例是「Realtime Database 触发器 + 第三方云 API 扇出写回」的教科书级模板:translated 标志的幂等设计、{languageID}/{messageID} 的双通配符路径、以及「同 key 多语言父节点」的存储布局,都可直接复用到邮件本地化、内容多语种化、机器人多语言回复等同类场景。
参考资料(仓库内路径)
- 示例说明文档:Node-1st-gen/message-translation/README.md
- 函数核心实现:functions/index.js
- 依赖与脚本:functions/package.json
- 代码库配置:firebase.json
- ESLint 配置:functions/eslint.config.js
- 仓库总览与示例索引:README.md