Пример кэширования результатов перевода Google Translate для мультиязычного сайта на NextJS

Делюсь своим опытом по организации перевода контента на разные языки. Сейчас работаю над многоязычным сайтом со стеком NextJS i18n + MongoDB (Mongoose). На сайте довольно много текста, который изредка обновляется.

Для перевода текстов на проекте используется Google Translate. Поэтому первоначально была создана простая функция для перевода вида:

// @/lib/translate.ts import "server-only"; import { z } from "zod"; const { Translate } = require("@google-cloud/translate").v2; const ApiKey = z.string().parse(process.env.GOOGLE_TRANSLATION_API_KEY); const googleTranslate = new Translate({ key: ApiKey }); export async function translateFromEn(text: string, locale: string): Promise<string | null> { try { if (locale === 'en') return text.trim(); const translations = await googleTranslate.translate(text, locale); const result = translations[0]; return result; } catch (error) { return null; } }

И далее она использовалась везде при подготовке страниц. Чтобы не переводить одни и те же тексты каждый раз, я использовал `unstable_cache` (несмотря на свое название, данная функция у меня еще ни разу не вызывала проблем).

// .../page.tsx type Params = { locale: string }; export default async function Page({ params }: { params: Params }) { const { locale } = params; ... const translatedTitle = await unstable_cache( async () => await translateFromRu(post.title, locale), [`post:title:${post.slug}:${locale}`], { revalidate: false, tags: [`post:${post.slug}`] }, )(); const translatedShortDescription = await unstable_cache( async () => await translateFromRu(post.metaCustom.shortdescription, locale), [`post:shortDescription:${post.slug}:${locale}`], { revalidate: false, tags: [`post:${post.slug}`] }, )(); return { ... } }

Соответственно, при обновлении записи в блоге вызывался метод `revalidateTag` и переводы обновлялись.

Это вполне рабочее решение, но оно имеет изъян в том, что кэш сбрасывался при каждом обновлении сайта. Соответственно, это грозило большими счетами от Google.

Поэтому я добавил хранение результатов перевода в базе Mongo. Идея здесь следующая:

  1. Для каждой сущности перевода создаем уникальный `id`
  2. Перед обращением к Google APi делаем проверку по базе, есть ли запись с данным `id` и соответствует ли она текущему тексту, который требуется перевести
  3. Если да -> возвращаем запись из базы
  4. Если нет -> запрашиваем перевод по API и отдаем его + сохраняем результат в базе

Для простого сопоставления текстов я использую хэширование.

С новой логикой функция `translateFromEn` выглядит так:

import "server-only"; import crypto from "crypto"; import { z } from "zod"; import { TranslateResult, zTranslateResult } from "../mongoModels"; const { Translate } = require("@google-cloud/translate").v2; const ApiKey = z.string().parse(process.env.GOOGLE_TRANSLATION_API_KEY); const googleTranslate = new Translate({ key: ApiKey }); // ------------------------------------------------------------------- type GetTranslationKeyProps = { locale: string; // en, ko, ch... entity: string; // user, post, comment entityType: string; // shortDescription, metaTitle entityId: string; // slug, _id ... }; function getTranslationKey(props: GetTranslationKeyProps) { const { locale, entity, entityType, entityId } = props; return `${locale}-${entity}-${entityType}-${entityId}`; } // -------------------------------------------------------------------- // 1. If locale === 'en' - just return text // 2. Generate Key and Check if translation exists in DB // 3. Generate hash from text // 4. If old Db Translation exist and hash is the same - return translation // 5. If hash is different or hash not the save - create new translation // 6. Return translation // -------------------------------------------------------------------- export async function translateFromEn( text: string, idData: GetTranslationKeyProps, ): Promise<string | null> { try { // 1. If locale === 'en' - just return text if (idData.locale === "en" || text.trim().length === 0) { return text.trim(); } // 2. Generate Key and Check if translation exists in DB const key = getTranslationKey(idData); const oldTranslateDoc = await TranslateResult.findOne({ key, }).lean(); // 3. Generate hash from text const textHash = crypto.createHash("SHA-256").update(text).digest("hex"); // 4. If old Db Translation exist and hash is the same - return translation if (oldTranslateDoc && oldTranslateDoc.hash === textHash) { return oldTranslateDoc.translation.trim(); } // 5. If hash is different or hash not the save - create new translation const translations = await googleTranslate.translate(text.trim(), idData.locale); const translateResult = translations[0]; const newTranslationResult = zTranslateResult.parse({ key, hash: textHash, translation: translateResult, }); await TranslateResult.findOneAndUpdate({ key }, newTranslationResult, { upsert: true, }); // 6. Return translation return translateResult; // --- } catch (error) { return null; } }

Таким образом нам удалось значительно снизить число обращений к `Google Translate API` и удержать бюджет в рамках разумного.

1
Начать дискуссию