From d639d9a14d1cbae36b00c360b571217e9676f7d8 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 10 Oct 2024 14:06:04 -0500 Subject: [PATCH] Reorganize translation interfaces/files --- src/app.ts | 2 +- src/caches/translationCache.ts | 16 ++++++ src/controllers/api/translate.ts | 21 ++++---- src/entities/MastodonTranslation.ts | 17 ++++++ src/interfaces/DittoTranslator.ts | 18 +++++++ src/translators/DeepLTranslator.ts | 10 ++-- src/translators/LibreTranslateTranslator.ts | 6 +-- src/translators/translator.ts | 57 --------------------- 8 files changed, 71 insertions(+), 76 deletions(-) create mode 100644 src/caches/translationCache.ts create mode 100644 src/entities/MastodonTranslation.ts create mode 100644 src/interfaces/DittoTranslator.ts delete mode 100644 src/translators/translator.ts diff --git a/src/app.ts b/src/app.ts index 3f6f7413..ad80cbb1 100644 --- a/src/app.ts +++ b/src/app.ts @@ -120,6 +120,7 @@ import { indexController } from '@/controllers/site.ts'; import { manifestController } from '@/controllers/manifest.ts'; import { nodeInfoController, nodeInfoSchemaController } from '@/controllers/well-known/nodeinfo.ts'; import { nostrController } from '@/controllers/well-known/nostr.ts'; +import { DittoTranslator } from '@/interfaces/DittoTranslator.ts'; import { auth98Middleware, requireProof, requireRole } from '@/middleware/auth98Middleware.ts'; import { cspMiddleware } from '@/middleware/cspMiddleware.ts'; import { metricsMiddleware } from '@/middleware/metricsMiddleware.ts'; @@ -129,7 +130,6 @@ import { requireSigner } from '@/middleware/requireSigner.ts'; import { signerMiddleware } from '@/middleware/signerMiddleware.ts'; import { storeMiddleware } from '@/middleware/storeMiddleware.ts'; import { uploaderMiddleware } from '@/middleware/uploaderMiddleware.ts'; -import { DittoTranslator } from '@/translators/translator.ts'; import { translatorMiddleware } from '@/middleware/translatorMiddleware.ts'; interface AppEnv extends HonoEnv { diff --git a/src/caches/translationCache.ts b/src/caches/translationCache.ts new file mode 100644 index 00000000..88121c1f --- /dev/null +++ b/src/caches/translationCache.ts @@ -0,0 +1,16 @@ +import { LanguageCode } from 'iso-639-1'; +import { LRUCache } from 'lru-cache'; + +import { MastodonTranslation } from '@/entities/MastodonTranslation.ts'; +import { Time } from '@/utils/time.ts'; + +/** Entity returned by DittoTranslator and LRUCache */ +interface DittoTranslation { + data: MastodonTranslation; +} + +/** Translations LRU cache. */ +export const translationCache = new LRUCache<`${LanguageCode}-${string}`, DittoTranslation>({ + max: 1000, + ttl: Time.hours(6), +}); diff --git a/src/controllers/api/translate.ts b/src/controllers/api/translate.ts index e1577478..56bd1b8d 100644 --- a/src/controllers/api/translate.ts +++ b/src/controllers/api/translate.ts @@ -2,10 +2,11 @@ import { LanguageCode } from 'iso-639-1'; import { z } from 'zod'; import { AppController } from '@/app.ts'; -import { localeSchema } from '@/schema.ts'; -import { dittoTranslations, dittoTranslationsKey, MastodonTranslation } from '@/translators/translator.ts'; -import { parseBody } from '@/utils/api.ts'; +import { translationCache } from '@/caches/translationCache.ts'; +import { MastodonTranslation } from '@/entities/MastodonTranslation.ts'; import { getEvent } from '@/queries.ts'; +import { localeSchema } from '@/schema.ts'; +import { parseBody } from '@/utils/api.ts'; import { renderStatus } from '@/views/mastodon/statuses.ts'; const translateSchema = z.object({ @@ -45,11 +46,11 @@ const translateController: AppController = async (c) => { return c.json({ error: 'Bad request.', schema: result.error }, 400); } - const translatedId = `${lang}-${id}` as dittoTranslationsKey; - const translationCache = dittoTranslations.get(translatedId); + const cacheKey: `${LanguageCode}-${string}` = `${lang}-${id}`; + const cached = translationCache.get(cacheKey); - if (translationCache) { - return c.json(translationCache.data, 200); + if (cached) { + return c.json(cached.data, 200); } const mediaAttachments = status?.media_attachments.map((value) => { @@ -68,7 +69,7 @@ const translateController: AppController = async (c) => { media_attachments: [], poll: null, detected_source_language: event.language ?? 'en', - provider: translator.getProvider(), + provider: translator.provider, }; if ((status?.poll as MastodonTranslation['poll'])?.options) { @@ -130,10 +131,10 @@ const translateController: AppController = async (c) => { mastodonTranslation.detected_source_language = data.source_lang; - dittoTranslations.set(translatedId, { data: mastodonTranslation }); + translationCache.set(cacheKey, { data: mastodonTranslation }); return c.json(mastodonTranslation, 200); } catch (e) { - if (e instanceof Error && e.message?.includes('not supported')) { + if (e instanceof Error && e.message.includes('not supported')) { return c.json({ error: `Translation of source language '${event.language}' not supported` }, 422); } return c.json({ error: 'Service Unavailable' }, 503); diff --git a/src/entities/MastodonTranslation.ts b/src/entities/MastodonTranslation.ts new file mode 100644 index 00000000..d59b9aad --- /dev/null +++ b/src/entities/MastodonTranslation.ts @@ -0,0 +1,17 @@ +import { LanguageCode } from 'iso-639-1'; + +/** https://docs.joinmastodon.org/entities/Translation/ */ +export interface MastodonTranslation { + /** HTML-encoded translated content of the status. */ + content: string; + /** The translated spoiler warning of the status. */ + spoiler_text: string; + /** The translated media descriptions of the status. */ + media_attachments: { id: string; description: string }[]; + /** The translated poll of the status. */ + poll: { id: string; options: { title: string }[] } | null; + //** The language of the source text, as auto-detected by the machine translation provider. */ + detected_source_language: LanguageCode; + /** The service that provided the machine translation. */ + provider: string; +} diff --git a/src/interfaces/DittoTranslator.ts b/src/interfaces/DittoTranslator.ts new file mode 100644 index 00000000..426e1428 --- /dev/null +++ b/src/interfaces/DittoTranslator.ts @@ -0,0 +1,18 @@ +import type { LanguageCode } from 'iso-639-1'; + +/** DittoTranslator class, used for status translation. */ +export interface DittoTranslator { + /** Translate the 'content' into 'targetLanguage'. */ + translate( + /** Texts to translate. */ + texts: string[], + /** The language of the source texts. */ + sourceLanguage: LanguageCode | undefined, + /** The texts will be translated into this language. */ + targetLanguage: LanguageCode, + /** Custom options. */ + opts?: { signal?: AbortSignal }, + ): Promise<{ results: string[]; source_lang: LanguageCode }>; + /** Provider name, eg `DeepL.com` */ + provider: string; +} diff --git a/src/translators/DeepLTranslator.ts b/src/translators/DeepLTranslator.ts index 2b4da175..b856d1ec 100644 --- a/src/translators/DeepLTranslator.ts +++ b/src/translators/DeepLTranslator.ts @@ -1,7 +1,7 @@ import { LanguageCode } from 'iso-639-1'; import { z } from 'zod'; -import { DittoTranslator, SourceLanguage, TargetLanguage } from '@/translators/translator.ts'; +import { DittoTranslator } from '@/interfaces/DittoTranslator.ts'; import { languageSchema } from '@/schema.ts'; interface DeepLTranslatorOpts { @@ -28,8 +28,8 @@ export class DeepLTranslator implements DittoTranslator { async translate( texts: string[], - source: SourceLanguage | undefined, - dest: TargetLanguage, + source: LanguageCode | undefined, + dest: LanguageCode, opts?: { signal?: AbortSignal }, ) { const { translations } = await this.translateMany(texts, source, dest, opts); @@ -43,8 +43,8 @@ export class DeepLTranslator implements DittoTranslator { /** DeepL translate request. */ private async translateMany( texts: string[], - source: SourceLanguage | undefined, - targetLanguage: TargetLanguage, + source: LanguageCode | undefined, + targetLanguage: LanguageCode, opts?: { signal?: AbortSignal }, ) { const body: any = { diff --git a/src/translators/LibreTranslateTranslator.ts b/src/translators/LibreTranslateTranslator.ts index bd2850ff..b7a749cf 100644 --- a/src/translators/LibreTranslateTranslator.ts +++ b/src/translators/LibreTranslateTranslator.ts @@ -1,7 +1,7 @@ import { LanguageCode } from 'iso-639-1'; import { z } from 'zod'; -import { DittoTranslator, SourceLanguage, TargetLanguage } from '@/translators/translator.ts'; +import { DittoTranslator } from '@/interfaces/DittoTranslator.ts'; import { languageSchema } from '@/schema.ts'; interface LibreTranslateTranslatorOpts { @@ -28,8 +28,8 @@ export class LibreTranslateTranslator implements DittoTranslator { async translate( texts: string[], - source: SourceLanguage | undefined, - dest: TargetLanguage, + source: LanguageCode | undefined, + dest: LanguageCode, opts?: { signal?: AbortSignal }, ) { const translations = await Promise.all( diff --git a/src/translators/translator.ts b/src/translators/translator.ts deleted file mode 100644 index df3c7929..00000000 --- a/src/translators/translator.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { LanguageCode } from 'iso-639-1'; -import { LRUCache } from 'lru-cache'; - -import { Time } from '@/utils/time.ts'; - -/** Original language of the post */ -export type SourceLanguage = LanguageCode; - -/** Content will be translated to this language */ -export type TargetLanguage = LanguageCode; - -/** Entity returned by DittoTranslator and LRUCache */ -type DittoTranslation = { - data: MastodonTranslation; -}; - -export type MastodonTranslation = { - /** HTML-encoded translated content of the status. */ - content: string; - /** The translated spoiler warning of the status. */ - spoiler_text: string; - /** The translated media descriptions of the status. */ - media_attachments: { id: string; description: string }[]; - /** The translated poll of the status. */ - poll: { id: string; options: { title: string }[] } | null; - //** The language of the source text, as auto-detected by the machine translation provider. */ - detected_source_language: SourceLanguage; - /** The service that provided the machine translation. */ - provider: string; -}; - -/** DittoTranslator class, used for status translation. */ -export interface DittoTranslator { - /** Translate the 'content' into 'targetLanguage'. */ - translate( - texts: string[], - /** The language of the source text/status. */ - sourceLanguage: SourceLanguage | undefined, - /** The status content will be translated into this language. */ - targetLanguage: TargetLanguage, - /** Custom options. */ - opts?: { signal?: AbortSignal }, - ): Promise<{ results: string[]; source_lang: SourceLanguage }>; - /** Provider name, eg `DeepL.com` */ - provider: string; -} - -/** Includes the TARGET language and the status id. - * Example: en-390f5b01b49a8ee6e13fe917420c023d889b3da8e983a14c9e84587e43d12c15 - * The example above means: - * I want the status 390f5b01b49a8ee6e13fe917420c023d889b3da8e983a14c9e84587e43d12c15 translated to english (if it exists in the LRUCache). */ -export type dittoTranslationsKey = `${TargetLanguage}-${string}`; - -export const dittoTranslations = new LRUCache({ - max: 1000, - ttl: Time.hours(6), -});