functions-samples 实战:基于 Google Translate API 的 Realtime Database 自动消息翻译

原创2026-10-13 13:59:571,935 阅读
文章标签:示例工程

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);

这段代码的要点:

  1. 跳过源语言自身:if (language !== context.params.languageID) 避免把原文「翻译」到它自己的语言节点;
  2. 逐语言发起翻译请求:translateText 接收 contents(源文本数组)、sourceLanguageCode(源语言,来自路径通配符)、targetLanguageCode(目标语言);
  3. Promise.all 并行执行:所有语言的翻译任务被收集为异步函数数组,最终 Promise.all(promises) 保证 Cloud Functions 会等待全部翻译与写回完成后再结束实例,避免函数提前终止导致部分语言缺失;
  4. 保持消息 key 一致:写回路径使用 snapshot.key,即源消息的 messageID,从而保证同一逻辑消息在各语言节点下 key 相同;
  5. 写回即置位:翻译结果连同 translated: true 一起写入目标语言节点,该写入会再次触发本函数,但因 translated 已被置位而被幂等拦截。

细节提示:从代码结构看,translateText 的返回值被整体赋给 results,随后以 results[0] 作为翻译结果写入数据库。由于 @google-cloud/translate 不同版本对返回结构(数组解构 vs response.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 端到端验证流程

  1. 在 Realtime Database 中写入一条消息,例如在 /messages/en/key-123456 下写入 { "message": "Hey Bob! How Are you?", "translated": false };
  2. 触发器随即启动,调用 Translation API 将该消息翻译为目标语言列表中的其他语言;
  3. 观察数据库,应出现 /messages/fr/key-123456、/messages/es/key-123456 等节点,内容为对应语言的译文且 translated: true;
  4. 若需调整目标语言,直接修改 index.js#L27 中的 LANGUAGES 数组并重新部署。

六、扩展思路与设计启示

从实现事实出发,该示例可进一步演进的点包括:

  • 动态语言列表:将 LANGUAGES 数组改为从 Realtime Database 读取,实现不重新部署即可增删语言(仓库根 README 对该用例的说明即点明了这一思路);
  • 删除语义:当前 onWrite 对删除操作(change.after 为空)未做处理,如业务需要「删除源消息同时清理各语言副本」,可在触发器内补充对 snapshot.val() 为空的分支;
  • 错误补偿:Promise.all 中任一语言翻译失败会导致整个函数调用失败,可引入按语言独立捕获错误与重试机制;
  • 成本控制:每次消息写入都会产生最多 7 次(8 个目标语言减去源语言)翻译请求,可按业务量评估翻译配额与预算。

整体而言,本示例是「Realtime Database 触发器 + 第三方云 API 扇出写回」的教科书级模板:translated 标志的幂等设计、{languageID}/{messageID} 的双通配符路径、以及「同 key 多语言父节点」的存储布局,都可直接复用到邮件本地化、内容多语种化、机器人多语言回复等同类场景。


参考资料(仓库内路径)

登录后查看全文
functions-samples