Selaa lähdekoodia

Merge pull request #33149 from overleaf/ar-ja-remove-i18next-additional-packages

[web] remove i18next additional libraries

GitOrigin-RevId: 98fc17b409090db32b02bb66953f1c2e6efee608
Andrew Rumble 3 kuukautta sitten
vanhempi
sitoutus
f434b1fc28

+ 0 - 1
services/web/app/src/infrastructure/Server.mjs

@@ -234,7 +234,6 @@ await Modules.applyNonCsrfRouter(webRouter, privateApiRouter, publicApiRouter)
 
 webRouter.csrf = new CsrfClass()
 webRouter.use(webRouter.csrf.middleware)
-webRouter.use(translations.i18nMiddleware)
 webRouter.use(translations.setLangBasedOnDomainMiddleware)
 
 if (Settings.cookieRollingSession) {

+ 85 - 56
services/web/app/src/infrastructure/Translations.mjs

@@ -1,12 +1,48 @@
 import i18n from 'i18next'
-import fsBackend from 'i18next-fs-backend'
-import middleware from 'i18next-http-middleware'
-import path from 'node:path'
 import Settings from '@overleaf/settings'
 import { URL } from 'node:url'
 import pug from 'pug-runtime'
 import logger from '@overleaf/logger'
 import SafeHTMLSubstitution from '../Features/Helpers/SafeHTMLSubstitution.mjs'
+import cs from '../../../locales/cs.json' with { type: 'json' }
+import da from '../../../locales/da.json' with { type: 'json' }
+import de from '../../../locales/de.json' with { type: 'json' }
+import en from '../../../locales/en.json' with { type: 'json' }
+import es from '../../../locales/es.json' with { type: 'json' }
+import fi from '../../../locales/fi.json' with { type: 'json' }
+import fr from '../../../locales/fr.json' with { type: 'json' }
+import it from '../../../locales/it.json' with { type: 'json' }
+import ja from '../../../locales/ja.json' with { type: 'json' }
+import ko from '../../../locales/ko.json' with { type: 'json' }
+import nl from '../../../locales/nl.json' with { type: 'json' }
+import no from '../../../locales/no.json' with { type: 'json' }
+import pl from '../../../locales/pl.json' with { type: 'json' }
+import pt from '../../../locales/pt.json' with { type: 'json' }
+import ru from '../../../locales/ru.json' with { type: 'json' }
+import sv from '../../../locales/sv.json' with { type: 'json' }
+import tr from '../../../locales/tr.json' with { type: 'json' }
+import zhCN from '../../../locales/zh-CN.json' with { type: 'json' }
+
+const locales = {
+  cs,
+  da,
+  de,
+  en,
+  es,
+  fi,
+  fr,
+  it,
+  ja,
+  ko,
+  nl,
+  no,
+  pl,
+  pt,
+  ru,
+  sv,
+  tr,
+  'zh-CN': zhCN,
+}
 
 const fallbackLanguageCode = Settings.i18n.defaultLng || 'en'
 const availableLanguageCodes = []
@@ -31,28 +67,19 @@ if (!availableLanguageCodes.includes(fallbackLanguageCode)) {
   availableLanguageCodes.push(fallbackLanguageCode)
 }
 
-// The "node --watch" flag is not easy to detect.
-if (process.argv.includes('--watch-locales')) {
-  // Dummy imports for setting up watching of locales files.
-  for (const lngCode of availableLanguageCodes) {
-    await import(`../../../locales/${lngCode}.json`, { with: { type: 'json' } })
-  }
-}
+const resources = Object.fromEntries(
+  Object.entries(locales)
+    .filter(([lngCode]) => availableLanguageCodes.includes(lngCode))
+    .map(([lngCode, translations]) => [lngCode, { translation: translations }])
+)
 
 i18n
-  .use(fsBackend)
-  .use(middleware.LanguageDetector)
   .init({
-    backend: {
-      loadPath: path.join(import.meta.dirname, '../../../locales/__lng__.json'),
-    },
+    resources,
 
     // still using the v3 plural suffixes
     compatibilityJSON: 'v3',
 
-    // Load translation files synchronously: https://www.i18next.com/overview/configuration-options#initimmediate
-    initImmediate: false,
-
     // We use the legacy v1 JSON format, so configure interpolator to use
     // underscores instead of curly braces
     interpolation: {
@@ -72,7 +99,6 @@ i18n
       },
     },
 
-    preload: availableLanguageCodes,
     supportedLngs: availableLanguageCodes,
     fallbackLng: fallbackLanguageCode,
   })
