| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176 |
- import i18n from 'i18next'
- 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 = []
- const availableHosts = new Map()
- const subdomainConfigs = new Map()
- const I18N_HTML_INJECTIONS = new Set()
- Object.values(Settings.i18n.subdomainLang || {}).forEach(function (spec) {
- availableLanguageCodes.push(spec.lngCode)
- // prebuild a host->lngCode mapping for the usage at runtime in the
- // middleware
- availableHosts.set(new URL(spec.url).host, spec.lngCode)
- // prebuild a lngCode -> language config mapping; some subdomains should
- // not appear in the language picker
- if (!spec.hide) {
- subdomainConfigs.set(spec.lngCode, spec)
- }
- })
- if (!availableLanguageCodes.includes(fallbackLanguageCode)) {
- // always load the fallback locale
- availableLanguageCodes.push(fallbackLanguageCode)
- }
- const resources = Object.fromEntries(
- Object.entries(locales)
- .filter(([lngCode]) => availableLanguageCodes.includes(lngCode))
- .map(([lngCode, translations]) => [lngCode, { translation: translations }])
- )
- i18n
- .init({
- resources,
- // still using the v3 plural suffixes
- compatibilityJSON: 'v3',
- // We use the legacy v1 JSON format, so configure interpolator to use
- // underscores instead of curly braces
- interpolation: {
- prefix: '__',
- suffix: '__',
- unescapeSuffix: 'HTML',
- // Disable escaping of interpolated values for backwards compatibility.
- // We escape the value after it's translated in web, so there's no
- // security risk
- escapeValue: Settings.i18n.escapeHTMLInVars,
- // Disable nesting in interpolated values, preventing user input
- // injection via another nested value
- skipOnVariables: true,
- defaultVariables: {
- appName: Settings.appName,
- },
- },
- supportedLngs: availableLanguageCodes,
- fallbackLng: fallbackLanguageCode,
- })
- .catch(err => {
- logger.error({ err }, 'failed to initialize i18next library')
- })
- function setLangBasedOnDomainMiddleware(req, res, next) {
- // Determine language from subdomain
- const lang = availableHosts.get(req.headers.host) ?? fallbackLanguageCode
- req.i18n = {
- language: lang,
- }
- 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 =
- req.acceptsLanguage(availableLanguageCodes) || fallbackLanguageCode
- if (req.language !== detectedLanguageCode) {
- res.locals.suggestedLanguageSubdomainConfig =
- subdomainConfigs.get(detectedLanguageCode)
- }
- // Decorate req.i18n with translate function alias for backwards
- // compatibility usage in requests
- 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 = i18n.t(key, vars)
- if (components) {
- return SafeHTMLSubstitution.render(locale, components)
- } else {
- return locale
- }
- }
- next()
- }
- // Decorate i18n with translate function alias for backwards compatibility
- // in direct usage
- i18n.translate = i18n.t
- export default {
- setLangBasedOnDomainMiddleware,
- i18n,
- }
|