From 29028a81f5dab592a6af571af14940744fb60a50 Mon Sep 17 00:00:00 2001 From: Aldrin <53973174+Dhoni77@users.noreply.github.com> Date: Thu, 19 Oct 2023 20:43:40 +0530 Subject: [PATCH] Add i18n service to query XML files (#2503) * Add i18n service to query XML files * Update changelog --------- Co-authored-by: Thomas <4159106+dtslvr@users.noreply.github.com> --- CHANGELOG.md | 1 + .../middlewares/html-template.middleware.ts | 19 ++---- apps/api/src/services/i18n/i18n.service.ts | 67 +++++++++++++++++++ 3 files changed, 75 insertions(+), 12 deletions(-) create mode 100644 apps/api/src/services/i18n/i18n.service.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index caf00d539..e3c00bdc2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Added a chart to the account detail dialog +- Added an `i18n` service to query `messages.*.xlf` files on the server ### Changed diff --git a/apps/api/src/middlewares/html-template.middleware.ts b/apps/api/src/middlewares/html-template.middleware.ts index 9d44bdbe0..f0358eca6 100644 --- a/apps/api/src/middlewares/html-template.middleware.ts +++ b/apps/api/src/middlewares/html-template.middleware.ts @@ -2,6 +2,7 @@ import * as fs from 'fs'; import { join } from 'path'; import { environment } from '@ghostfolio/api/environments/environment'; +import { I18nService } from '@ghostfolio/api/services/i18n/i18n.service'; import { DEFAULT_LANGUAGE_CODE, DEFAULT_ROOT_URL, @@ -11,20 +12,11 @@ import { DATE_FORMAT, interpolate } from '@ghostfolio/common/helper'; import { format } from 'date-fns'; import { NextFunction, Request, Response } from 'express'; -const descriptions = { - de: 'Mit dem Finanz-Dashboard Ghostfolio können Sie Ihr Vermögen in Form von Aktien, ETFs oder Kryptowährungen verteilt über mehrere Finanzinstitute überwachen.', - en: 'Ghostfolio is a personal finance dashboard to keep track of your assets like stocks, ETFs or cryptocurrencies across multiple platforms.', - es: 'Ghostfolio es un dashboard de finanzas personales para hacer un seguimiento de tus activos como acciones, ETFs o criptodivisas a través de múltiples plataformas.', - fr: 'Ghostfolio est un dashboard de finances personnelles qui permet de suivre vos actifs comme les actions, les ETF ou les crypto-monnaies sur plusieurs plateformes.', - it: 'Ghostfolio è un dashboard di finanza personale per tenere traccia delle vostre attività come azioni, ETF o criptovalute su più piattaforme.', - nl: 'Ghostfolio is een persoonlijk financieel dashboard om uw activa zoals aandelen, ETF’s of cryptocurrencies over meerdere platforms bij te houden.', - pt: 'Ghostfolio é um dashboard de finanças pessoais para acompanhar os seus activos como acções, ETFs ou criptomoedas em múltiplas plataformas.', - tr: 'Ghostfolio, hisse senetleri, ETF’ler veya kripto para birimleri gibi varlıklarınızı birden fazla platformda takip etmenizi sağlayan bir kişisel finans panosudur.' -}; - const title = 'Ghostfolio – Open Source Wealth Management Software'; const titleShort = 'Ghostfolio'; +const i18nService = new I18nService(); + let indexHtmlMap: { [languageCode: string]: string } = {}; try { @@ -130,7 +122,10 @@ export const HtmlTemplateMiddleware = async ( languageCode, path, rootUrl, - description: descriptions[languageCode], + description: i18nService.getTranslation({ + languageCode, + id: 'metaDescription' + }), featureGraphicPath: locales[path]?.featureGraphicPath ?? 'assets/cover.png', title: locales[path]?.title ?? title diff --git a/apps/api/src/services/i18n/i18n.service.ts b/apps/api/src/services/i18n/i18n.service.ts new file mode 100644 index 000000000..35c7b638d --- /dev/null +++ b/apps/api/src/services/i18n/i18n.service.ts @@ -0,0 +1,67 @@ +import { readFileSync, readdirSync } from 'fs'; +import { join } from 'path'; + +import { DEFAULT_LANGUAGE_CODE } from '@ghostfolio/common/config'; +import { Logger } from '@nestjs/common'; +import * as cheerio from 'cheerio'; + +export class I18nService { + private localesPath = join(__dirname, 'assets', 'locales'); + private translations: { [locale: string]: cheerio.CheerioAPI } = {}; + + public constructor() { + this.loadFiles(); + } + + public getTranslation({ + id, + languageCode + }: { + id: string; + languageCode: string; + }): string { + const $ = this.translations[languageCode]; + + if (!$) { + Logger.warn(`Translation not found for locale '${languageCode}'`); + } + + const translatedText = $( + `trans-unit[id="${id}"] > ${ + languageCode === DEFAULT_LANGUAGE_CODE ? 'source' : 'target' + }` + ).text(); + + if (!translatedText) { + Logger.warn( + `Translation not found for id '${id}' in locale '${languageCode}'` + ); + } + + return translatedText; + } + + private loadFiles() { + try { + const files = readdirSync(this.localesPath, 'utf-8'); + + for (const file of files) { + const xmlData = readFileSync(join(this.localesPath, file), 'utf8'); + this.translations[this.parseLanguageCode(file)] = + this.parseXml(xmlData); + } + } catch (error) { + Logger.error(error, 'I18nService'); + } + } + + private parseLanguageCode(aFileName: string) { + const match = aFileName.match(/\.([a-zA-Z]+)\.xlf$/); + + return match ? match[1] : DEFAULT_LANGUAGE_CODE; + } + + private parseXml(xmlData: string): cheerio.CheerioAPI { + return cheerio.load(xmlData, { xmlMode: true }); + } +}