@@ -80,25 +106,26 @@ i18n
     logger.error({ err }, 'failed to initialize i18next library')
   })
 
-// Make custom language detector for Accept-Language header
-const headerLangDetector = new middleware.LanguageDetector(i18n.services, {
-  order: ['header'],
-})
-
 function setLangBasedOnDomainMiddleware(req, res, next) {
   // Determine language from subdomain
-  const lang = availableHosts.get(req.headers.host)
-  if (lang) {
-    req.i18n.changeLanguage(lang)
+  const lang = availableHosts.get(req.headers.host) ?? fallbackLanguageCode
+
+  req.i18n = {
+    language: lang,
   }
 
-  // expose the language code to pug
-  res.locals.currentLngCode = req.language
+  req.language =
+    req.locale =
+    req.lng =
+    res.locals.currentLngCode =
+    res.locals.language =
+      lang
 
   // If the set language is different from the language detection (based on
   // the Accept-Language header), then set flag which will show a banner
   // offering to switch to the appropriate library
-  const detectedLanguageCode = headerLangDetector.detect(req, res)
+  const detectedLanguageCode =
+    req.acceptsLanguage(availableLanguageCodes) || fallbackLanguageCode
   if (req.language !== detectedLanguageCode) {
     res.locals.suggestedLanguageSubdomainConfig =
       subdomainConfigs.get(detectedLanguageCode)
@@ -106,32 +133,35 @@ function setLangBasedOnDomainMiddleware(req, res, next) {
 
   // Decorate req.i18n with translate function alias for backwards
   // compatibility usage in requests
-  req.i18n.translate = (key, vars, components) => {
-    vars = vars || {}
-
-    if (Settings.i18n.checkForHTMLInVars) {
-      Object.entries(vars).forEach(([field, value]) => {
-        if (pug.escape(value) !== value) {
-          const violationsKey = key + field
-          // do not flood the logs, log one sample per pod + key + field
-          if (!I18N_HTML_INJECTIONS.has(violationsKey)) {
-            logger.warn(
-              { key, field, value },
-              'html content in translations context vars'
-            )
-            I18N_HTML_INJECTIONS.add(violationsKey)
-          }
+  req.i18n.translate =
+    res.locals.t =
+    req.i18n.t =
+      (key, vars, components) => {
+        vars = { lng: lang, ...(vars ?? {}) }
+
+        if (Settings.i18n.checkForHTMLInVars) {
+          Object.entries(vars).forEach(([field, value]) => {
+            if (pug.escape(value) !== value) {
+              const violationsKey = key + field
+              // do not flood the logs, log one sample per pod + key + field
+              if (!I18N_HTML_INJECTIONS.has(violationsKey)) {
+                logger.warn(
+                  { key, field, value },
+                  'html content in translations context vars'
+                )
+                I18N_HTML_INJECTIONS.add(violationsKey)
+              }
+            }
+          })
         }
-      })
-    }
-
-    const locale = req.i18n.t(key, vars)
-    if (components) {
-      return SafeHTMLSubstitution.render(locale, components)
-    } else {
-      return locale
-    }
-  }
+
+        const locale = i18n.t(key, vars)
+        if (components) {
+          return SafeHTMLSubstitution.render(locale, components)
+        } else {
+          return locale
+        }
+      }
 
   next()
 }
@@ -141,7 +171,6 @@ function setLangBasedOnDomainMiddleware(req, res, next) {
 i18n.translate = i18n.t
 
 export default {
-  i18nMiddleware: middleware.handle(i18n),
   setLangBasedOnDomainMiddleware,
   i18n,
 }

+ 0 - 2
services/web/package.json

@@ -141,8 +141,6 @@
     "helmet": "^6.0.1",
     "https-proxy-agent": "^7.0.6",
     "i18next": "^23.10.0",
-    "i18next-fs-backend": "2.6.1",
-    "i18next-http-middleware": "^3.5.0",
     "jose": "^4.3.8",
     "json2csv": "^4.3.3",
     "jsonwebtoken": "^9.0.3",

+ 18 - 9
services/web/test/unit/src/infrastructure/Translations.test.mjs

@@ -1,19 +1,18 @@
 import { describe, expect, it, vi } from 'vitest'
+import express from 'express'
 
 const MODULE_PATH = '../../../../app/src/infrastructure/Translations.mjs'
 
 describe('Translations', function () {
   let req, res, translations
-  async function runMiddlewares(cb) {
+  async function runMiddlewares() {
     return await new Promise((resolve, reject) =>
-      translations.i18nMiddleware(req, res, () => {
-        translations.setLangBasedOnDomainMiddleware(req, res, (err, result) => {
-          if (err) {
-            reject(err)
-          } else {
-            resolve(result)
-          }
-        })
+      translations.setLangBasedOnDomainMiddleware(req, res, (err, result) => {
+        if (err) {
+          reject(err)
+        } else {
+          resolve(result)
+        }
       })
     )
   }
@@ -39,6 +38,7 @@ describe('Translations', function () {
       headers: {
         'accept-language': '',
       },
+      acceptsLanguage: express.request.acceptsLanguages,
     }
     res = {
       locals: {},
@@ -59,6 +59,15 @@ describe('Translations', function () {
     it('has translate alias', function () {
       expect(req.i18n.translate('give_feedback')).to.equal('Give feedback')
     })
+
+    it('does not persist across different languages', function () {
+      expect([
+        req.i18n.translate('log_in', { lng: 'fr' }),
+        req.i18n.translate('log_in', { lng: 'en' }),
+        req.i18n.translate('log_in', { lng: 'da' }),
+        req.i18n.translate('log_in'),
+      ]).to.deep.equal(['Se connecter', 'Log in', 'Log ind', 'Log in'])
+    })
   })
 
   describe('interpolation', function () {

+ 7 - 0
services/web/types/backend/express/request.d.ts

@@ -19,5 +19,12 @@ declare module 'express' {
     userRestrictions?: Set
     oauth_user?: OAuth2Server.User
     logger: RequestLogger
+    i18n: {
+      translate(
+        key: string,
+        vars?: Record<string, any>,
+        components?: any
+      ): string
+    }
   }
 }

+ 0 - 16
yarn.lock

@@ -7371,8 +7371,6 @@ __metadata:
     html-webpack-plugin: "npm:^5.5.3"
     https-proxy-agent: "npm:^7.0.6"
     i18next: "npm:^23.10.0"
-    i18next-fs-backend: "npm:2.6.1"
-    i18next-http-middleware: "npm:^3.5.0"
     i18next-scanner: "npm:4.4.0"
     idb: "npm:^8.0.0"
     inversify: "npm:^6.2.2"
@@ -20737,20 +20735,6 @@ __metadata:
   languageName: node
   linkType: hard
 
-"i18next-fs-backend@npm:2.6.1":
-  version: 2.6.1
-  resolution: "i18next-fs-backend@npm:2.6.1"
-  checksum: 10c0/9751745d40a9f5e57fd6144129dba11f120372f5dd9e906d025c3f96c569d65aeeddfd1a36cdfcfe7acda9c624d80ef9610917544485174a363793ca00cf8636
-  languageName: node
-  linkType: hard
-
-"i18next-http-middleware@npm:3.5.0":
-  version: 3.5.0
-  resolution: "i18next-http-middleware@npm:3.5.0"
-  checksum: 10c0/fc3cb67c6984c0eb29c7456ecf320230eb361534f7d58983f514e66f14addbf082d5aa7af04b4c0bbad45b696faf4709aac64840fcd5db02038f5870b7285ea9
-  languageName: node
-  linkType: hard
-
 "i18next-scanner@npm:4.4.0":
   version: 4.4.0
   resolution: "i18next-scanner@npm:4.4.0"