Translations.mjs 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176
  1. import i18n from 'i18next'
  2. import Settings from '@overleaf/settings'
  3. import { URL } from 'node:url'
  4. import pug from 'pug-runtime'
  5. import logger from '@overleaf/logger'
  6. import SafeHTMLSubstitution from '../Features/Helpers/SafeHTMLSubstitution.mjs'
  7. import cs from '../../../locales/cs.json' with { type: 'json' }
  8. import da from '../../../locales/da.json' with { type: 'json' }
  9. import de from '../../../locales/de.json' with { type: 'json' }
  10. import en from '../../../locales/en.json' with { type: 'json' }
  11. import es from '../../../locales/es.json' with { type: 'json' }
  12. import fi from '../../../locales/fi.json' with { type: 'json' }
  13. import fr from '../../../locales/fr.json' with { type: 'json' }
  14. import it from '../../../locales/it.json' with { type: 'json' }
  15. import ja from '../../../locales/ja.json' with { type: 'json' }
  16. import ko from '../../../locales/ko.json' with { type: 'json' }
  17. import nl from '../../../locales/nl.json' with { type: 'json' }
  18. import no from '../../../locales/no.json' with { type: 'json' }
  19. import pl from '../../../locales/pl.json' with { type: 'json' }
  20. import pt from '../../../locales/pt.json' with { type: 'json' }
  21. import ru from '../../../locales/ru.json' with { type: 'json' }
  22. import sv from '../../../locales/sv.json' with { type: 'json' }
  23. import tr from '../../../locales/tr.json' with { type: 'json' }
  24. import zhCN from '../../../locales/zh-CN.json' with { type: 'json' }
  25. const locales = {
  26. cs,
  27. da,
  28. de,
  29. en,
  30. es,
  31. fi,
  32. fr,
  33. it,
  34. ja,
  35. ko,
  36. nl,
  37. no,
  38. pl,
  39. pt,
  40. ru,
  41. sv,
  42. tr,
  43. 'zh-CN': zhCN,
  44. }
  45. const fallbackLanguageCode = Settings.i18n.defaultLng || 'en'
  46. const availableLanguageCodes = []
  47. const availableHosts = new Map()
  48. const subdomainConfigs = new Map()
  49. const I18N_HTML_INJECTIONS = new Set()
  50. Object.values(Settings.i18n.subdomainLang || {}).forEach(function (spec) {
  51. availableLanguageCodes.push(spec.lngCode)
  52. // prebuild a host->lngCode mapping for the usage at runtime in the
  53. // middleware
  54. availableHosts.set(new URL(spec.url).host, spec.lngCode)
  55. // prebuild a lngCode -> language config mapping; some subdomains should
  56. // not appear in the language picker
  57. if (!spec.hide) {
  58. subdomainConfigs.set(spec.lngCode, spec)
  59. }
  60. })
  61. if (!availableLanguageCodes.includes(fallbackLanguageCode)) {
  62. // always load the fallback locale
  63. availableLanguageCodes.push(fallbackLanguageCode)
  64. }
  65. const resources = Object.fromEntries(
  66. Object.entries(locales)
  67. .filter(([lngCode]) => availableLanguageCodes.includes(lngCode))
  68. .map(([lngCode, translations]) => [lngCode, { translation: translations }])
  69. )
  70. i18n
  71. .init({
  72. resources,
  73. // still using the v3 plural suffixes
  74. compatibilityJSON: 'v3',
  75. // We use the legacy v1 JSON format, so configure interpolator to use
  76. // underscores instead of curly braces
  77. interpolation: {
  78. prefix: '__',
  79. suffix: '__',
  80. unescapeSuffix: 'HTML',
  81. // Disable escaping of interpolated values for backwards compatibility.
  82. // We escape the value after it's translated in web, so there's no
  83. // security risk
  84. escapeValue: Settings.i18n.escapeHTMLInVars,
  85. // Disable nesting in interpolated values, preventing user input
  86. // injection via another nested value
  87. skipOnVariables: true,
  88. defaultVariables: {
  89. appName: Settings.appName,
  90. },
  91. },
  92. supportedLngs: availableLanguageCodes,
  93. fallbackLng: fallbackLanguageCode,
  94. })
  95. .catch(err => {
  96. logger.error({ err }, 'failed to initialize i18next library')
  97. })
  98. function setLangBasedOnDomainMiddleware(req, res, next) {
  99. // Determine language from subdomain
  100. const lang = availableHosts.get(req.headers.host) ?? fallbackLanguageCode
  101. req.i18n = {
  102. language: lang,
  103. }
  104. req.language =
  105. req.locale =
  106. req.lng =
  107. res.locals.currentLngCode =
  108. res.locals.language =
  109. lang
  110. // If the set language is different from the language detection (based on
  111. // the Accept-Language header), then set flag which will show a banner
  112. // offering to switch to the appropriate library
  113. const detectedLanguageCode =
  114. req.acceptsLanguage(availableLanguageCodes) || fallbackLanguageCode
  115. if (req.language !== detectedLanguageCode) {
  116. res.locals.suggestedLanguageSubdomainConfig =
  117. subdomainConfigs.get(detectedLanguageCode)
  118. }
  119. // Decorate req.i18n with translate function alias for backwards
  120. // compatibility usage in requests
  121. req.i18n.translate =
  122. res.locals.t =
  123. req.i18n.t =
  124. (key, vars, components) => {
  125. vars = { lng: lang, ...(vars ?? {}) }
  126. if (Settings.i18n.checkForHTMLInVars) {
  127. Object.entries(vars).forEach(([field, value]) => {
  128. if (pug.escape(value) !== value) {
  129. const violationsKey = key + field
  130. // do not flood the logs, log one sample per pod + key + field
  131. if (!I18N_HTML_INJECTIONS.has(violationsKey)) {
  132. logger.warn(
  133. { key, field, value },
  134. 'html content in translations context vars'
  135. )
  136. I18N_HTML_INJECTIONS.add(violationsKey)
  137. }
  138. }
  139. })
  140. }
  141. const locale = i18n.t(key, vars)
  142. if (components) {
  143. return SafeHTMLSubstitution.render(locale, components)
  144. } else {
  145. return locale
  146. }
  147. }
  148. next()
  149. }
  150. // Decorate i18n with translate function alias for backwards compatibility
  151. // in direct usage
  152. i18n.translate = i18n.t
  153. export default {
  154. setLangBasedOnDomainMiddleware,
  155. i18n,
  156. }