Explorar o código

[web] Add ESLint rules for locale JSON (#34221)

* Remove double spaces

* Enforce curly apostrophes

* Enforce French typographic spaces in fr.json

Add vitest test enforcing French spacing rules per
https://typographisme.net/post/Les-espaces-typographiques-et-le-web:
  - NNBSP (U+202F) before ? ! ;
  - NBSP (U+00A0) before :
  - NBSP (U+00A0) around guillemets « »

Fix 38 fr.json values that were using NBSP (or no space) before ? ! ;.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Extend French typography test to numbers and percent signs

Add rules per https://typographisme.net/post/Les-espaces-typographiques-et-le-web:
  - NNBSP (U+202F) between digit groups (thousands separator)
  - NBSP (U+00A0) between a number and a following word/unit
  - NBSP (U+00A0) before %

Fix 9 fr.json values where a regular space was used instead of NBSP between
a number and a following word (e.g. "1 résultat", "10 jetons", "7 jours").

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Use point médian for inclusive writing in fr.json

Replace parenthetical gender/plural markers like "sûr(e)", "abonné(e)",
"licence(s)" with the point médian "·" (e.g. "sûr·e", "abonné·e",
"licence·s") per modern French écriture inclusive.

Add a vitest rule rejecting "word(suffix)" patterns where suffix is 1-3
letters and there is no space before the opening parenthesis.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Simplify inclusive-writing check to "(e)" substring match

The previous regex (matchAll without the /g flag) would have thrown at
runtime. Drop matchAll entirely and use includes('(e)').

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Add point médian to participles in fr.json

Apply inclusive writing (·e) to past participles agreeing with the
addressed user ("affilié·e", "invité·e", "connecté·e", "abonné·e", etc.).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Use inclusive forms in fr.json

* Change "de Overleaf" to "d’Overleaf"

* Fix tests

* Check for consecutive spaces, not just double spaces

* Update comment about guillemets

* Add ESLint rules for locale JSON conventions

Add three rules to @overleaf/eslint-plugin that run against locale JSON
files via @eslint/json:

- no-consecutive-spaces-in-locales
- no-straight-apostrophes-in-locales
- french-typography-in-locales

Wire them up in services/web/eslint.config.mjs against `locales/*.json`
(and `locales/fr.json` for the French-typography rule).

* Add `@eslint/json` so JSON files can be parsed

* Scope JS/TS-specific config to JS/TS files only so that built-in rules aren't applied to JSON files

* Disable `no-irregular-whitespace` in french-typography-in-locales.js

* Make the rules fixable

* Improve regexes: user `\s` instead of custom matchers

* Add sorted-keys-in-locales ESLint rule

Mirrors scripts/translations/sort.js as a fixable ESLint rule:
require top-level keys in locale JSON files to be sorted alphabetically.
The autofix re-serializes the file with sorted keys.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Add no-unused-locale-keys ESLint rule

Mirrors scripts/translations/cleanupUnusedLocales.js as a fixable ESLint
rule applied only to en.json. Shells out to `find` + `cat` once per
process (cached) to grep all candidate source files for key references,
then reports any en.json key that isn't found. The autofix re-serializes
en.json without the unused keys.

Hardcoded ALWAYS_USED set covers keys built at runtime (month names,
third-party-datastore notifications) that the regex scan can't see.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Add locale-variables-match-en ESLint rule

Mirrors scripts/translations/checkVariables.js as an ESLint rule applied
to non-en locale JSON files. Reads en.json (cached) and reports any
member whose `__name__` placeholders differ from the base.

GLOBALS (e.g. __appName__) are allowed as extras; IGNORE_NESTING_FOR
preserves the script's per-key exceptions.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Replace some double linebreak (@overleaf/no-consecutive-spaces-in-locales)

* Add no-orphan-locale-keys ESLint rule

Detect translation keys in non-en locale files that aren't present in
en.json. Mirrors the `--sync-non-en` flag of cleanupUnusedLocales.js.
The autofix re-serializes the locale file without the orphan keys,
sorted alphabetically.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Add tests for the locale JSON ESLint rules

Cover the four single-file rules:
  - no-consecutive-spaces-in-locales
  - no-straight-apostrophes-in-locales
  - sorted-keys-in-locales
  - french-typography-in-locales (including the inclusive-writing
    suggestion for "(e)")

The three cross-file rules (no-unused-locale-keys, no-orphan-locale-keys,
locale-variables-match-en) read from disk and aren't covered here; they
need integration tests via fixtures.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Drop bin/lint_locales — checks are now covered by ESLint rules

The four checks in bin/lint_locales are duplicated by the new
@overleaf/eslint-plugin rules:

  - sort.js --check                  → sorted-keys-in-locales
  - cleanupUnusedLocales.js --check  → no-unused-locale-keys
                                     + no-orphan-locale-keys
  - checkVariables.js                → locale-variables-match-en
  - single-quote grep                → no-straight-apostrophes-in-locales

`yarn lint` runs the ESLint rules against locales/*.json, so the
lint_locales script and Makefile target are redundant.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Remove Locales.test.mjs

* Delete scripts replaced by @overleaf/eslint-plugin rules

Remove the three locale scripts now superseded by ESLint rules:

  - scripts/translations/sort.js                   → sorted-keys-in-locales
  - scripts/translations/cleanupUnusedLocales.js   → no-unused-locale-keys
                                                   + no-orphan-locale-keys
  - scripts/translations/checkVariables.js         → locale-variables-match-en

Also drop the matching Makefile targets (sort_locales,
cleanup_unused_locales), update AGENTS.md / copilot-instructions.md to
point at `yarn lint --fix` instead, and prune the deleted entries from
the esm-check-migration allowlist.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Delete redundant scripts/translations/sort-locale-file.js

Single-file sort utility — superseded by `yarn lint --fix path/to/file.json`
via the sorted-keys-in-locales ESLint rule. No callers in repo.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Update comments and messages

* Add test file pattern to ESLint configuration

* Reorder "Backend specific rules" so `no-extraneous-dependencies` doesn't fail on test files

* Make french-typography fixes idempotent and catch edge cases

Apply Copilot's review feedback on PR #34221:

  - autoFix() now uses `\\s*`/`\\s+` instead of `\\s?`/`\\s` so it
    collapses any run of mixed whitespace into the correct NBSP/NNBSP,
    making the fix idempotent and order-independent.
  - The `:` lint regex now captures optional whitespace and reports
    when it isn't NBSP (catches `"Liste :"` with a regular space).
  - The `%` lint regex now matches the missing-space case (`"10%"`)
    via `\\s*` instead of requiring one char between digit and `%`.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Remove sort-locale-file.js from pre-commit

* Revert no-unused-locale-keys ESLint rule; Restore lint_locales scripts

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
GitOrigin-RevId: 8351fab074e6e7f77920b70237df1cd1c781e053
Antoine Clausse hai 2 meses
pai
achega
9dde7d3d71

+ 160 - 0
libraries/eslint-plugin/french-typography-in-locales.js

@@ -0,0 +1,160 @@
+// French typographic spacing rules for JSON string values.
+// Per https://typographisme.net/post/Les-espaces-typographiques-et-le-web
+//   - NNBSP  before ? ! ;
+//   - NBSP   before :
+//   - NBSP   inside guillemets (after «, before ») — keeps the
+//     guillemet attached to its content so it cannot be orphaned at a
+//     line break
+//   - NNBSP  between digit groups (thousands separator)
+//   - NBSP   between a number and a following word/unit (50 Mo, 5 jours)
+//   - NBSP   before %
+//   - point médian "·" instead of parenthetical (e) for inclusive writing
+
+const NBSP = ' '
+const NNBSP = ' '
+
+function autoFix(value) {
+  let v = value
+  // Normalize whitespace before ? ! ; to NNBSP
+  v = v.replace(/\s*([?!;])/g, `${NNBSP}$1`)
+  v = v.replace(/([^\s])([?!;])/g, `$1${NNBSP}$2`)
+  // Normalize whitespace before : (after a letter, not before /) to NBSP
+  v = v.replace(/(\p{L})\s*:(?!\/)/gu, `$1${NBSP}:`)
+  // NBSP after «
+  v = v.replace(/«\s*/g, `«${NBSP}`)
+  // NBSP before »
+  v = v.replace(/\s*»/g, `${NBSP}»`)
+  // NNBSP between digits (thousands separator)
+  v = v.replace(/(\d)\s+(\d)/g, `$1${NNBSP}$2`)
+  // NBSP between digit and following letter (number + unit/word)
+  v = v.replace(/(\d)\s+(\p{L})/gu, `$1${NBSP}$2`)
+  // NBSP before %
+  v = v.replace(/(\d)\s*%/g, `$1${NBSP}%`)
+  return v
+}
+
+function lint(value) {
+  const violations = []
+  for (const m of value.matchAll(/(.)([?!;])/g)) {
+    if (m[1] !== NNBSP) {
+      violations.push({
+        message: `expected NNBSP before "${m[2]}", got ${JSON.stringify(m[1])}`,
+        fixable: true,
+      })
+    }
+  }
+  for (const m of value.matchAll(/(\p{L})(\s*):(?!\/)/gu)) {
+    if (m[2] !== NBSP) {
+      violations.push({
+        message: `expected NBSP before ":" after letter ${JSON.stringify(m[1])}, got ${JSON.stringify(m[2])}`,
+        fixable: true,
+      })
+    }
+  }
+  for (const m of value.matchAll(/«(.)/g)) {
+    if (m[1] !== NBSP) {
+      violations.push({
+        message: `expected NBSP after "«", got ${JSON.stringify(m[1])}`,
+        fixable: true,
+      })
+    }
+  }
+  for (const m of value.matchAll(/(.)»/g)) {
+    if (m[1] !== NBSP) {
+      violations.push({
+        message: `expected NBSP before "»", got ${JSON.stringify(m[1])}`,
+        fixable: true,
+      })
+    }
+  }
+  for (const m of value.matchAll(/(\d)(\s)(\d)/g)) {
+    if (m[2] !== NNBSP) {
+      violations.push({
+        message: `expected NNBSP between digits "${m[1]}${m[3]}", got ${JSON.stringify(m[2])}`,
+        fixable: true,
+      })
+    }
+  }
+  for (const m of value.matchAll(/(\d)(\s)(\p{L})/gu)) {
+    if (m[2] !== NBSP) {
+      violations.push({
+        message: `expected NBSP between digit "${m[1]}" and "${m[3]}", got ${JSON.stringify(m[2])}`,
+        fixable: true,
+      })
+    }
+  }
+  for (const m of value.matchAll(/(\d)(\s*)%/g)) {
+    if (m[2] !== NBSP) {
+      violations.push({
+        message: `expected NBSP before "%" after "${m[1]}", got ${JSON.stringify(m[2])}`,
+        fixable: true,
+      })
+    }
+  }
+  if (value.includes('(e)')) {
+    violations.push({
+      message:
+        'expected point médian "·" instead of "(e)" for inclusive writing',
+      fixable: false, // suggestion-only
+    })
+  }
+  return violations
+}
+
+module.exports = {
+  meta: {
+    type: 'problem',
+    fixable: 'code',
+    hasSuggestions: true,
+    docs: {
+      description:
+        'Enforce French typographic spacing in JSON string values (NNBSP before ? ! ;, NBSP before : and inside « », etc.).',
+    },
+    schema: [],
+  },
+  create(context) {
+    return {
+      Member(node) {
+        if (node.value.type !== 'String') return
+        const original = node.value.value
+        const violations = lint(original)
+        if (violations.length === 0) return
+
+        const fixed = autoFix(original)
+        const replacement = JSON.stringify(fixed)
+        const inclusiveSuggestion = original.includes('(e)')
+          ? JSON.stringify(original.replace(/\(e\)/g, '·e'))
+          : null
+
+        for (const v of violations) {
+          context.report({
+            node: node.value,
+            message: v.message,
+            ...(v.fixable && fixed !== original
+              ? {
+                  fix(fixer) {
+                    return fixer.replaceText(node.value, replacement)
+                  },
+                }
+              : {}),
+            ...(!v.fixable && inclusiveSuggestion
+              ? {
+                  suggest: [
+                    {
+                      desc: 'Replace "(e)" with "·e"',
+                      fix(fixer) {
+                        return fixer.replaceText(
+                          node.value,
+                          inclusiveSuggestion
+                        )
+                      },
+                    },
+                  ],
+                }
+              : {}),
+          })
+        }
+      },
+    }
+  },
+}

+ 6 - 0
libraries/eslint-plugin/index.js

@@ -15,5 +15,11 @@ module.exports = {
     'require-loading-label': require('./require-loading-label'),
     'require-cio-snake-case-properties': require('./require-cio-snake-case-properties'),
     'no-throw-in-callback': require('./no-throw-in-callback'),
+    'no-consecutive-spaces-in-locales': require('./no-consecutive-spaces-in-locales'),
+    'no-straight-apostrophes-in-locales': require('./no-straight-apostrophes-in-locales'),
+    'french-typography-in-locales': require('./french-typography-in-locales'),
+    'sorted-keys-in-locales': require('./sorted-keys-in-locales'),
+    'locale-variables-match-en': require('./locale-variables-match-en'),
+    'no-orphan-locale-keys': require('./no-orphan-locale-keys'),
   },
 }

+ 93 - 0
libraries/eslint-plugin/locale-variables-match-en.js

@@ -0,0 +1,93 @@
+// Ensure variable placeholders (`__name__`) in non-en locale values match
+// those in the corresponding en.json key.
+//
+// Apply this rule to all non-en locale JSON files.
+
+const fs = require('node:fs')
+const Path = require('node:path')
+
+// Placeholders always available in translations (injected by the i18n layer
+// or globally defined as defaults).
+const GLOBALS = new Set(['__appName__'])
+
+// Keys whose translations are allowed to omit specific base placeholders.
+const IGNORE_NESTING_FOR = {
+  over_x_templates_easy_getting_started: ['__templates__'],
+  all_packages_and_templates: ['__templatesLink__'],
+}
+
+function extractPlaceholders(str) {
+  return Array.from(str.matchAll(/__.*?__/g), m => m[0])
+}
+
+const enCache = new Map()
+function loadEn(localesDir) {
+  if (enCache.has(localesDir)) return enCache.get(localesDir)
+  const path = Path.join(localesDir, 'en.json')
+  const data = JSON.parse(fs.readFileSync(path, 'utf8'))
+  enCache.set(localesDir, data)
+  return data
+}
+
+module.exports = {
+  meta: {
+    type: 'problem',
+    docs: {
+      description:
+        'Ensure variable placeholders in non-en locale values match the en.json base.',
+    },
+    schema: [],
+    messages: {
+      missing:
+        'Translation for key "{{key}}" is missing placeholder(s) {{placeholders}} present in en.json.',
+      extra:
+        'Translation for key "{{key}}" has placeholder(s) {{placeholders}} not present in en.json.',
+    },
+  },
+  create(context) {
+    const filename = context.filename
+    const localesDir = Path.dirname(filename)
+    if (Path.basename(filename) === 'en.json') return {}
+
+    let base
+    try {
+      base = loadEn(localesDir)
+    } catch {
+      return {}
+    }
+
+    return {
+      Member(node) {
+        if (node.value.type !== 'String') return
+        const key = node.name.value
+        if (!(key in base)) return // orphan key — covered by no-orphan-locale-keys
+
+        const basePlaceholders = extractPlaceholders(base[key])
+        const targetPlaceholders = extractPlaceholders(node.value.value)
+        const ignored = IGNORE_NESTING_FOR[key] || []
+
+        const missing = basePlaceholders.filter(
+          p => !targetPlaceholders.includes(p) && !ignored.includes(p)
+        )
+        const extra = targetPlaceholders.filter(
+          p => !basePlaceholders.includes(p) && !GLOBALS.has(p)
+        )
+
+        if (missing.length > 0) {
+          context.report({
+            node: node.value,
+            messageId: 'missing',
+            data: { key, placeholders: missing.join(', ') },
+          })
+        }
+        if (extra.length > 0) {
+          context.report({
+            node: node.value,
+            messageId: 'extra',
+            data: { key, placeholders: extra.join(', ') },
+          })
+        }
+      },
+    }
+  },
+}

+ 37 - 0
libraries/eslint-plugin/no-consecutive-spaces-in-locales.js

@@ -0,0 +1,37 @@
+// Reject runs of two or more consecutive whitespace characters in JSON string
+// values. Catches ASCII spaces, NBSP, NNBSP, tabs, and other Unicode whitespace.
+
+const CONSECUTIVE_WHITESPACE = /\s{2,}/
+
+module.exports = {
+  meta: {
+    type: 'problem',
+    fixable: 'code',
+    docs: {
+      description:
+        'Disallow runs of two or more consecutive whitespace characters in JSON string values (typically locale files).',
+    },
+    schema: [],
+    messages: {
+      consecutiveSpaces:
+        'Locale value contains a run of consecutive whitespace. Collapse to a single space.',
+    },
+  },
+  create(context) {
+    return {
+      Member(node) {
+        if (node.value.type !== 'String') return
+        const original = node.value.value
+        if (!CONSECUTIVE_WHITESPACE.test(original)) return
+        const fixed = original.replace(/\s{2,}/g, ' ')
+        context.report({
+          node: node.value,
+          messageId: 'consecutiveSpaces',
+          fix(fixer) {
+            return fixer.replaceText(node.value, JSON.stringify(fixed))
+          },
+        })
+      },
+    }
+  },
+}

+ 69 - 0
libraries/eslint-plugin/no-orphan-locale-keys.js

@@ -0,0 +1,69 @@
+// Detect translation keys in non-en locale files that don't exist in
+// en.json (orphans).
+
+const fs = require('node:fs')
+const Path = require('node:path')
+
+const enCache = new Map()
+function loadEnKeys(localesDir) {
+  if (enCache.has(localesDir)) return enCache.get(localesDir)
+  const path = Path.join(localesDir, 'en.json')
+  const data = JSON.parse(fs.readFileSync(path, 'utf8'))
+  const keys = new Set(Object.keys(data))
+  enCache.set(localesDir, keys)
+  return keys
+}
+
+module.exports = {
+  meta: {
+    type: 'problem',
+    fixable: 'code',
+    docs: {
+      description:
+        'Detect translation keys in non-en locale files that are not present in en.json.',
+    },
+    schema: [],
+    messages: {
+      orphan:
+        'Translation key "{{key}}" is not present in en.json (orphan key).',
+    },
+  },
+  create(context) {
+    const filename = context.filename
+    const localesDir = Path.dirname(filename)
+    if (Path.basename(filename) === 'en.json') return {}
+
+    let enKeys
+    try {
+      enKeys = loadEnKeys(localesDir)
+    } catch {
+      return {}
+    }
+
+    return {
+      'Document > Object'(node) {
+        const orphans = node.members.filter(m => !enKeys.has(m.name.value))
+        if (orphans.length === 0) return
+
+        const text = context.sourceCode.text
+        const parsed = JSON.parse(text)
+        for (const member of orphans) {
+          delete parsed[member.name.value]
+        }
+        const sortedRemaining = Object.keys(parsed).sort()
+        const cleaned = JSON.stringify(parsed, sortedRemaining, 2) + '\n'
+
+        for (const member of orphans) {
+          context.report({
+            node: member,
+            messageId: 'orphan',
+            data: { key: member.name.value },
+            fix(fixer) {
+              return fixer.replaceTextRange([0, text.length], cleaned)
+            },
+          })
+        }
+      },
+    }
+  },
+}

+ 36 - 0
libraries/eslint-plugin/no-straight-apostrophes-in-locales.js

@@ -0,0 +1,36 @@
+// Reject straight apostrophes (') in JSON string values. Straight apostrophes
+// in Angular templates can lead to XSS; use the right single quotation mark
+// ’ (U+2019) instead. See https://github.com/overleaf/issues/issues/4478
+
+module.exports = {
+  meta: {
+    type: 'problem',
+    fixable: 'code',
+    docs: {
+      description:
+        'Disallow straight apostrophes in JSON string values (typically locale files).',
+    },
+    schema: [],
+    messages: {
+      straightApostrophe:
+        "Locale value contains a straight apostrophe ('). Use the right single quotation mark ’ (U+2019) instead.",
+    },
+  },
+  create(context) {
+    return {
+      Member(node) {
+        if (node.value.type !== 'String') return
+        const original = node.value.value
+        if (!original.includes("'")) return
+        const fixed = original.replace(/'/g, '’')
+        context.report({
+          node: node.value,
+          messageId: 'straightApostrophe',
+          fix(fixer) {
+            return fixer.replaceText(node.value, JSON.stringify(fixed))
+          },
+        })
+      },
+    }
+  },
+}

+ 1 - 0
libraries/eslint-plugin/package.json

@@ -8,6 +8,7 @@
     "lodash": "^4.18.1"
   },
   "devDependencies": {
+    "@eslint/json": "^2.0.0",
     "@typescript-eslint/parser": "^8.59.4"
   },
   "peerDependencies": {

+ 125 - 0
libraries/eslint-plugin/rules.test.js

@@ -1,5 +1,6 @@
 const { RuleTester } = require('eslint')
 const tsParser = require('@typescript-eslint/parser')
+const json = require('@eslint/json').default
 const noThrowInCallback = require('./no-throw-in-callback')
 const preferKebabUrl = require('./prefer-kebab-url')
 const noUnnecessaryTrans = require('./no-unnecessary-trans')
@@ -7,6 +8,10 @@ const shouldUnescapeTrans = require('./should-unescape-trans')
 const noGeneratedEditorThemes = require('./no-generated-editor-themes')
 const viDoMockValidPath = require('./require-vi-doMock-valid-path')
 const requireCioSnakeCaseProperties = require('./require-cio-snake-case-properties')
+const noConsecutiveSpacesInLocales = require('./no-consecutive-spaces-in-locales')
+const noStraightApostrophesInLocales = require('./no-straight-apostrophes-in-locales')
+const frenchTypographyInLocales = require('./french-typography-in-locales')
+const sortedKeysInLocales = require('./sorted-keys-in-locales')
 
 const ruleTester = new RuleTester({
   languageOptions: {
@@ -16,6 +21,11 @@ const ruleTester = new RuleTester({
   },
 })
 
+const jsonRuleTester = new RuleTester({
+  plugins: { json },
+  language: 'json/json',
+})
+
 ruleTester.run('prefer-kebab-url', preferKebabUrl, {
   valid: [
     { code: `app.get('/foo-bar')` },
@@ -329,3 +339,118 @@ ruleTester.run('no-throw-in-callback', noThrowInCallback, {
     },
   ],
 })
+
+
+jsonRuleTester.run(
+  'no-consecutive-spaces-in-locales',
+  noConsecutiveSpacesInLocales,
+  {
+    valid: [
+      { code: '{ "key": "one space" }' },
+      { code: '{ "key": "no whitespace" }' },
+    ],
+    invalid: [
+      {
+        code: '{ "key": "two  spaces" }',
+        errors: [{ messageId: 'consecutiveSpaces' }],
+        output: '{ "key": "two spaces" }',
+      },
+      {
+        code: '{ "key": "three   spaces" }',
+        errors: [{ messageId: 'consecutiveSpaces' }],
+        output: '{ "key": "three spaces" }',
+      },
+      {
+        // \t then space → two consecutive whitespace chars
+        code: '{ "key": "tab\\t and" }',
+        errors: [{ messageId: 'consecutiveSpaces' }],
+        output: '{ "key": "tab and" }',
+      },
+    ],
+  }
+)
+
+jsonRuleTester.run(
+  'no-straight-apostrophes-in-locales',
+  noStraightApostrophesInLocales,
+  {
+    valid: [
+      { code: '{ "key": "no apostrophe" }' },
+      { code: '{ "key": "it’s curly" }' },
+    ],
+    invalid: [
+      {
+        code: `{ "key": "it's straight" }`,
+        errors: [{ messageId: 'straightApostrophe' }],
+        output: '{ "key": "it’s straight" }',
+      },
+    ],
+  }
+)
+
+jsonRuleTester.run('sorted-keys-in-locales', sortedKeysInLocales, {
+  valid: [
+    { code: '{\n  "a": "1",\n  "b": "2"\n}\n' },
+    { code: '{}' },
+    { code: '{ "only": "one" }' },
+  ],
+  invalid: [
+    {
+      code: '{\n  "b": "2",\n  "a": "1"\n}\n',
+      errors: [{ messageId: 'unsorted' }],
+      output: '{\n  "a": "1",\n  "b": "2"\n}\n',
+    },
+  ],
+})
+
+jsonRuleTester.run('french-typography-in-locales', frenchTypographyInLocales, {
+  valid: [
+    { code: '{ "key": "Bonjour ?" }' },
+    { code: '{ "key": "Liste :" }' },
+    { code: '{ "key": "« contenu »" }' },
+    { code: '{ "key": "abc 123" }' },
+    { code: '{ "key": "10 %" }' },
+  ],
+  invalid: [
+    {
+      code: '{ "key": "Bonjour?" }',
+      errors: 1,
+      output: '{ "key": "Bonjour ?" }',
+    },
+    {
+      code: '{ "key": "Bonjour ?" }',
+      errors: 1,
+      output: '{ "key": "Bonjour ?" }',
+    },
+    {
+      code: '{ "key": "Liste:" }',
+      errors: 1,
+      output: '{ "key": "Liste :" }',
+    },
+    {
+      code: '{ "key": "«contenu»" }',
+      errors: 2,
+      output: '{ "key": "« contenu »" }',
+    },
+    {
+      code: '{ "key": "10%" }',
+      errors: 1,
+      output: '{ "key": "10 %" }',
+    },
+    {
+      code: '{ "key": "sûr(e)" }',
+      errors: [
+        {
+          message:
+            'expected point médian "·" instead of "(e)" for inclusive writing',
+          suggestions: [
+            {
+              desc: 'Replace "(e)" with "·e"',
+              output: '{ "key": "sûr·e" }',
+            },
+          ],
+        },
+      ],
+    },
+  ],
+})

+ 43 - 0
libraries/eslint-plugin/sorted-keys-in-locales.js

@@ -0,0 +1,43 @@
+// Enforce alphabetically sorted top-level keys in JSON files.
+
+module.exports = {
+  meta: {
+    type: 'problem',
+    fixable: 'code',
+    docs: {
+      description:
+        'Require top-level keys to be sorted alphabetically in JSON files (typically locale files).',
+    },
+    schema: [],
+    messages: {
+      unsorted:
+        'Top-level keys are not sorted alphabetically. Key "{{key}}" should come before "{{predecessor}}".',
+    },
+  },
+  create(context) {
+    return {
+      'Document > Object'(node) {
+        const members = node.members
+        for (let i = 1; i < members.length; i++) {
+          const prev = members[i - 1].name.value
+          const curr = members[i].name.value
+          if (curr < prev) {
+            const text = context.sourceCode.text
+            const parsed = JSON.parse(text)
+            const sortedKeys = Object.keys(parsed).sort()
+            const sorted = JSON.stringify(parsed, sortedKeys, 2) + '\n'
+            context.report({
+              node: members[i],
+              messageId: 'unsorted',
+              data: { key: curr, predecessor: prev },
+              fix(fixer) {
+                return fixer.replaceTextRange([0, text.length], sorted)
+              },
+            })
+            return // one violation per file is enough; the fix re-sorts everything
+          }
+        }
+      },
+    }
+  },
+}

+ 0 - 3
services/web/Makefile

@@ -574,9 +574,6 @@ lint: check_extracted_translations
 check_extracted_translations:
 	bin/check_extracted_translations
 
-sort_locales:
-	 node scripts/translations/sort.js
-
 cleanup_unused_locales:
 	 node scripts/translations/cleanupUnusedLocales.js
 

+ 0 - 35
services/web/bin/lint_locales

@@ -2,43 +2,8 @@
 
 set -e
 
-# Ensure all locale files are sorted.
-node scripts/translations/sort.js --check
-
 # Ensure all locales are still in use
 node scripts/translations/cleanupUnusedLocales.js --check
 
 # Ensure all locales use the same variables
 node scripts/translations/checkVariables.js --ignore-orphaned-translations
-
-# Ensure no locales contain single quotes.
-LOCALES_WITH_SINGLE_QUOTE=$(\
-  grep \
-    --files-with-matches \
-    --recursive locales/ \
-    --regex "'" \
-    --exclude="README.md" \
-  || true
-)
-
-for file in ${LOCALES_WITH_SINGLE_QUOTE}; do
-  cat <<MSG >&2
-
-ERROR: $file contains a locale with a single quote.
-
----
-$(grep "$file" --regex "'")
----
-
-Using single quotes in locales can lead to Angular XSS.
-
-You will need to replace the quote with a similar looking character.
-’ (\u2019) is a good candidate.
-
-Links:
- - https://en.wikipedia.org/wiki/Right_single_quotation_mark
- - https://github.com/overleaf/issues/issues/4478
-
-MSG
-  exit 1
-done

+ 27 - 0
services/web/eslint.config.mjs

@@ -23,6 +23,7 @@ import n from 'eslint-plugin-n'
 import promise from 'eslint-plugin-promise'
 import typescriptEslint from '@typescript-eslint/eslint-plugin'
 import js from '@eslint/js'
+import json from '@eslint/json'
 import prettier from 'eslint-config-prettier/flat'
 import { fixupPluginRules } from '@eslint/compat'
 
@@ -57,6 +58,11 @@ export default defineConfig([
     files: ['**/*.{js,jsx,mjs,cjs,ts,tsx}'],
   },
   {
+    // Scope JS/TS-specific config to JS/TS files only so that built-in
+    // rules like `no-irregular-whitespace` aren't applied to JSON files
+    // linted via @eslint/json elsewhere in this config.
+    files: ['**/*.{js,jsx,mjs,cjs,ts,tsx}'],
+
     languageOptions: {
       parser: tsParser,
       // Default to node globals for all backend-style files; the
@@ -945,6 +951,27 @@ export default defineConfig([
   // preceding configs (eslint:recommended, @typescript-eslint, react,
   // jsx-a11y, ...).
   prettier,
+  {
+    // Lint locale JSON files for typographic and i18n conventions.
+    files: ['locales/*.json'],
+    language: 'json/json',
+    plugins: { json, '@overleaf': overleaf },
+    rules: {
+      '@overleaf/no-consecutive-spaces-in-locales': 'error',
+      '@overleaf/no-straight-apostrophes-in-locales': 'error',
+      '@overleaf/sorted-keys-in-locales': 'error',
+      '@overleaf/locale-variables-match-en': 'error',
+      '@overleaf/no-orphan-locale-keys': 'error',
+    },
+  },
+  {
+    files: ['locales/fr.json'],
+    language: 'json/json',
+    plugins: { json, '@overleaf': overleaf },
+    rules: {
+      '@overleaf/french-typography-in-locales': 'error',
+    },
+  },
   globalIgnores([
     '**/data/',
     'scripts/translations/.cache/',

+ 5 - 12
services/web/locales/da.json

@@ -89,7 +89,7 @@
   "adjust_column_width": "Justér kolonnebredde",
   "admin": "admin",
   "admin_panel": "Adminpanel",
-  "admin_user_created_message": "Administratorkonto oprettet,  <a href=\"__link__\">Log ind her</a> for at fortsætte",
+  "admin_user_created_message": "Administratorkonto oprettet, <a href=\"__link__\">Log ind her</a> for at fortsætte",
   "administration_and_security": "Administration og sikkerhed",
   "advanced_reference_search": "Avanceret <0>henvisningssøgning</0>",
   "advanced_reference_search_mode": "Avanceret henvisningssøgning",
@@ -107,7 +107,6 @@
   "all": "Alle",
   "all_borders": "Alle kanter",
   "all_premium_features": "Alle Premium-funktioner",
-  "all_premium_features_including": "Alle Premium-funktioner, inklusiv:",
   "all_prices_displayed_are_in_currency": "Alle priser er vist i __recommendedCurrency__.",
   "all_projects": "Alle projekter",
   "all_projects_will_be_transferred_immediately": "Alle projekter vil øjeblikkeligt blive overdraget til den nye ejer",
@@ -262,7 +261,6 @@
   "clsi_unavailable": "Beklager, kompileringsserveren til dit projekt var midlertidigt utilgængelig. Prøv igen om lidt.",
   "cn": "Kinesisk (forenklet)",
   "code_check_failed_explanation": "Din kode har fejl, der skal rettes før auto-kompileren kan køre",
-  "code_editor": "Kodeeditor",
   "collaborate_easily_on_your_projects": "Samarbejd nemt på dine projekter. Arbejd på længere eller mere komplekse dokumenter.",
   "collaborate_online_and_offline": "Samarbejd online og offline, med dit eget workflow",
   "collaboration": "Samarbejde",
@@ -504,7 +502,6 @@
   "email": "E-mail",
   "email_address": "E-mailadresse",
   "email_address_is_invalid": "Ugyldig e-mailadresse",
-  "email_already_associated_with": "E-mailadressen <b>__email1__</b> er allerede associeret med <b>__appName__</b>-kontoen <b>__email2__</b>.",
   "email_already_registered": "Denne e-mailadresse er allerede registreret",
   "email_already_registered_secondary": "Denne e-mailadresse er allerede registreret som en sekundær e-mailaddresse",
   "email_already_registered_sso": "Denne e-mailaddresse er allerede registeret. Log venligst ind på din konto på anden vis og tilknyt din konto til den nye udbyder via dine kontoindstillinger.",
@@ -681,7 +678,7 @@
   "github_is_no_longer_connected": "GitHub er ikke længere forbundet til dette projekt.",
   "github_is_premium": "GitHub synkronisering er en Premium-funktion",
   "github_large_files_error": "Merge mislykkedes: Dit GitHub reopsitory indeholder filer, som er større end grænsen på 50MB ",
-  "github_no_master_branch_error": "Dette repository kan ikke forbindes, da det ikke har nogen default branch.  Du må først sørge for, at projektet har en default branch",
+  "github_no_master_branch_error": "Dette repository kan ikke forbindes, da det ikke har nogen default branch. Du må først sørge for, at projektet har en default branch",
   "github_private_description": "Du vælger hvem der kan se, og committe til, dette repository.",
   "github_public_description": "Alle kan se dette repository. Du kan vælge hvem der kan comitte.",
   "github_repository_diverged": "Default branch i det forbundne repository er blevet force-push’et. Det kan desynkronisere Overleaf og GitHub at pull’e ændringer efter et force push. Det vil muligvis være nødvendigt at push’e ændringer efter pullet for blive synkroniseret igen.",
@@ -990,11 +987,9 @@
   "log_in": "Log ind",
   "log_in_and_link": "Log ind og forbind",
   "log_in_and_link_accounts": "Log ind og forbind konti",
-  "log_in_first_to_proceed": "Du kan først fortsætte, når du har <b>logget ind</b>.",
   "log_in_now": "Log ind nu",
   "log_in_with": "Log ind med __provider__",
   "log_in_with_a_different_account": "Log ind med en anden konto",
-  "log_in_with_email": "Log ind med __email__",
   "log_in_with_existing_institution_email": "For at forbinde din <0>__appName__</0>- og din <0>__institutionName__</0>-konto, er du nødt til først at logge ind med din eksisterende <0>__appName__</0>-konto.",
   "log_in_with_primary_email_address": "Dette bliver e-mailaddressen du skal bruge hvis du logger ind med e-mailaddresse og kode. Vigtige __appName__ beskeder vil blive sendt til denne adresse.",
   "log_in_with_sso": "Log ind med SSO",
@@ -1537,7 +1532,7 @@
   "saml_login_identity_mismatch_error": "Beklager, du prøver at logge ind i Overleaf som __email__ men identiteten modtaget fra din identity provider er ikke den korrekte for denne Overleaf konto.",
   "saml_login_identity_not_found_error": "Beklager, vi kunne ikke finde en Overleaf konto sat op for single sign-on med denne identity provider.",
   "saml_metadata": "Overleaf SAML Metadata",
-  "saml_missing_signature_error": "Beklager, informationen modtaget fra din identity provider er ikke signeret (både response og assertion signaturer er påkrævet).  Kontakt venligst din administrator for mere information.",
+  "saml_missing_signature_error": "Beklager, informationen modtaget fra din identity provider er ikke signeret (både response og assertion signaturer er påkrævet). Kontakt venligst din administrator for mere information.",
   "saml_response": "SAML svar",
   "save": "Gem",
   "save_20_percent": "spar 20%",
@@ -1713,7 +1708,7 @@
   "sso_is_enabled_explanation_2": "Hvis der er nogen problemer med konfigurationen, udelukkende dig (som gruppeadministrator) vil kunne deaktivere SSO.",
   "sso_link_account_with_idp": "Jeres gruppe bruger SSO. Det betyder at vi skal autentificere din konto med gruppens identity provider. Klik <0>Sæt SSO op</0> for at autentificere.",
   "sso_link_error": "Fejl i kontosammenkædningen",
-  "sso_link_invite_has_been_sent_to_email": "En påmindelse om SSO invitation er blevet sendt til  <0>__email__</0>",
+  "sso_link_invite_has_been_sent_to_email": "En påmindelse om SSO invitation er blevet sendt til <0>__email__</0>",
   "sso_login": "SSO login",
   "sso_logs": "SSO log",
   "sso_not_active": "SSO deaktiveret",
@@ -1863,7 +1858,6 @@
   "too_many_requests": "Der kom for mange forespørgsler inden for et kort tidsrum. Det kan hjælpe, hvis du venter lidt før du prøver igen.",
   "too_many_search_results": "Der var mere end 100 resultater. Indskrænk venligst din søgning.",
   "too_recently_compiled": "Dette projekt er lige blevet kompileret, hvorfor denne kompilering er blevet udsat.",
-  "toolbar_choose_section_heading_level": "Vælg overskriftsniveau",
   "toolbar_decrease_indent": "Formindsk indryk",
   "toolbar_increase_indent": "Forøg indryk",
   "toolbar_insert_citation": "Indsæt citation",
@@ -1950,7 +1944,7 @@
   "unlink_dropbox_folder": "Afkobl Dropbox konto",
   "unlink_dropbox_warning": "Alle de projekter du har synkroniseret med Dropbox, afkobles og synkroniseres ikke længere med Dropbox. Er du sikker på at du vil afkoble din Dropbox konto?",
   "unlink_github_repository": "Afkobl GitHub Repository",
-  "unlink_github_warning": "Alle de projekter, som du har synkroniseret med GitHub, afkobles og synkroniseres ikke længere  med GitHub. Er du sikker på du vil afkoble din GitHub konto?",
+  "unlink_github_warning": "Alle de projekter, som du har synkroniseret med GitHub, afkobles og synkroniseres ikke længere med GitHub. Er du sikker på du vil afkoble din GitHub konto?",
   "unlink_linked_accounts": "Afkobl alle tilknyttede konti (så som ORCID ID, IEEE). <0>Fjern dem i kontoindstillingerne (under forbundne konti).</0>",
   "unlink_linked_google_account": "Afkobl din Google konto. <0>Fjern den i kontoindstillingerne (under forbundne konti).</0>",
   "unlink_provider_account_title": "Afkobl __provider__ konto",
@@ -2037,7 +2031,6 @@
   "view_your_invoices": "Se dine fakturaer",
   "viewer": "Læser",
   "viewing_x": "Ser <0>__endTime__</0>",
-  "visual_editor": "Visuel editor",
   "want_access_to_overleaf_premium_features_through_your_university": "Vil du have adgang til __appName__ Premium-funktioner gennem dit universitet?",
   "want_change_to_apply_before_plan_end": "Hvis du ønsker at denne ændring skal tage effekt før slutningen på din nuværende faktureringsperiode, kontakt os venligst.",
   "we_are_unable_to_opt_you_into_this_experiment": "Vi kan ikke melde dig til dette eksperiment lige nu. Tjek venligst at din organisation tillader denne funktion, eller prøv igen senere.",

+ 0 - 24
services/web/locales/de.json

@@ -15,7 +15,6 @@
   "Terms": "Nutzungsbedingungen",
   "Universities": "Universitäten",
   "a_custom_size_has_been_used_in_the_latex_code": "Es wurde eine benutzerdefinierte Größe im LaTeX Code verwendet.",
-  "a_dashboard_that_follows_your_lead": "Ein Dashboard, das sich nach dir richtet",
   "a_file_with_that_name_already_exists_and_will_be_overriden": "Eine Datei mit diesem Name existiert bereits. Die Datei wird überschrieben.",
   "a_more_comprehensive_list_of_keyboard_shortcuts": "Eine vollständige Liste der Tastaturbelegung befindet sich in <0>dieser __appName__ Projekt Vorlage</0>",
   "a_new_reference_was_added": "Eine neue Referenz wurde hinzugefügt",
@@ -93,8 +92,6 @@
   "add_email": "E-Mail-Adresse hinzufügen",
   "add_email_address": "E-Mail-Adresse hinzufügen",
   "add_email_to_claim_features": "Füge eine institutionelle E-Mail-Adresse hinzu, um deine Funktionen zu freizuschalten.",
-  "add_error_assist_annual_to_your_projects": "Füge die Jährliche Fehlerhilfe <sparkle/> zu deinen Projekten hinzu und erhalte unbegrenzte KI-Unterstützung, um LaTeX-Fehler schneller zu beheben.",
-  "add_error_assist_to_your_projects": "Füge die Fehlerhilfe <sparkle/> zu deinen Projekten hinzu und erhalte unbegrenzte KI-Unterstützung, um LaTeX-Fehler schneller zu beheben.",
   "add_files": "Dateien hinzufügen",
   "add_manager_user_not_found": "Diese E-Mail-Adresse ist nicht bei Overleaf registriert. Bitte die Person, die du als Verwalter hinzufügen möchtest, ein Overleaf-Konto zu erstellen, falls sie noch keines hat. Verwende beim Hinzufügen eines Verwalters eine der E-Mail-Adressen, die mit dessen Konto verknüpft sind.",
   "add_more_collaborators": "Weitere Mitarbeiter hinzufügen",
@@ -152,7 +149,6 @@
   "all_features_in_group_standard_plus": "Alle Funktionen von Group Standard, plus:",
   "all_logs": "Alle Logs",
   "all_premium_features": "Alle Premiumfunktionen",
-  "all_premium_features_including": "Alle Premiumfunktionen, darunter:",
   "all_prices_displayed_are_in_currency": "Alle Preise sind in __recommendedCurrency__ angegeben.",
   "all_project_activity": "Alle Projektaktivitäten",
   "all_project_activity_description": "Du wirst über alle Kommentare und Nachverfolgung von Änderungen benachrichtigt.",
@@ -199,7 +195,6 @@
   "ascending": "Aufsteigend",
   "ask_proj_owner_to_unlink_from_current_github": "Bitte den Projektbesitzer (<0>__projectOwnerEmail__</0>), das Projekt vom aktuellen GitHub-Repository zu entkoppeln und eine Verbindung zu einem anderen herzustellen.",
   "ask_proj_owner_to_upgrade_for_full_history": "Bitte den Projektbesitzer um ein Abonnement-Upgrade, um auf den vollständigen Verlauf dieses Projekts zugreifen zu können.",
-  "ask_proj_owner_to_upgrade_for_references_search": "Bitte den Projektbesitzer um ein Abonnement-Upgrade, damit du die Referenzsuche verwenden kannst.",
   "ask_repo_owner_to_reconnect": "Bitte den Besitzer des GitHub-Repositorys (<0>__repoOwnerEmail__</0>), das Projekt neu zu verbinden.",
   "ask_repo_owner_to_renew_overleaf_subscription": "Bitte den Besitzer des GitHub-Repositorys (<0>__repoOwnerEmail__</0>), sein __appName__-Abonnement zu erneuern und das Projekt neu zu verbinden.",
   "at_most_x_libraries_can_be_selected": "Es können höchstens __maxCount__ Bibliotheken ausgewählt werden",
@@ -217,7 +212,6 @@
   "automatic_user_registration_uppercase": "Automatische Benutzerregistrierung",
   "automatically_insert_closing_brackets_and_parentheses": "Schließende eckige und runde Klammern automatisch einfügen",
   "automatically_recompile_the_project_as_you_edit": "Das Projekt während der Bearbeitung automatisch neu kompilieren",
-  "available_with_group_professional": "Verfügbar mit Group Professional",
   "back": "Zurück",
   "back_to_account_settings": "Zurück zu den Kontoeinstellungen",
   "back_to_editor": "Zurück zum Editor",
@@ -282,11 +276,7 @@
   "certificate": "Zertifikat",
   "change": "Ändern",
   "change_currency": "Währung ändern",
-  "change_email": "E-Mail ändern",
   "change_language": "Sprache ändern",
-  "change_or_cancel-cancel": "Abbrechen",
-  "change_or_cancel-change": "Ändern",
-  "change_or_cancel-or": "oder",
   "change_owner": "Besitzer ändern",
   "change_password": "Passwort ändern",
   "change_plan": "Abonnement ändern",
@@ -323,7 +313,6 @@
   "clsi_unavailable": "Entschuldigung, der Kompilierserver für dein Projekt war vorübergehend nicht verfügbar. Versuche es in einigen Augenblicken erneut.",
   "cn": "Chinesisch (vereinfacht)",
   "code_check_failed_explanation": "Dein Code enthält Fehler, die behoben werden müssen, bevor das automatische Kompilieren fortgefahren werden kann",
-  "code_editor": "Code-Editor",
   "collaborate_online_and_offline": "Zusammenarbeit online und offline mit deinem eigenen Workflow",
   "collaboration": "Zusammenarbeit",
   "collaborator": "Mitarbeiter",
@@ -380,7 +369,6 @@
   "cookie_banner": "Cookie-Banner",
   "copied": "Kopiert",
   "copy": "Kopieren",
-  "copy_code": "Code kopieren",
   "copy_project": "Projekt kopieren",
   "copy_response": "Antwort kopieren",
   "copying": "kopieren",
@@ -393,7 +381,6 @@
   "create_a_new_password_for_your_account": "Erstelle ein neues Passwort für dein Konto",
   "create_a_new_project": "Erstelle ein neues Projekt",
   "create_account": "Konto erstellen",
-  "create_an_account": "Konto erstellen",
   "create_first_admin_account": "Erstelle das erste Admin-Konto",
   "create_new_account": "Neues Konto erstellen",
   "create_new_subscription": "Neues Abonnement erstellen",
@@ -521,7 +508,6 @@
   "email": "E-Mail",
   "email_address": "E-Mail-Adresse",
   "email_address_is_invalid": "Die E-Mail-Adresse ist ungültig",
-  "email_already_associated_with": "Die E-Mail-Adresse <b>__email1__</b> ist bereits mit dem Konto <b>__email2__</b> <b>__appName__</b> verknüpft.",
   "email_already_registered": "Diese E-Mail-Adresse ist bereits registriert.",
   "email_already_registered_secondary": "Diese E-Mail-Adresse ist bereits als sekundäre E-Mail-Adresse registriert",
   "email_already_registered_sso": "Diese E-Mail-Adresse wurde bereits registriert. Bitte logge dich auf einem anderen Weg in dein Konto ein und verknüpfe dein Konto über deine Kontoeinstellungen mit dem neuen Anbieter.",
@@ -545,7 +531,6 @@
   "enabling": "Wird aktiviert",
   "end_of_document": "Ende des Dokuments",
   "ensure_recover_account": "So kann sie zur Wiederherstellung deines __appName__-Kontos verwendet werden, falls du den Zugang zu deiner primären E-Mail-Adresse verlierst.",
-  "enter_image_url": "Bild-URL eingeben",
   "enter_the_code": "Gib den 6-stelligen Code ein, der an __email__ gesendet wurde.",
   "enter_the_confirmation_code": "Gib den 6-stelligen Bestätigungscode ein, der an <0>__email__</0> gesendet wurde.",
   "enter_your_email": "Gib deine E-Mail ein",
@@ -568,7 +553,6 @@
   "export_csv": "CSV-Datei exportieren",
   "export_project_to_github": "Projekt nach GitHub exportieren",
   "failed_to_send_managed_user_invite_to_email": "Der Versand der Einladung für Verwaltete Benutzer an <0>__email__</0> hat nicht funktioniert. Bitte versuche es später noch einmal.",
-  "fancy_going_dark": "Lust auf den Dark Mode?",
   "faq_how_does_free_trial_works_answer": "Während deines __len__-tägigen Probe-Abonnements erhältst du vollen Zugriff auf die Funktionen des von dir gewählten __appName__-Abonnements. Es besteht keine Verpflichtung, über die Testperiode hinaus fortzufahren. Deine Karte wird am Ende des __len__-tägigen Testzeitraums belastet, sofern du nicht vorher gekündigt hast. Um zu kündigen, gehe zu deinen Abonnementeinstellungen in deinem Konto.",
   "fast": "Schnell",
   "fast_draft": "Schnell [Entwurf]",
@@ -685,7 +669,6 @@
   "go_to_overleaf": "Zu Overleaf gehen",
   "go_to_pdf_location_in_code": "Gehe zum Code an der PDF-Position",
   "go_to_settings": "Zu den Kontoeinstellungen",
-  "got_it": "Verstanden",
   "group_admin": "Gruppenadministrator",
   "group_full": "Diese Gruppe ist bereits voll",
   "group_managed_by_group_administrator": "Die Benutzerkonten in diesem Team werden vom Gruppenadministrator verwaltet.",
@@ -922,9 +905,7 @@
   "log_in": "Anmelden",
   "log_in_and_link": "Anmelden und verknüpfen",
   "log_in_and_link_accounts": "Anmelden und Konten verknüpfen",
-  "log_in_first_to_proceed": "Du musst dich zuerst <b>anmelden</b>, um fortzufahren.",
   "log_in_with": "Einloggen mit __provider__",
-  "log_in_with_email": "Melde dich mit __email__ an",
   "log_in_with_existing_institution_email": "Bitte melde dich mit deinem bestehenden <0>__appName__</0>-Konto an, um deine institutionellen Konten <0>__appName__</0> und <0>__institutionName__</0> zu verknüpfen.",
   "log_in_with_primary_email_address": "Das ist die E-Mail-Adresse, die du verwendest, wenn du dich mit E-Mail-Adresse und Passwort anmeldest. Wichtige __appName__-Benachrichtigungen werden an diese E-Mail-Adresse gesendet.",
   "log_out": "Abmelden",
@@ -979,7 +960,6 @@
   "maximum_files_uploaded_together": "Maximal __max__ Dateien zusammen hochgeladen",
   "may": "Mai",
   "maybe_later": "Vielleicht später",
-  "meet_the_new_dark_dashboard": "Das neue dunkle Dashboard",
   "members_management": "Mitgliederverwaltung",
   "mendeley": "Mendeley",
   "mendeley_groups_loading_error": "Beim Laden von Gruppen von Mendeley ist ein Fehler aufgetreten",
@@ -1012,7 +992,6 @@
   "need_contact_group_admin_to_make_changes": "Du musst deinen Gruppenadministrator kontaktieren, wenn du bestimmte Änderungen an deinem Konto vornehmen möchtest. <0>Erfahre mehr über verwaltete Benutzer.</0>",
   "need_to_add_new_primary_before_remove": "Du musst eine neue primäre E-Mail-Adresse hinzufügen, bevor du diese entfernen kannst.",
   "need_to_leave": "Du musst gehen?",
-  "new_editor": "Neuer Editor",
   "new_file": "Neue Datei",
   "new_folder": "Neuer Ordner",
   "new_name": "Neuer Name",
@@ -1435,14 +1414,12 @@
   "synctex_failed": "Die entsprechende Quelldatei konnte nicht gefunden werden",
   "syntax_validation": "Syntaxüberprüfung",
   "tab_connecting": "Verbindung mit dem Editor wird hergestellt",
-  "tab_no_longer_connected": "Dieser Browser Tab ist nicht mehr mit dem Editor verbunden",
   "tabs_open_in_preview_mode_until_you_interact_with_them": "Tabs werden im Vorschaumodus geöffnet, bis du mit ihnen interagierst",
   "tag_color": "Stichwortfarbe",
   "tag_name_cannot_exceed_characters": "Der Stichwortname darf __maxLength__ Zeichen nicht überschreiten",
   "tag_name_is_already_used": "Das Stichwort „__tagName__“ existiert bereits",
   "tags": "Stichworte",
   "take_me_home": "Bring mich nach Hause!",
-  "take_short_survey": "Nimm an einer kurzen Umfrage teil",
   "take_survey": "An Umfrage teilnehmen",
   "template": "Vorlage",
   "template_approved_by_publisher": "Diese Vorlage wurde vom Verlag genehmigt",
@@ -1621,7 +1598,6 @@
   "website_status": "Website-Status",
   "wed_love_you_to_stay": "Wir würden uns freuen, wenn du bleibst",
   "welcome_to_sl": "Willkommen bei __appName__",
-  "welcome_to_the_dark_side": "Willkommen auf der dunklen Seite",
   "wide": "Weit",
   "will_need_to_log_out_from_and_in_with": "Du musst dich von deinem <b>__email1__</b>-Konto <b>abmelden</b> und dich dann mit <b>__email2__</b> anmelden.",
   "word_count": "Wortanzahl",

+ 11 - 11
services/web/locales/en.json

@@ -312,7 +312,7 @@
   "by_sharing_link": "By sharing link",
   "by_subscribing_you_agree_to_our_terms_of_service": "By subscribing, you agree to our <0>terms of service</0>.",
   "can_link_institution_email_acct_to_institution_acct": "You can now link your <b>__email__</b> <b>__appName__</b> account to your <b>__institutionName__</b> institutional account.",
-  "can_link_institution_email_by_clicking": "You can link your <b>__email__</b> <b>__appName__</b> account to your  <b>__institutionName__</b> account by clicking <b>__clickText__</b>.",
+  "can_link_institution_email_by_clicking": "You can link your <b>__email__</b> <b>__appName__</b> account to your <b>__institutionName__</b> account by clicking <b>__clickText__</b>.",
   "can_link_institution_email_to_login": "You can link your <b>__email__</b> <b>__appName__</b> account to your <b>__institutionName__</b> account, which will allow you to log in to <b>__appName__</b> through your institution and will reconfirm your institutional email address.",
   "can_link_your_institution_acct_2": "You can now <0>link</0> your <0>__appName__</0> account to your <0>__institutionName__</0> institutional account.",
   "can_now_relink_dropbox": "You can now <0>relink your Dropbox account</0>.",
@@ -1224,7 +1224,7 @@
   "imported_from_mendeley_at_date": "Imported from Mendeley at __formattedDate__ __relativeDate__",
   "imported_from_papers_at_date": "Imported from Papers at __formattedDate__ __relativeDate__",
   "imported_from_the_output_of_another_project_at_date": "Imported from the output of <0>Another project</0>: __sourceOutputFilePathHTML__, at __formattedDate__ __relativeDate__",
-  "imported_from_zotero_at_date": "Imported from Zotero  at __formattedDate__ __relativeDate__",
+  "imported_from_zotero_at_date": "Imported from Zotero at __formattedDate__ __relativeDate__",
   "importing": "Importing",
   "importing_and_merging_changes_in_github": "Importing and merging changes in GitHub",
   "in_order_to_have_a_secure_account_make_sure_your_password": "To help keep your account secure, make sure your new password:",
@@ -1278,7 +1278,7 @@
   "institution_account": "Institution Account",
   "institution_account_tried_to_add_affiliated_with_another_institution_2": "The email <b>__email__</b> is <b>already associated</b> with your account but affiliated with another institution.",
   "institution_account_tried_to_add_already_linked": "This institution is <b>already linked</b> with your account via another email address.",
-  "institution_account_tried_to_add_already_registered": "The email/institution account you tried to add is <b>already registered</b> with  __appName__.",
+  "institution_account_tried_to_add_already_registered": "The email/institution account you tried to add is <b>already registered</b> with __appName__.",
   "institution_account_tried_to_add_not_affiliated_2": "The email address provided to us when you logged in was not an institutional email address. Your institution provided the email <b>__email__</b>, which is not considered an institutional email. Overleaf currently requires that an institutional email address is sent as part of the SSO login. Please follow up with your IT Services team to ensure that an institutional address is provided at login time.",
   "institution_account_tried_to_confirm_saml": "This email cannot be confirmed. Please remove the email from your account and try adding it again.",
   "institution_acct_successfully_linked_2": "Your <0>__appName__</0> account was successfully linked to your <0>__institutionName__</0> institutional account.",
@@ -1499,7 +1499,7 @@
   "log_in_now": "Log in now",
   "log_in_with": "Log in with __provider__",
   "log_in_with_a_different_account": "Log in with a different account",
-  "log_in_with_existing_institution_email": "Please log in with your existing <0>__appName__</0> account in order to get your <0>__appName__</0>  and <0>__institutionName__</0> institutional accounts linked.",
+  "log_in_with_existing_institution_email": "Please log in with your existing <0>__appName__</0> account in order to get your <0>__appName__</0> and <0>__institutionName__</0> institutional accounts linked.",
   "log_in_with_primary_email_address": "This will be the email address to use if you log in with an email address and password. Important __appName__ notifications will be sent to this email address.",
   "log_in_with_sso": "Log in with SSO",
   "log_in_with_sso_email": "Work or university email address",
@@ -1956,7 +1956,7 @@
   "please_select_a_project": "Please Select a Project",
   "please_select_an_output_file": "Please Select an Output File",
   "please_set_a_password": "Please set a password",
-  "please_set_main_file": "Please choose the main file for this project in the project menu.  ",
+  "please_set_main_file": "Please choose the main file for this project in the project menu. ",
   "please_wait": "Please wait",
   "plus_additional_collaborators_and_more": "(plus additional collaborators, higher AI allowance, track changes, and more).",
   "plus_more": "plus more",
@@ -2241,7 +2241,7 @@
   "run_current_script_to_see_output": "Run the current script to see output.",
   "run_python_code": "Run Python code",
   "saml": "SAML",
-  "saml_auth_error": "Sorry, your identity provider responded with an error.  Please contact your administrator for more information.",
+  "saml_auth_error": "Sorry, your identity provider responded with an error. Please contact your administrator for more information.",
   "saml_authentication_required_error": "Other login methods have been disabled by your group administrator. Please use your group SSO login.",
   "saml_commons_reconfirmation_unable_to_find_user": "Sorry, we were unable to confirm your affiliation. This may have been caused by a change in the SSO data that is being sent to us from your institution. Please <0>contact us</0> for help with relinking your Overleaf account to your institution’s SSO.",
   "saml_commons_unavailable": "Institution SSO is currently unavailable. For more details, please see <0>__linkText__</0>.",
@@ -2249,15 +2249,15 @@
   "saml_email_not_in_account_error_managed_users_2": "Your email address <0>__userEmail__</0> was not recognized. Your organization’s identity provider returned <1>__samlEmail__</1>. You will need to log out of <0>__userEmail__</0> and use <1>__samlEmail__</1> to create an account via SSO. You can <2>transfer your existing projects</2> to the new account.",
   "saml_email_not_recognized_error": "This email address isn’t set up for SSO. Please check it and try again or contact your administrator.",
   "saml_email_on_another_account_error": "Your identity provider returned __email__, which is on another __appName__ account. Please log out and log in with the other account to join your __teamName__ enterprise group.",
-  "saml_identity_exists_error": "Sorry, the identity returned by your identity provider is already linked with a different Overleaf account.  Please contact your administrator for more information.",
-  "saml_invalid_signature_error": "Sorry, the information received from your identity provider has an invalid signature.  Please contact your administrator for more information.",
-  "saml_login_disabled_error": "Sorry, single sign-on login has been disabled for __email__.  Please contact your administrator for more information.",
-  "saml_login_failure": "Sorry, there was a problem logging you in.  Please contact your administrator for more information.",
+  "saml_identity_exists_error": "Sorry, the identity returned by your identity provider is already linked with a different Overleaf account. Please contact your administrator for more information.",
+  "saml_invalid_signature_error": "Sorry, the information received from your identity provider has an invalid signature. Please contact your administrator for more information.",
+  "saml_login_disabled_error": "Sorry, single sign-on login has been disabled for __email__. Please contact your administrator for more information.",
+  "saml_login_failure": "Sorry, there was a problem logging you in. Please contact your administrator for more information.",
   "saml_login_identity_mismatch_error": "Sorry, you are trying to log in to Overleaf as __email__ but the identity returned by your identity provider is not the correct one for this Overleaf account.",
   "saml_login_identity_not_found_error": "Sorry, we were not able to find an Overleaf account set up for single sign-on with this identity provider.",
   "saml_login_requested_email_not_confirmed_error": "Sorry, the email address you requested to use for login has not been confirmed and we cannot complete your SSO log in request. If you have another method to log in please log in and then confirm your email. Please <0>contact us</0> if you need assistance.",
   "saml_metadata": "Overleaf SAML Metadata",
-  "saml_missing_signature_error": "Sorry, the information received from your identity provider is not signed (both response and assertion signatures are required).  Please contact your administrator for more information.",
+  "saml_missing_signature_error": "Sorry, the information received from your identity provider is not signed (both response and assertion signatures are required). Please contact your administrator for more information.",
   "saml_missing_user_attribute": "There was a problem with your Single Sign-on authentication. Your organization did not provide the required information to complete authentication. Please contact your administrator for more information.",
   "saml_request_denied_error": "Sorry, your identity provider denied the request to authenticate you. Please contact your administrator for more information.",
   "saml_response": "SAML Response",

+ 12 - 13
services/web/locales/es.json

@@ -20,7 +20,7 @@
   "about_to_delete_tag": "Estás a punto de eliminar las siguientes etiquetas (no se eliminarán los proyectos en las mismas):",
   "about_to_delete_the_following_project": "Estás a punto de eliminar el siguiente proyecto:",
   "about_to_delete_the_following_projects": "Estás a punto de eliminar los siguientes proyectos:",
-  "about_to_delete_user_preamble": "Estás a punto de eliminar  __userName__ (__userEmail__). Hacer esto significará:",
+  "about_to_delete_user_preamble": "Estás a punto de eliminar __userName__ (__userEmail__). Hacer esto significará:",
   "about_to_enable_managed_users": "Al activar la función Usuarios administrados, todos los miembros existentes de su suscripción de grupo serán invitados a convertirse en administrados. Esto te dará derechos de administrador sobre sus cuentas. También tendrás la opción de invitar a nuevos miembros a unirse a la suscripción y convertirse en administrados.",
   "about_to_leave_project": "Estás a punto de abandonar este proyecto.",
   "about_to_leave_projects": "Estás apunto de abandonar los siguientes proyectos:",
@@ -41,7 +41,7 @@
   "account_has_past_due_invoice_change_plan_warning": "Su cuenta tiene actualmente una factura vencida. No podrás cambiar de plan hasta que esto se resuelva.",
   "account_linking": "Vinculación de cuentas",
   "account_managed_by_group_administrator": "Su cuenta está administrada por el administrador de su grupo (__admin__)",
-  "account_not_linked_to_dropbox": "Tu cuenta no está   conectada con Dropbox",
+  "account_not_linked_to_dropbox": "Tu cuenta no está conectada con Dropbox",
   "account_settings": "Opciones de la cuenta",
   "account_with_email_exists": "Parece que una cuenta <b>__appName__</b> con el email <b>__email__</b> ya existe.",
   "acct_linked_to_institution_acct_2": "Puedes <b>unirte</b> a Overleaf a través de tu login institucional de <b>__institutionName__</b>",
@@ -98,7 +98,6 @@
   "all": "Todos",
   "all_borders": "Todos los bordes",
   "all_premium_features": "Todas las características premium",
-  "all_premium_features_including": "Todas las características premium, incluyendo:",
   "all_prices_displayed_are_in_currency": "Todos los precios mostrados son en __recommendedCurrency__.",
   "all_projects": "Todos los proyectos",
   "all_projects_will_be_transferred_immediately": "Todos los proyectos se transferirán inmediatamente al nuevo propietario.",
@@ -108,7 +107,7 @@
   "already_have_an_account": "¿Ya tiene una cuenta?",
   "already_have_sl_account": "¿Ya tienes una cuenta de __appName__?",
   "also": "También",
-  "alternatively_create_new_institution_account": "Alternativamente, puede crear una  <b>nueva cuenta</b> con su correo institucional (<b>__email__</b>) haciendo click en <b>__clickText__</b>.",
+  "alternatively_create_new_institution_account": "Alternativamente, puede crear una <b>nueva cuenta</b> con su correo institucional (<b>__email__</b>) haciendo click en <b>__clickText__</b>.",
   "an_email_has_already_been_sent_to": "Ya se ha enviado un correo electrónico a <0>__email__</0>. Espere e inténtelo de nuevo más tarde.",
   "an_error_occured_while_restoring_project": "Se ha producido un error al restaurar el proyecto",
   "an_error_occurred_when_verifying_the_coupon_code": "Se ha producido un error al verificar el código del cupón",
@@ -172,9 +171,9 @@
   "by_joining_labs": "Al unirte a Labs, aceptas recibir ocasionalmente correos electrónicos y actualizaciones de Overleaf, por ejemplo, para solicitar tu opinión. También acepta nuestras <0>condiciones del servicio</0> y nuestro <1>aviso de privacidad</1>.",
   "by_registering_you_agree_to_our_terms_of_service": "Al registrarse, acepta nuestras <0>condiciones del servicio</0> y <1>notificación de privacidad</1>.",
   "by_subscribing_you_agree_to_our_terms_of_service": "Al suscribirse, acepta nuestras <0>condiciones del servicio</0>.",
-  "can_link_institution_email_acct_to_institution_acct": "Ahora puedes vincular tu cuenta  <b>__appName__</b> <b>__email__</b> a tu cuenta institucional <b>__institutionName__</b>.",
-  "can_link_institution_email_by_clicking": "Puedes vincular tu cuenta  <b>__appName__</b> <b>__email__</b> a tu cuenta institucional <b>__institutionName__</b> haciendo click en <b>__clickText__</b>.",
-  "can_link_institution_email_to_login": "Puedes vincular tu cuenta  <b>__appName__</b> <b>__email__</b> a tu cuenta institucional <b>__institutionName__</b>, lo cual te permitirá entrar en <b>__appName__</b> a través de tu institución y confirmará de nuevo tu cuenta de correo institucional.",
+  "can_link_institution_email_acct_to_institution_acct": "Ahora puedes vincular tu cuenta <b>__appName__</b> <b>__email__</b> a tu cuenta institucional <b>__institutionName__</b>.",
+  "can_link_institution_email_by_clicking": "Puedes vincular tu cuenta <b>__appName__</b> <b>__email__</b> a tu cuenta institucional <b>__institutionName__</b> haciendo click en <b>__clickText__</b>.",
+  "can_link_institution_email_to_login": "Puedes vincular tu cuenta <b>__appName__</b> <b>__email__</b> a tu cuenta institucional <b>__institutionName__</b>, lo cual te permitirá entrar en <b>__appName__</b> a través de tu institución y confirmará de nuevo tu cuenta de correo institucional.",
   "can_now_relink_dropbox": "Ya puedes <0>vincular de nuevo tu cuenta de Dropbox</0>.",
   "cancel": "Cancelar",
   "cancel_my_account": "Cancelar mi suscripción",
@@ -514,15 +513,15 @@
   "ro": "Rumano",
   "role": "Rol",
   "ru": "Ruso",
-  "saml_auth_error": "Lo sentimos, su proveedor de identidad respondió con un error.  Póngase en contacto con su administrador para obtener más información.",
+  "saml_auth_error": "Lo sentimos, su proveedor de identidad respondió con un error. Póngase en contacto con su administrador para obtener más información.",
   "saml_email_not_recognized_error": "Esta dirección de correo electrónico no está configurada para SSO. Por favor, compruébelo e inténtelo de nuevo o póngase en contacto con su administrador.",
-  "saml_identity_exists_error": "Lo sentimos, la identidad devuelta por su proveedor de identidad ya está vinculada con una cuenta Overleaf diferente.  Póngase en contacto con su administrador para obtener más información.",
-  "saml_invalid_signature_error": "Lo sentimos, la información recibida de su proveedor de identidad tiene una firma no válida.  Póngase en contacto con su administrador para obtener más información.",
-  "saml_login_disabled_error": "Lo sentimos, el inicio de sesión único (SSO) se ha desactivado para __email__.  Póngase en contacto con su administrador para obtener más información.",
-  "saml_login_failure": "Lo sentimos, ha habido un problema al iniciar sesión.  Póngase en contacto con su administrador para obtener más información.",
+  "saml_identity_exists_error": "Lo sentimos, la identidad devuelta por su proveedor de identidad ya está vinculada con una cuenta Overleaf diferente. Póngase en contacto con su administrador para obtener más información.",
+  "saml_invalid_signature_error": "Lo sentimos, la información recibida de su proveedor de identidad tiene una firma no válida. Póngase en contacto con su administrador para obtener más información.",
+  "saml_login_disabled_error": "Lo sentimos, el inicio de sesión único (SSO) se ha desactivado para __email__. Póngase en contacto con su administrador para obtener más información.",
+  "saml_login_failure": "Lo sentimos, ha habido un problema al iniciar sesión. Póngase en contacto con su administrador para obtener más información.",
   "saml_login_identity_mismatch_error": "Lo sentimos, estás intentando iniciar sesión en Overleaf como __email__ pero la identidad devuelta por tu proveedor de identidad no es la correcta para esta cuenta de Overleaf.",
   "saml_login_identity_not_found_error": "Lo sentimos, no hemos podido encontrar una cuenta de Overleaf configurada para el inicio de sesión único con este proveedor de identidad.",
-  "saml_missing_signature_error": "Lo sentimos, la información recibida de su proveedor de identidad no está firmada (se requieren las firmas de respuesta y de aserción).  Póngase en contacto con su administrador para obtener más información.",
+  "saml_missing_signature_error": "Lo sentimos, la información recibida de su proveedor de identidad no está firmada (se requieren las firmas de respuesta y de aserción). Póngase en contacto con su administrador para obtener más información.",
   "saving": "Guardando",
   "saving_notification_with_seconds": "Guardando __docname__... (__seconds__ segundos de cambios no guardados)",
   "search_bib_files": "Buscar por autor, título, año",

+ 80 - 86
services/web/locales/fr.json

@@ -16,15 +16,15 @@
   "about": "À propos",
   "about_to_archive_projects": "Vous êtes sur le point d’archiver les projets suivants :",
   "about_to_delete_cert": "Vous allez supprimer le certificat suivant :",
-  "about_to_delete_projects": "Vous allez supprimer les projets suivants :",
+  "about_to_delete_projects": "Vous allez supprimer les projets suivants :",
   "about_to_delete_tag": "Vous êtes sur le point de supprimer l’étiquette suivante (tout projet avec cette étiquette ne sera pas supprimé) :",
   "about_to_delete_the_following_project": "Vous allez supprimer le projet suivant",
   "about_to_delete_the_following_projects": "Vous allez supprimer les projets suivants",
   "about_to_delete_user_preamble": "Vous êtes sur le point de supprimer __userName__ (__userEmail__). Ceci signifie :",
   "about_to_enable_managed_users": "En activant la fonctionnalité Gestion des Utilisateur·rice·s, tous les membres existants de votre abonnement de groupe seront invités à devenir administrés. Cela vous donnera des droits d’administrateur sur leur compte. Vous aurez également la possibilité d’inviter de nouveaux membres à rejoindre l’abonnement et à devenir administrés.",
   "about_to_leave_project": "Vous êtes sur le point de quitter ce projet.",
-  "about_to_leave_projects": "Vous allez quitter les projets suivants :",
-  "about_to_trash_projects": "Vous êtes sur le point de mettre à la corbeille les projets suivants:",
+  "about_to_leave_projects": "Vous allez quitter les projets suivants :",
+  "about_to_trash_projects": "Vous êtes sur le point de mettre à la corbeille les projets suivants :",
   "abstract": "Résumé",
   "accept": "Accepter",
   "accept_and_continue": "Accepter et continuer",
@@ -96,11 +96,10 @@
   "ai_feedback_the_suggestion_wasnt_the_best_fix_available": "La suggestion n’était pas la meilleure solution disponible",
   "ai_feedback_there_was_no_code_fix_suggested": "Aucune correction de code n’a été suggérée",
   "all_premium_features": "Toutes les fonctionnalités premium",
-  "all_premium_features_including": "Toutes les fonctionnalités premium, comprenant:",
   "all_prices_displayed_are_in_currency": "Tous les prix affichés sont en __recommendedCurrency__.",
   "all_projects": "Tous les projets",
   "all_templates": "Tous les modèles",
-  "already_have_sl_account": "Avez-vous déjà un compte __appName__ ?",
+  "already_have_sl_account": "Avez-vous déjà un compte __appName__?",
   "also": "Aussi",
   "alternatively_create_new_institution_account": "Autrement, vous pouvez créer un <b>nouveau compte</b> avec votre adresse courriel institutionnelle (<b>__email__</b>) en cliquant <b>__clickText__</b>.",
   "an_error_occurred_when_verifying_the_coupon_code": "Une erreur est survenue lors de la vérification du code coupon",
@@ -117,12 +116,12 @@
   "archived": "Archivé",
   "archived_projects": "Projets archivés",
   "archiving_projects_wont_affect_collaborators": "L’archivage d’un projet n’affectera pas vos collaborateur·rice·s.",
-  "are_you_affiliated_with_an_institution": "Êtes-vous affilié à une institution ?",
-  "are_you_getting_an_undefined_control_sequence_error": "Recevez-vous une erreur Undefined Control Sequence ? Si oui, assurez-vous d’avoir chargé le package graphicx—<0>\\usepackage{graphicx}</0>—dans le préambule (première section du code) de votre document. <1>En savoir plus</1>",
-  "are_you_still_at": "Êtes-vous toujours à <0>__institutionName__</0> ?",
+  "are_you_affiliated_with_an_institution": "Êtes-vous affilié·e à une institution ?",
+  "are_you_getting_an_undefined_control_sequence_error": "Recevez-vous une erreur Undefined Control Sequence? Si oui, assurez-vous d’avoir chargé le package graphicx—<0>\\usepackage{graphicx}</0>—dans le préambule (première section du code) de votre document. <1>En savoir plus</1>",
+  "are_you_still_at": "Êtes-vous toujours à <0>__institutionName__</0>?",
   "as_a_member_of_sso_required": "En tant que membre de <b>__institutionName__</b>, vous devez vous connecter à <b>__appName__</b> via votre portail institutionnel.",
   "ascending": "Ascendant",
-  "ask_proj_owner_to_upgrade_for_full_history": "Veuillez demander au propriétaire du projet de mettre à niveau son compte pour accéder à l’historique complet de ce projet.",
+  "ask_proj_owner_to_upgrade_for_full_history": "Veuillez demander au·à la propriétaire du projet de mettre à niveau son compte pour accéder à l’historique complet de ce projet.",
   "august": "Août",
   "author": "Auteur",
   "auto_close_brackets": "Auto-fermeture des accolades",
@@ -173,7 +172,7 @@
   "cannot_verify_user_not_robot": "Désolé, nous n’avons pas pu nous assurer que vous n’étiez pas un robot. Veuillez vérifier que Google reCAPTCHA n’est pas bloqué par un bloqueur de publicités ou un pare-feu.",
   "cant_find_email": "Cette adresse électronique n’est pas connue, désolé.",
   "cant_find_page": "Désolé, nous ne trouvons pas la page que vous cherchez.",
-  "cant_see_what_youre_looking_for_question": "Vous ne trouvez pas ce que vous cherchez ?",
+  "cant_see_what_youre_looking_for_question": "Vous ne trouvez pas ce que vous cherchez?",
   "card_details": "Détails de la carte",
   "card_details_are_not_valid": "Les informations de carte de paiement sont invalides",
   "card_must_be_authenticated_by_3dsecure": "Vous devez authentifier votre carte avec 3D Secure avant de poursuivre",
@@ -191,7 +190,7 @@
   "change_password_in_account_settings": "Changer le mot de passe dans les Paramètres du Compte",
   "change_plan": "Changer d’offre",
   "change_primary_email_address_instructions": "Pour changer votre email principal, veuillez dans un premier temps ajouter votre email principal (en cliquant sur <0>Ajouter un autre email</0>) et confirmer. Ensuite, cliquez sur le bouton <0>Définir comme principal</0>. <1>En savoir plus</1> à propos de la gestion de vos emails __appName__.",
-  "change_project_owner": "Changer le propriétaire du projet",
+  "change_project_owner": "Changer le·la propriétaire du projet",
   "change_to_group_plan": "Passer à une offre collective",
   "change_to_this_plan": "Changer pour cette offre",
   "changing_the_position_of_your_figure": "Changer la position de votre figure",
@@ -209,7 +208,7 @@
   "clear_sessions_description": "Ceci est une liste des autres sessions (ou connexions) actives sur votre compte, excluant votre session actuelle. Cliquez sur le bouton « Effacer les sessions » ci-dessous pour les déconnecter.",
   "clear_sessions_success": "Sessions effacées",
   "clearing": "Nettoyage en cours",
-  "click_here_to_view_sl_in_lng": "Cliquez ici pour utiliser __appName__ en  <0>__lngName__</0>",
+  "click_here_to_view_sl_in_lng": "Cliquez ici pour utiliser __appName__ en <0>__lngName__</0>",
   "click_link_to_proceed": "Cliquez sur <b>__clickText__</b> ci-dessous pour poursuivre.",
   "clone_with_git": "Cloner avec Git",
   "close": "Fermer",
@@ -217,7 +216,6 @@
   "clsi_unavailable": "Désolé, le serveur de compilation attribué à votre projet est temporairement indisponible. Veuillez réessayer dans quelques instants.",
   "cn": "Chinois (simplifié)",
   "code_check_failed_explanation": "Votre code contient des erreurs qui doivent être corrigées avant que l’auto-compilation puisse avoir lieu",
-  "code_editor": "Éditeur de code",
   "collaborate_online_and_offline": "Collaborez en ligne et hors ligne, avec votre propre organisation de travail",
   "collaboration": "Collaboration",
   "collaborator": "Collaborateur·rice",
@@ -255,7 +253,7 @@
   "confirmation_token_invalid": "Désolé, votre jeton de confirmation est invalide ou a expiré. Veuillez en demander un nouveau.",
   "confirming": "Confirmation",
   "conflicting_paths_found": "Chemins conflictuels détectés",
-  "connected_users": "Utilisateurs connectés",
+  "connected_users": "Utilisateur·rice·s connecté·e·s",
   "connecting": "Connexion en cours",
   "contact": "Contact",
   "contact_message_label": "Message",
@@ -292,7 +290,7 @@
   "current_file": "Fichier actuel",
   "current_password": "Mot de passe actuel",
   "current_session": "Session courante",
-  "currently_seeing_only_24_hrs_history": "Vous ne pouvez actuellement voir que les modifications des 24dernières heures dans ce projet.",
+  "currently_seeing_only_24_hrs_history": "Vous ne pouvez actuellement voir que les modifications des 24 dernières heures dans ce projet.",
   "currently_subscribed_to_plan": "Vous bénéficiez actuellement de l’offre <0>__planName__</0>.",
   "customize": "Personnaliser",
   "customize_your_group_subscription": "Personnaliser votre abonnement de groupe",
@@ -329,14 +327,14 @@
   "descending": "Descendant",
   "description": "Description",
   "dictionary": "Dictionnaire",
-  "did_you_know_institution_providing_professional": "Saviez-vous que __institutionName__ fournit des <0>fonctionnalités professionnelles __appName__ gratuites</0> à tous les membres de __institutionName__ ?",
+  "did_you_know_institution_providing_professional": "Saviez-vous que __institutionName__ fournit des <0>fonctionnalités professionnelles __appName__ gratuites</0> à tous les membres de __institutionName__?",
   "disable_stop_on_first_error": "Désactiver “Arrêter à la première erreur”",
   "disconnected": "Déconnecté",
   "discount_of": "Remise de __amount__",
   "do_not_have_acct_or_do_not_want_to_link": "Si vous n’avez pas de compte <b>__appName__</b> ou si vous ne souhaitez pas le lier à votre compte <b>__institutionName__</b>, veuillez cliquer <b>__clickText__</b>.",
   "do_not_link_accounts": "Ne pas lier les comptes",
-  "do_you_want_to_change_your_primary_email_address_to": "Voulez-vous définir <b>__email__</b> comme votre adresse email principale ?",
-  "do_you_want_to_overwrite_them": "Voulez-vous les écraser ?",
+  "do_you_want_to_change_your_primary_email_address_to": "Voulez-vous définir <b>__email__</b> comme votre adresse email principale?",
+  "do_you_want_to_overwrite_them": "Voulez-vous les écraser?",
   "documentation": "Documentation",
   "does_not_contain_or_significantly_match_your_email": "ne contient ou ne ressemble pas significativement à votre email",
   "doesnt_match": "Ne correspond pas",
@@ -344,7 +342,7 @@
   "doing_this_allow_log_in_through_institution_2": "Faire ceci vous permettra de vous connecter à <0>__appName__</0> via votre institution et reconfirmera votre adresse email institutionnelle.",
   "doing_this_will_verify_affiliation_and_allow_log_in_2": "Faire ceci vérifiera votre affiliation avec <0>__institutionName__</0> et vous permettra de vous connecter à <0>__appName__</0> via votre institution.",
   "done": "Terminé",
-  "dont_have_account": "Vous n’avez pas de compte ?",
+  "dont_have_account": "Vous n’avez pas de compte?",
   "download": "Télécharger",
   "download_pdf": "Télécharger le PDF",
   "download_zip_file": "Télécharger le fichier Zip",
@@ -354,11 +352,11 @@
   "dropbox_already_linked_error": "Votre compte Dropbox ne peut pas être lié à ce compte car il l’est déjà à un autre compte Overleaf.",
   "dropbox_already_linked_error_with_email": "Votre compte Dropbox ne peut pas être lié car il est déjà lié avec un autre compte Overleaf utilisant l’adresse email __otherUsersEmail__.",
   "dropbox_checking_sync_status": "Vérification de l’état de l’intégration Dropbox",
-  "dropbox_duplicate_names_error": "Votre compte Dropbox ne peut pas être lié, car vous avez plus d’un projet avec le même nom: ",
+  "dropbox_duplicate_names_error": "Votre compte Dropbox ne peut pas être lié, car vous avez plus d’un projet avec le même nom : ",
   "dropbox_duplicate_project_names": "Votre compte Dropbox a été dissocié, car vous avez plus d’un projet portant le nom <0>« __projectName__ »</0>.",
   "dropbox_duplicate_project_names_suggestion": "Veuillez vous assurer de l’unicité des noms de tous vos projets <0>actifs, archivés ou à la corbeille</0> puis réassociez votre compte Dropbox.",
   "dropbox_email_not_verified": "Nous ne parvenons pas à joindre votre compte Dropbox. Le service rapporte que votre adresse courriel n’est pas vérifiée. Veuillez vérifier votre adresse depuis votre compte Dropbox pour résoudre ce problème.",
-  "dropbox_for_link_share_projs": "Vous avez accédé à ce projet par un partage de lien : celui-ci ne sera pas synchronisé à votre Dropbox tant que vous n’aurez pas été invité par courriel par le propriétaire du projet.",
+  "dropbox_for_link_share_projs": "Vous avez accédé à ce projet par un partage de lien : celui-ci ne sera pas synchronisé à votre Dropbox tant que vous n’aurez pas été invité·e par courriel par le·la propriétaire du projet.",
   "dropbox_integration_lowercase": "Intégration avec Dropbox",
   "dropbox_successfully_linked_description": "Merci, nous avons associé votre compte Dropbox à __appName__.",
   "dropbox_sync": "Synchronisation Dropbox",
@@ -369,14 +367,14 @@
   "dropbox_sync_now_rate_limited": "La synchronisation manuelle est limitée à une fois par minute. Veuillez attendre quelques instants avant de réessayer.",
   "dropbox_sync_now_running": "Une synchronisation manuelle de ce projet a été démarrée en arrière-plan. Veuillez lui accorder quelques minutes pour procéder.",
   "dropbox_sync_out": "Mise à jour vers Dropbox",
-  "dropbox_sync_troubleshoot": "Des changements n’apparaissent pas dans Dropbox ? Veuillez attendre quelques minutes. Si les changements n’apparaissent toujours pas, vous pouvez <0>synchroniser ce projet maintenant</0>.",
+  "dropbox_sync_troubleshoot": "Des changements n’apparaissent pas dans Dropbox? Veuillez attendre quelques minutes. Si les changements n’apparaissent toujours pas, vous pouvez <0>synchroniser ce projet maintenant</0>.",
   "dropbox_synced": "Overleaf et Dropbox sont à jour",
   "dropbox_unlinked_because_access_denied": "La liaison avec votre compte Dropbox a été supprimée car le service Dropbox a rejeté vos identifiants. Veuillez restaurer cette liaison pour continuer à utiliser Dropbox avec Overleaf.",
   "dropbox_unlinked_because_full": "La liaison avec votre compte Dropbox a été supprimée car le quota de celui-ci a été atteint et nous ne sommes plus en mesure d’y envoyer les mises à jour. Veuillez libérer de l’espace puis restaurer cette liaison pour continuer à utiliser Dropbox avec Overleaf.",
   "dropbox_unlinked_premium_feature": "<0>Votre compte Dropbox a été déconnecté</0> car la synchronisation avec Dropbox est une fonctionnalité premium à laquelle vous aviez accès via votre licence institutionnelle.",
   "duplicate_file": "Dupliquer le fichier",
-  "duplicate_projects": "Cet utilisateur a des projets avec des noms identiques",
-  "each_user_will_have_access_to": "Chaque utilisateur aura accès à",
+  "duplicate_projects": "Cet·te utilisateur·rice a des projets avec des noms identiques",
+  "each_user_will_have_access_to": "Chaque utilisateur·rice aura accès à",
   "easily_manage_your_project_files_everywhere": "Gérez facilement vos fichiers de projets, depuis partout",
   "edit": "Modifier",
   "edit_dictionary": "Modifier le dictionnaire",
@@ -389,11 +387,10 @@
   "editor_disconected_click_to_reconnect": "L’éditeur a été déconnecté. Cliquez n’importe où pour vous reconnecter",
   "editor_theme": "Apparence de l’éditeur",
   "educational_discount_tooltip": "Disponible pour les groupes au sein d’établissements délivrant des diplômes.",
-  "educational_percent_discount_applied": "La remise éducation de __percent__% a été appliquée !",
+  "educational_percent_discount_applied": "La remise éducation de __percent__% a été appliquée!",
   "email": "Courriel",
   "email_address": "Adresse e-mail",
   "email_address_is_invalid": "Adresse email invalide",
-  "email_already_associated_with": "L’adresse courriel <b>__email1__</b> est déjà associée avec le compte <b>__appName__</b> <b>__email2__</b>.",
   "email_already_registered": "Cette adresse courriel est déjà utilisée",
   "email_already_registered_secondary": "Cette adresse courriel est déjà utilisée en tant qu’adresse secondaire",
   "email_already_registered_sso": "Cet email est déjà enregistré. Veuillez vous connecter à votre compte d’une autre manière et lier votre compte au nouveau fournisseur via vos paramètres de compte.",
@@ -489,7 +486,7 @@
   "git_authentication_token_create_modal_info_1": "Ceci est votre jeton d’authentification Git. Vous devrez l’entrer lorsqu’un mot de passe vous sera demandé.",
   "git_authentication_token_create_modal_info_2": "<0>Vous ne verrez ce jeton d’authentification qu’une seule fois, veuillez le copier et le garder en sécurité</0>. Pour des instructions complètes sur l’utilisation des jetons, consultez notre <1>page d’aide</1>.",
   "git_bridge_modal_click_generate": "Cliquez sur Générer un jeton pour générez votre jeton d’authentification maintenant. Vous pouvez aussi faire cela plus tard dans vos Paramètres de Compte.",
-  "git_bridge_modal_enter_authentication_token": "Lorsqu’un mot de passe vous sera demandé, entrez votre nouveau jeton d’authentification :",
+  "git_bridge_modal_enter_authentication_token": "Lorsqu’un mot de passe vous sera demandé, entrez votre nouveau jeton d’authentification :",
   "git_bridge_modal_see_once": "Vous n’allez voir ce jeton qu’une seule fois. Pour l’effacer ou pour en générer un nouveau, allez dans Paramètres du compte. Pour des instructions détaillées ou pour résoudre des problèmes, lisez notre <0>page d’aide</0>.",
   "git_bridge_modal_use_previous_token": "Si un mot de passe vous est demandé, vous pouvez utiliser un jeton Git précédent ou en générer un nouveau dans Paramètres du compte. Pour plus d’informations, lisez notre <0>page d’aide</0>.",
   "git_integration": "Intégration Git",
@@ -499,7 +496,7 @@
   "github_git_folder_error": "Ce projet contient un répertoire .git à sa racine, ce qui indique qu’il s’agit déjà d’un dépôt Git. Le service de synchronisation GitHub d’Overleaf n’est pas en mesure de synchroniser les historiques Git. Veuillez supprimer le répertoire .git et réessayer.",
   "github_integration_lowercase": "Intégration avec Git et GitHub",
   "github_is_premium": "La synchronisation GitHub est une fonctionnalité premium",
-  "github_large_files_error": "Échec de fusion : votre dépôt GitHub contient des fichiers dépassant la taille limite de 50 Mo ",
+  "github_large_files_error": "Échec de fusion : votre dépôt GitHub contient des fichiers dépassant la taille limite de 50 Mo ",
   "github_no_master_branch_error": "Ce dépôt ne peut pas être importé car il n’a pas de branche master. Veuillez vous assurer qu’une branche master existe dans le projet.",
   "github_private_description": "Vous choisissez qui peut voir et commiter dans ce dépôt.",
   "github_public_description": "Tout le monde peut voir ce dépôt. Vous choisissez qui peut commiter.",
@@ -526,7 +523,7 @@
   "group_full": "Ce groupe est déjà complet",
   "groups": "Groupes",
   "have_an_extra_backup": "Gardez une sauvegarde supplémentaire",
-  "have_more_days_to_try": "Voici <strong>__days__ days</strong> d’essai en plus !",
+  "have_more_days_to_try": "Voici <strong>__days__ days</strong> d’essai en plus!",
   "headers": "Titres",
   "help": "Aide",
   "help_articles_matching": "Fiches d’aide correspondant à votre sujet",
@@ -569,10 +566,10 @@
   "hundreds_templates_info": "Créez de magnifiques documents en vous basant sur notre galerie de modèles LaTeX pour les revues, conférences, thèses, rapports, CV et bien plus encore.",
   "i_want_to_stay": "Je veux rester",
   "if_have_existing_can_link": "Si vous avez déjà un compte <b>__appName__</b> sur une autre adresse courriel, vous pouvez le lier à votre compte <b>__institutionName__</b> en cliquant <b>__clickText__</b>.",
-  "if_owner_can_link": "Si vous possédez le compte <b>__appName__</b> ayant pour adresse <b>__email__</b>, vous serez autorisé à le lier à votre compte institutionnel <b>__institutionName__</b>.",
+  "if_owner_can_link": "Si vous possédez le compte <b>__appName__</b> ayant pour adresse <b>__email__</b>, vous serez autorisé·e à le lier à votre compte institutionnel <b>__institutionName__</b>.",
   "ignore_and_continue_institution_linking": "Vous pouvez également ignorer ceci et <a href=\"__link__\">continuer vers __appName__ avec votre compte <b>__email__</b></a>.",
   "ignore_validation_errors": "Ne pas vérifier la syntaxe",
-  "ill_take_it": "Je le prends !",
+  "ill_take_it": "Je le prends!",
   "import_from_github": "Importer depuis GitHub",
   "import_to_sharelatex": "Importer dans __appName__",
   "imported_from_another_project_at_date": "Importé d’un <0>autre projet</0>/__sourceEntityPathHTML__, le __formattedDate__ __relativeDate__",
@@ -597,17 +594,17 @@
   "invalid_email": "Une adresse courriel est invalide",
   "invalid_file_name": "Nom de fichier invalide",
   "invalid_filename": "Échec de l’envoi : assurez-vous que le nom du fichier ne contienne pas de caractères spéciaux, de blancs au début ou à la fin ou plus de __nameLimit__ caractères",
-  "invalid_institutional_email": "Le service d’authentification central de votre établissement a indiqué que votre adresse courriel était __email__, mais le domaine de cette adresse n’appartient pas à ceux que nous reconnaissons pour cet établissement. Il est peut-être possible de modifier votre adresse courriel principale depuis le profil utilisateur de votre établissement pour qu’elle soit dans un domaine reconnu. Veuillez contacter votre département informatique si vous avez des questions.",
+  "invalid_institutional_email": "Le service d’authentification central de votre établissement a indiqué que votre adresse courriel était __email__, mais le domaine de cette adresse n’appartient pas à ceux que nous reconnaissons pour cet établissement. Il est peut-être possible de modifier votre adresse courriel principale depuis le profil utilisateur·rice de votre établissement pour qu’elle soit dans un domaine reconnu. Veuillez contacter votre département informatique si vous avez des questions.",
   "invalid_password": "Mot de passe invalide.",
   "invalid_request": "Requête invalide. Veuillez corriger les données et réessayer.",
   "invalid_zip_file": "Archive invalide",
   "invite_group_members": "Inviter des membres du groupe",
   "invite_not_accepted": "Invitation en attente",
   "invite_not_valid": "Cette invitation à un projet n’est pas valable",
-  "invite_not_valid_description": "L’invitation a peut-être expiré. Veuillez contacter le propriétaire du projet",
-  "invited_to_group": "<0>__inviterName__</0> vous a invité à rejoindre une équipe sur __appName__",
+  "invite_not_valid_description": "L’invitation a peut-être expiré. Veuillez contacter le·la propriétaire du projet",
+  "invited_to_group": "<0>__inviterName__</0> vous a invité·e à rejoindre une équipe sur __appName__",
   "ip_address": "Adresse IP",
-  "is_email_affiliated": "Votre adresse courriel est-elle affiliée à un établissement ? ",
+  "is_email_affiliated": "Votre adresse courriel est-elle affiliée à un établissement? ",
   "it": "Italien",
   "ja": "Japonais",
   "january": "Janvier",
@@ -618,7 +615,7 @@
   "joining": "Jonction",
   "july": "Juillet",
   "june": "Juin",
-  "kb_suggestions_enquiry": "Avez-vous consulté notre <0>__kbLink__</0> ?",
+  "kb_suggestions_enquiry": "Avez-vous consulté notre <0>__kbLink__</0>?",
   "keep_current_plan": "Garder mon offre actuelle",
   "keybindings": "Raccourcis clavier",
   "knowledge_base": "Base de connaissances",
@@ -630,7 +627,7 @@
   "latex_templates": "Modèles LaTeX",
   "layout": "Disposition",
   "ldap": "LDAP",
-  "ldap_create_admin_instructions": "Choisissez une adresse courriel pour le compte __appName__ initial. Celle-ci doit correspondre à un compte dans la base LDAP. Vous serez ensuite invité à vous connecter avec ce compte.",
+  "ldap_create_admin_instructions": "Choisissez une adresse courriel pour le compte __appName__ initial. Celle-ci doit correspondre à un compte dans la base LDAP. Vous serez ensuite invité·e à vous connecter avec ce compte.",
   "learn_more": "En savoir plus",
   "learn_more_about_link_sharing": "En savoir plus sur le partage par lien",
   "leave": "Quitter",
@@ -662,13 +659,11 @@
   "log_in": "Se connecter",
   "log_in_and_link": "Se connecter et lier",
   "log_in_and_link_accounts": "Se connecter et lier les comptes",
-  "log_in_first_to_proceed": "Vous aurez besoin de <b>vous connecter</b> avant de poursuivre.",
   "log_in_with": "Se connecter avec __provider__",
-  "log_in_with_email": "Se connecter avec __email__",
-  "log_in_with_existing_institution_email": "Veuillez vous connecter sur votre compte <0>__appName__</0> existant afin de lier vos comptes institutionnels <0>__appName__</0>  et <0>__institutionName__</0>.",
+  "log_in_with_existing_institution_email": "Veuillez vous connecter sur votre compte <0>__appName__</0> existant afin de lier vos comptes institutionnels <0>__appName__</0> et <0>__institutionName__</0>.",
   "log_out": "Déconnexion",
   "log_out_from": "Se déconnecter de __email__",
-  "logged_in_with_email": "Vous êtes actuellement connecté à <b>__appName__</b> avec l’adresse <b>__email__</b>.",
+  "logged_in_with_email": "Vous êtes actuellement connecté·e à <b>__appName__</b> avec l’adresse <b>__email__</b>.",
   "logging_in": "Connexion en cours",
   "login": "Identifiant",
   "login_error": "Erreur de connexion",
@@ -679,9 +674,9 @@
   "login_to_overleaf": "Se connecter à Overleaf",
   "login_with_service": "Se connecter avec __service__",
   "logs_and_output_files": "Journaux et fichiers de sortie",
-  "looking_multiple_licenses": "Vous cherchez des licences groupées ?",
-  "looks_like_logged_in_with_email": "Il semble que vous soyez déjà connecté à <b>__appName__</b> avec l’adresse <b>__email__</b>.",
-  "looks_like_youre_at": "Il semblerait que vous soyez à <0>__institutionName__</0> !",
+  "looking_multiple_licenses": "Vous cherchez des licences groupées?",
+  "looks_like_logged_in_with_email": "Il semble que vous soyez déjà connecté·e à <b>__appName__</b> avec l’adresse <b>__email__</b>.",
+  "looks_like_youre_at": "Il semblerait que vous soyez à <0>__institutionName__</0>!",
   "lost_connection": "Connexion perdue",
   "main_document": "Document principal",
   "main_file_not_found": "Document principal inconnu",
@@ -724,10 +719,10 @@
   "name": "Nom",
   "native": "Native",
   "navigation": "Navigation",
-  "nearly_activated": "Il ne vous reste plus qu’une étape pour activer votre compte __appName__ !",
+  "nearly_activated": "Il ne vous reste plus qu’une étape pour activer votre compte __appName__!",
   "need_anything_contact_us_at": "Si vous avez besoin de quelque chose, n’hésitez pas à nous contacter directement à",
   "need_to_add_new_primary_before_remove": "Vous devrez ajouter une nouvelle adresse courriel principale avant de pouvoir supprimer celle-ci.",
-  "need_to_leave": "Besoin de partir ?",
+  "need_to_leave": "Besoin de partir?",
   "new_file": "Nouveau fichier",
   "new_folder": "Nouveau dossier",
   "new_name": "Nouveau nom",
@@ -736,7 +731,7 @@
   "new_snippet_project": "Sans titre",
   "new_tag": "Nouvelle étiquette",
   "new_tag_name": "Nouveau nom d’étiquette",
-  "next_payment_of_x_collectected_on_y": "Le prochain paiement de  <0>__paymentAmmount__</0> sera débité le <1>__collectionDate__</1>.",
+  "next_payment_of_x_collectected_on_y": "Le prochain paiement de <0>__paymentAmmount__</0> sera débité le <1>__collectionDate__</1>.",
   "nl": "Hollandais",
   "no": "Norvégien",
   "no_articles_matching_your_tags": "Aucun article ne correspond à vos étiquettes",
@@ -764,8 +759,8 @@
   "not_found_error_from_the_supplied_url": "Le lien pour ouvrir ce contenu sur Overleaf pointe vers un fichier introuvable. Si cela se produit régulièrement pour un site donné, veuillez leur faire part du problème.",
   "not_now": "pas maintenant",
   "not_registered": "Pas inscrit·e",
-  "notification_features_upgraded_by_affiliation": "Bonne nouvelle ! L’organisme dont vous faites partie, __institutionName__, est en partenariat avec Overleaf, ce qui vous permet d’accéder aux fonctionnalités professionnelles d’Overleaf.",
-  "notification_personal_subscription_not_required_due_to_affiliation": " Bonne nouvelle ! L’organisme dont vous faites partie, __institutionName__, est en partenariat avec Overleaf, ce qui vous permet d’accéder aux fonctionnalités professionnelles d’Overleaf. Vous pouvez ainsi annuler votre abonnement personnel en conservant l’accès à tous vos avantages.",
+  "notification_features_upgraded_by_affiliation": "Bonne nouvelle! L’organisme dont vous faites partie, __institutionName__, est en partenariat avec Overleaf, ce qui vous permet d’accéder aux fonctionnalités professionnelles d’Overleaf.",
+  "notification_personal_subscription_not_required_due_to_affiliation": " Bonne nouvelle! L’organisme dont vous faites partie, __institutionName__, est en partenariat avec Overleaf, ce qui vous permet d’accéder aux fonctionnalités professionnelles d’Overleaf. Vous pouvez ainsi annuler votre abonnement personnel en conservant l’accès à tous vos avantages.",
   "notification_project_invite_accepted_message": "Vous avez rejoint <b>__projectName__</b>",
   "notification_project_invite_message": "<b>__userName__</b> souhaiterait que vous rejoigniez <b>__projectName__</b>",
   "november": "Novembre",
@@ -802,7 +797,7 @@
   "password_reset_email_sent": "Un courriel vous a été envoyé afin de finaliser la réinitialisation de votre mot de passe.",
   "password_reset_token_expired": "Votre demande de réinitialisation de mot de passe a expiré. Veuillez refaire une demande de réinitialisation et suivre le lien qui figure dans le nouveau courriel.",
   "password_too_long_please_reset": "La longueur maximale autorisée pour le mot de passe a été dépassée. Merci de réinitialiser votre mot de passe.",
-  "payment_provider_unreachable_error": "Désolé, une erreur s’est produite lors de la communication avec notre fournisseur de paiements. Veuillez réessayer dans quelques instants.\n\nSi vous utilisez une extension dans votre navigateur pour bloquer les publicités ou les scripts, il peut être nécessaire de les désactiver temporairement.",
+  "payment_provider_unreachable_error": "Désolé, une erreur s’est produite lors de la communication avec notre fournisseur de paiements. Veuillez réessayer dans quelques instants. Si vous utilisez une extension dans votre navigateur pour bloquer les publicités ou les scripts, il peut être nécessaire de les désactiver temporairement.",
   "pdf_compile_in_progress_error": "Une compilation précédente est toujours en cours. Veuillez attendre un instant puis réessayer de compiler.",
   "pdf_compile_rate_limit_hit": "Limite de fréquence de compilation atteinte",
   "pdf_compile_try_again": "Veuillez attendre que votre compilation précédente se termine avant de réessayer.",
@@ -814,7 +809,7 @@
   "pl": "Polonais",
   "planned_maintenance": "Maintenance prévue",
   "plans_and_pricing": "Offres et prix",
-  "please_ask_the_project_owner_to_upgrade_to_track_changes": "Veuillez demander au propriétaire du projet de mettre à niveau son compte pour pouvoir suivre les modifications",
+  "please_ask_the_project_owner_to_upgrade_to_track_changes": "Veuillez demander au·à la propriétaire du projet de mettre à niveau son compte pour pouvoir suivre les modifications",
   "please_change_primary_to_remove": "Veuillez changer votre adresse courriel principale pour pouvoir la retirer",
   "please_compile_pdf_before_download": "Veuillez compiler votre projet avant de pouvoir télécharger le PDF",
   "please_confirm_email": "Veuillez confirmer votre adresse courriel __emailAddress__ en cliquant sur le lien contenu dans le courriel de confirmation ",
@@ -831,7 +826,7 @@
   "please_set_main_file": "Veuillez choisir le fichier principal pour ce projet depuis le menu du projet. ",
   "plus_additional_collaborators_and_more": "(en plus de collaborateur·rice·s supplémentaires, un quota d’IA plus élevé, le suivi des modifications et bien plus encore).",
   "popular_tags": "Étiquettes populaires",
-  "portal_add_affiliation_to_join": "Il semblerait que vous soyez déjà connecté à __appName__ ! Si vous avez une adresse courriel __portalTitle__, vous pouvez l’ajouter maintenant.",
+  "portal_add_affiliation_to_join": "Il semblerait que vous soyez déjà connecté·e à __appName__ ! Si vous avez une adresse courriel __portalTitle__, vous pouvez l’ajouter maintenant.",
   "position": "Grade",
   "postal_code": "Code postal",
   "presentation": "Présentation",
@@ -848,16 +843,16 @@
   "professional": "Professionnel·le",
   "project_approaching_file_limit": "Ce projet approche la limite de fichiers",
   "project_flagged_too_many_compiles": "Ce projet a été compilé trop fréquemment. Cette limite sera levée sous peu.",
-  "project_has_too_many_files": "La limite des 2 000 fichiers a été atteinte pour ce projet",
+  "project_has_too_many_files": "La limite des 2 000 fichiers a été atteinte pour ce projet",
   "project_has_too_many_files_limit": "La limite des __limit__ fichiers a été atteinte pour ce projet",
   "project_last_published_at": "Votre projet a été édité pour la dernière fois le",
   "project_name": "Nom du projet",
-  "project_not_linked_to_github": "Ce projet n’est pas lié à un dépôt GitHub. Vous pouvez lui créer un dépôt dans GitHub:",
-  "project_ownership_transfer_confirmation_1": "Êtes-vous sûr de vouloir faire de <0>__user__</0> le propriétaire de <1>__project__</1> ?",
-  "project_ownership_transfer_confirmation_2": "Cette action est irréversible. Le nouveau propriétaire sera notifié et sera en mesure de modifier les paramètres d’accès au projet (y compris de vous ôter le droit d’accès).",
+  "project_not_linked_to_github": "Ce projet n’est pas lié à un dépôt GitHub. Vous pouvez lui créer un dépôt dans GitHub :",
+  "project_ownership_transfer_confirmation_1": "Êtes-vous sûr·e de vouloir faire de <0>__user__</0> le·la propriétaire de <1>__project__</1> ?",
+  "project_ownership_transfer_confirmation_2": "Cette action est irréversible. Le·La nouvelle propriétaire sera notifié·e et sera en mesure de modifier les paramètres d’accès au projet (y compris de vous ôter le droit d’accès).",
   "project_synchronisation": "Synchronisation du projet",
   "project_too_large": "Projet trop volumineux",
-  "project_too_large_please_reduce": "Ce projet contient trop de texte, veuillez essayer de le réduire. Les fichiers les plus volumineux sont:",
+  "project_too_large_please_reduce": "Ce projet contient trop de texte, veuillez essayer de le réduire. Les fichiers les plus volumineux sont :",
   "project_too_much_editable_text": "Ce projet contient trop de texte, veuillez essayer de le réduire.",
   "project_url": "URL du projet concerné",
   "projects": "Projets",
@@ -894,7 +889,7 @@
   "regards": "Merci",
   "register": "S’inscrire",
   "register_error": "Erreur d’inscription",
-  "register_intercept_sso": "Vous pourrez lier votre compte __authProviderName__ depuis la page « Paramètres du compte » une fois que vous vous serez connecté.",
+  "register_intercept_sso": "Vous pourrez lier votre compte __authProviderName__ depuis la page « Paramètres du compte » une fois que vous vous serez connecté.",
   "register_to_edit_template": "Veuillez vous inscrire pour éditer le modèle __templateName__",
   "register_with_another_email": "<a href=\"__link__\">Inscrivez-vous avec __appName__</a> en utilisant une autre adresse courriel.",
   "registered": "Inscrit·e",
@@ -918,7 +913,7 @@
   "republish": "Publier à nouveau",
   "request_password_reset_to_reconfirm": "Faites une demande de modification du mot de passe pour revalider",
   "request_reconfirmation_email": "Demander un courriel de confirmation",
-  "request_sent_thank_you": "Message envoyé ! Notre équipe va l’examiner et vous répondre par courriel.",
+  "request_sent_thank_you": "Message envoyé! Notre équipe va l’examiner et vous répondre par courriel.",
   "requesting_password_reset": "Réinitialisation du mot de passe",
   "required": "requis",
   "resend": "Envoyer de nouveau",
@@ -939,7 +934,7 @@
   "role": "Grade",
   "ru": "Russe",
   "saml": "SAML",
-  "saml_create_admin_instructions": "Choisissez une adresse courriel pour le compte __appName__ initial. Celle-ci doit correspondre à un compte dans le système SAML. Vous serez ensuite invité à vous connecter avec ce compte.",
+  "saml_create_admin_instructions": "Choisissez une adresse courriel pour le compte __appName__ initial. Celle-ci doit correspondre à un compte dans le système SAML. Vous serez ensuite invité·e à vous connecter avec ce compte.",
   "save": "Enregistrer",
   "save_or_cancel-cancel": "annuler",
   "save_or_cancel-or": "ou",
@@ -994,7 +989,7 @@
   "skip_to_content": "Aller au contenu",
   "something_went_wrong_canceling_your_subscription": "Un problème est survenu lors de l’annulation de votre abonnement. Veuillez contacter le support.",
   "something_went_wrong_rendering_pdf": "Une erreur s’est produite lors du rendu de ce PDF.",
-  "something_went_wrong_server": "Une erreur s’est produite pendant la communication avec le serveur :( Veuillez réessayer.",
+  "something_went_wrong_server": "Une erreur s’est produite pendant la communication avec le serveur :( Veuillez réessayer.",
   "somthing_went_wrong_compiling": "Désolé, quelque chose ne fonctionne pas et votre projet ne peut pas être compilé. Veuillez réessayer dans quelques instants.",
   "sorry_something_went_wrong_opening_the_document_please_try_again": "Désolé, une erreur s’est produite lors de l’ouverture de ce contenu sur Overleaf. Veuillez réessayer",
   "source": "Code source",
@@ -1005,7 +1000,7 @@
   "start_free_trial": "Commencer l’essai gratuit",
   "state": "État",
   "status_checks": "Vérifications d’état",
-  "still_have_questions": "Vous avez d’autres questions ?",
+  "still_have_questions": "Vous avez d’autres questions?",
   "stop_compile": "Arrêter la compilation",
   "stop_on_first_error": "Arrêter à la première erreur",
   "stop_on_validation_error": "Vérifier la syntaxe avant la compilation",
@@ -1023,10 +1018,10 @@
   "subscription_canceled": "Abonnement annulé",
   "subscription_canceled_and_terminate_on_x": " Votre abonnement a été annulé et se terminera le <0>__terminateDate__</0>. Aucun paiement supplémentaire ne vous sera demandé.",
   "suggestion": "Suggestion",
-  "sure_you_want_to_cancel_plan_change": "Êtes-vous sûr(e) de vouloir annuler votre modification prévue d’offre ? Vous resterez abonné à l’offre <0>__planName__</0>.",
+  "sure_you_want_to_cancel_plan_change": "Êtes-vous sûr·e de vouloir annuler votre modification prévue d’offre ? Vous resterez abonné·e à l’offre <0>__planName__</0>.",
   "sure_you_want_to_change_plan": "Voulez-vous vraiment changer d’offre pour <0>__planName__</0> ?",
-  "sure_you_want_to_delete": "Êtes-vous sûr(e) de vouloir supprimer définitivement  les fichiers suivants ?",
-  "sure_you_want_to_leave_group": "Voulez-vous vraiment quitter ce groupe ?",
+  "sure_you_want_to_delete": "Êtes-vous sûr·e de vouloir supprimer définitivement les fichiers suivants ?",
+  "sure_you_want_to_leave_group": "Voulez-vous vraiment quitter ce groupe?",
   "sv": "Suedois",
   "sync": "Synchroniser",
   "sync_dropbox_github": "Synchroniser avec Dropbox et GitHub",
@@ -1039,7 +1034,7 @@
   "tag_name_cannot_exceed_characters": "Le nom de l’étiquette ne peut pas avoir plus de __maxLength__ caractères",
   "tag_name_is_already_used": "L’étiquette \"__tagName__\" existe déjà",
   "tags": "Étiquettes",
-  "take_me_home": "Retour à la maison !",
+  "take_me_home": "Retour à la maison!",
   "template_description": "Description des modèles",
   "template_gallery": "Galerie de modèles",
   "template_not_found_description": "Cette méthode de création de projets à partir de modèles n’est plus disponible. Merci de vous rendre sur notre galerie des modèles pour trouver d’autres modèles.",
@@ -1050,11 +1045,11 @@
   "terms": "Conditions",
   "tex_live_version": "Version de TeX Live",
   "thank_you": "Merci",
-  "thank_you_exclamation": "Merci !",
+  "thank_you_exclamation": "Merci!",
   "thank_you_for_being_part_of_our_beta_program": "Merci de votre participation au programme de bêta, qui vous permet d’accéder en avant-première aux nouvelles fonctionnalités et de nous aider à mieux comprendre vos besoins",
   "thanks": "Merci",
-  "thanks_for_subscribing": "Merci de vous être abonné(e) !",
-  "thanks_for_subscribing_you_help_sl": "Merci de vous être abonné à l’offre __planName__. C’est grâce au support de personnes comme vous que __appName__ peut prospérer et continuer à s’améliorer.",
+  "thanks_for_subscribing": "Merci de vous être abonné·e !",
+  "thanks_for_subscribing_you_help_sl": "Merci de vous être abonné·e à l’offre __planName__. C’est grâce au support de personnes comme vous que __appName__ peut prospérer et continuer à s’améliorer.",
   "thanks_settings_updated": "Merci, vos réglages ont été mis à jour.",
   "the_file_supplied_is_of_an_unsupported_type ": "Le lien pour ouvrir ce contenu sur Overleaf pointe vers un type de fichier invalide. Les types autorisés sont les documents .tex et les archives .zip. Si cela se produit régulièrement pour un site donné, veuillez leur faire part du problème.",
   "the_requested_conversion_job_was_not_found": "Le lien pour ouvrir ce contenu sur Overleaf spécifie une tâche de conversion inconnue. Il est possible que cette tâche ait expiré et qu’elle doive être lancée à nouveau. Si cela se produit régulièrement pour un site donné, veuillez leur faire part du problème.",
@@ -1062,7 +1057,7 @@
   "the_required_parameters_were_not_supplied": "Certains paramètres obligatoires sont manquants dans le lien pour ouvrir ce contenu sur Overleaf. Si cela se produit régulièrement pour un site donné, veuillez leur faire part du problème.",
   "the_supplied_parameters_were_invalid": "Certains paramètres invalides sont présents dans le lien pour ouvrir ce contenu sur Overleaf. Si cela se produit régulièrement pour un site donné, veuillez leur faire part du problème.",
   "the_supplied_uri_is_invalid": "Le lien pour ouvrir ce contenu sur Overleaf contient une URI invalide. Si cela se produit régulièrement pour un site donné, veuillez leur faire part du problème.",
-  "their_projects_will_be_transferred_to_another_user": "Leurs projets seront tous transférés à un autre utilisateur de votre choix",
+  "their_projects_will_be_transferred_to_another_user": "Leurs projets seront tous transférés à un·e autre utilisateur·rice de votre choix",
   "theme": "Thème",
   "then_x_price_per_month": "Puis __price__ par mois",
   "then_x_price_per_year": "Puis __price__ par an",
@@ -1078,15 +1073,15 @@
   "three_free_collab": "Trois collaborateurs offerts",
   "timedout": "Temps expiré",
   "title": "Titre",
-  "to_add_more_collaborators": "Pour ajouter des collaborateur·rice·s supplémentaires ou pour activer le partage par lien, veuillez vous adresser au propriétaire du projet",
-  "to_change_access_permissions": "Pour modifier les droits d’accès, contactez le propriétaire du projet",
+  "to_add_more_collaborators": "Pour ajouter des collaborateur·rice·s supplémentaires ou pour activer le partage par lien, veuillez vous adresser au·à la propriétaire du projet",
+  "to_change_access_permissions": "Pour modifier les droits d’accès, contactez le·la propriétaire du projet",
   "to_modify_your_subscription_go_to": "Pour modifier votre abonnement, allez sur",
   "toggle_compile_options_menu": "Activer le menu des options de compilation",
   "token": "jeton",
-  "token_access_failure": "Accès refusé ; contactez le propriétaire du projet pour plus d’assistance",
-  "token_limit_reached": "Vous avez atteint la limite de 10 jetons. Pour générer un nouveau jeton, veuillez effacer un jeton existant.",
+  "token_access_failure": "Accès refusé ; contactez le·la propriétaire du projet pour plus d’assistance",
+  "token_limit_reached": "Vous avez atteint la limite de 10 jetons. Pour générer un nouveau jeton, veuillez effacer un jeton existant.",
   "too_many_attempts": "Trop de tentatives. Veuillez patienter un moment puis réessayer.",
-  "too_many_files_uploaded_throttled_short_period": "Trop de fichiers téléversés, vos envois ont été mis en attente pour un court instant. Merci d’attendre 15 minutes avant de réessayer.",
+  "too_many_files_uploaded_throttled_short_period": "Trop de fichiers téléversés, vos envois ont été mis en attente pour un court instant. Merci d’attendre 15 minutes avant de réessayer.",
   "too_many_login_requests_2_mins": "Ce compte a reçu trop de demandes de connexion. Veuillez attendre deux minutes avant de tenter une nouvelle connexion",
   "too_many_requests": "Trop de requêtes ont été reçues sur une courte période. Veuillez patienter quelques instants puis réessayer.",
   "too_recently_compiled": "Ce projet a été compilé très récemment, cette compilation a donc été passée.",
@@ -1120,14 +1115,14 @@
   "unlimited_projects": "Projets illimités",
   "unlink": "Ne plus lier",
   "unlink_github_repository": "Annuler le lien avec le dépôt GitHub",
-  "unlink_github_warning": "Tous les projets que vous avez synchronisés avec GitHub seront déconnectés et ne seront plus maintenu synchronisés avec GitHub. Voulez-vous vraiment ne plus lier votre compte GitHub ?",
+  "unlink_github_warning": "Tous les projets que vous avez synchronisés avec GitHub seront déconnectés et ne seront plus maintenu synchronisés avec GitHub. Voulez-vous vraiment ne plus lier votre compte GitHub?",
   "unlink_reference": "Ne plus lier le fournisseur de références",
   "unlink_warning_reference": "Attention : si vous supprimez le lien entre votre compte et ce fournisseur, vous ne pourrez plus importer des références dans vos projets.",
   "unlinking": "Annuler le lien",
   "unpublish": "Dépublier",
   "unpublishing": "Dépublication en cours",
   "unsubscribe": "Se désabonner",
-  "unsubscribed": "Désabonné(e)",
+  "unsubscribed": "Désabonné·e",
   "unsubscribing": "Désabonnement en cours",
   "untrash": "Restaurer",
   "update": "Mettre à jour",
@@ -1136,9 +1131,9 @@
   "update_your_billing_details": "Mettre à jour vos données de facturation",
   "updating_site": "Mise à jour du site",
   "upgrade": "Mettre à niveau",
-  "upgrade_cc_btn": "Mettez à niveau maintenant, payez dans 7 jours",
+  "upgrade_cc_btn": "Mettez à niveau maintenant, payez dans 7 jours",
   "upgrade_now": "Mettre à niveau maintenant",
-  "upgrade_to_get_feature": "Mettre à niveau pour profiter de  __feature__, plus :",
+  "upgrade_to_get_feature": "Mettre à niveau pour profiter de __feature__, plus :",
   "upload": "Importer",
   "upload_failed": "Échec du téléversement",
   "upload_file": "Importer un fichier",
@@ -1157,33 +1152,32 @@
   "view_logs": "Voir les journaux",
   "view_pdf": "Voir le PDF",
   "view_your_invoices": "Voir vos factures",
-  "visual_editor": "Éditeur visuel",
   "want_change_to_apply_before_plan_end": "Si vous souhaitez que cette modification prenne effet avant la fin de l’échéance actuelle de facturation, veuillez nous contacter.",
   "we_cant_find_any_sections_or_subsections_in_this_file": "Nous n’avons trouvé aucune section ou sous-section dans ce fichier",
   "we_logged_you_in": "Nous vous avons connecté.",
-  "we_may_also_contact_you_from_time_to_time_by_email_with_a_survey": "Nous serons également amenés à vous inviter par courriel à participer à des sondages ou à d’autres initiatives de recherche utilisateur",
+  "we_may_also_contact_you_from_time_to_time_by_email_with_a_survey": "Nous serons également amenés à vous inviter par courriel à participer à des sondages ou à d’autres initiatives de recherche utilisateur·rice",
   "wed_love_you_to_stay": "Nous aimerions beaucoup que vous restiez",
   "welcome_to_sl": "Bienvenue dans __appName__",
-  "why_latex": "Pourquoi LaTeX?",
+  "why_latex": "Pourquoi LaTeX?",
   "wide": "Large",
   "will_need_to_log_out_from_and_in_with": "Vous devrez vous <b>déconnecter</b> de votre compte <b>__email1__</b> et vous reconnecter sur votre compte <b>__email2__</b>.",
   "word_count": "Nombre de mots",
   "work_offline": "Travaillez hors ligne",
-  "work_with_non_overleaf_users": "Travaillez avec des utilisateurs hors de Overleaf",
+  "work_with_non_overleaf_users": "Travaillez avec des utilisateur·rice·s hors d’Overleaf",
   "x_price_for_first_month": "<0>__price__</0> pour votre premier mois",
   "x_price_for_first_year": "<0>__price__</0> pour votre première année",
   "x_price_per_month": "<0>__price__</0> par mois",
   "x_price_per_year": "__price__ par an",
   "year": "année",
   "you": "Vous",
-  "you_can_now_log_in_sso": "Vous pouvez maintenant vous connecter via votre établissement pour potentiellement bénéficier de <0>fonctionnalités professionnelles __appName__ gratuites</0> !",
+  "you_can_now_log_in_sso": "Vous pouvez maintenant vous connecter via votre établissement pour potentiellement bénéficier de <0>fonctionnalités professionnelles __appName__ gratuites</0>!",
   "you_can_opt_in_and_out_of_the_program_at_any_time_on_this_page": "Vous pouvez rejoindre ou quitter le programme à tout moment depuis cette page",
-  "you_have_added_x_of_group_size_y": "Vous avez ajouté <0>__addedUsersSize__</0> membres sur les <1>__groupSize__</1> disponibles",
+  "you_have_added_x_of_group_size_y": "Vous avez ajouté·e <0>__addedUsersSize__</0> membres sur les <1>__groupSize__</1> disponibles",
   "you_will_be_able_to_contact_us_any_time_to_share_your_feedback": "Vous pourrez nous contacter à tout moment pour donner votre avis",
   "you_will_be_able_to_reassign_subscription": "Vous pourrez réattribuer leur abonnement à une autre personne de votre organisation",
   "your_affiliation_is_confirmed": "Votre affiliation à <0>__institutionName__</0> est validée.",
   "your_git_access_info": "Vos jetons d’authentification Git devront être utilisés lorsqu’un mot de passe Git vous sera demandé.",
-  "your_git_access_info_bullet_1": "Vous pouvez avoir jusqu’à 10 jetons.",
+  "your_git_access_info_bullet_1": "Vous pouvez avoir jusqu’à 10 jetons.",
   "your_git_access_info_bullet_2": "Si vous atteignez la limite maximale, vous devrez effacer un jeton pour pouvoir en générer un nouveau.",
   "your_git_access_tokens": "Vos jetons authentification Git",
   "your_message_to_collaborators": "Envoyez un message à vos collaborateur·rice·s",

+ 1 - 1
services/web/locales/ko.json

@@ -535,7 +535,7 @@
   "upload_file": "파일 업로드",
   "upload_project": "프로젝트 업로드",
   "upload_zipped_project": "압축된 프로젝트 업로드",
-  "user_wants_you_to_see_project": "__username__ 님이  __projectname__에 참여하길 원합니다",
+  "user_wants_you_to_see_project": "__username__ 님이 __projectname__에 참여하길 원합니다",
   "vat_number": "VAT 번호",
   "view_all": "모두 보기",
   "view_in_template_gallery": "템플릿 갤러리에서 보기",

+ 1 - 1
services/web/locales/nl.json

@@ -432,7 +432,7 @@
   "save_or_cancel-or": "of",
   "save_or_cancel-save": "Sla op",
   "saving": "Aan het opslaan",
-  "saving_notification_with_seconds": "__docname__  aan het opslaan... (__seconds__ seconden aan niet-opgeslagen wijzigingen)",
+  "saving_notification_with_seconds": "__docname__ aan het opslaan... (__seconds__ seconden aan niet-opgeslagen wijzigingen)",
   "search_bib_files": "Zoek op auteur, titel, jaar",
   "search_projects": "Projecten zoeken",
   "search_references": "Doorzoek het .bib bestand in dit project",

+ 1 - 3
services/web/locales/sv.json

@@ -368,7 +368,7 @@
   "institution": "Instution",
   "institution_account": "Institutionellt konto",
   "institution_account_tried_to_add_already_linked": "Denna institution är <b>redan länkad</b> till ditt konto via en annan e-post-adress.",
-  "institution_account_tried_to_add_already_registered": "Det e-post-konto/institutionella konto som du försökte lägga till är <b>redan registrerat</b> i  __appName__.",
+  "institution_account_tried_to_add_already_registered": "Det e-post-konto/institutionella konto som du försökte lägga till är <b>redan registrerat</b> i __appName__.",
   "institution_account_tried_to_confirm_saml": "Detta e-postmeddelande kan inte bekräftas. Vänligen ta bort e-postmeddelandet från ditt konto och försök att lägga till det igen.",
   "institution_and_role": "Institution och befattning",
   "institutional": "Institutionell",
@@ -441,9 +441,7 @@
   "log_in": "Logga in",
   "log_in_and_link": "Logga in och länka",
   "log_in_and_link_accounts": "Logga in och länka konton",
-  "log_in_first_to_proceed": "Du måste först <b>logga in</b> för att fortsätta.",
   "log_in_with": "Logga in med __provider__",
-  "log_in_with_email": "Logga in med __email__",
   "log_in_with_existing_institution_email": "Vänligen logga in med ditt befintliga <0>__appName__</0>-konto för att länka kontot <0>__appName__</0> och det institutionella kontot <0>__institutionName__ </0>.",
   "log_out": "Logga ut",
   "log_out_from": "Logga ut från __email__",

+ 11 - 19
services/web/locales/zh-CN.json

@@ -123,7 +123,6 @@
   "all_borders": "全边框",
   "all_features_in_group_standard_plus": "Group Standard 中的所有功能,以及:",
   "all_premium_features": "所有高级付费功能",
-  "all_premium_features_including": "所有高级功能,包括:",
   "all_prices_displayed_are_in_currency": "所有展示的价格都以__recommendedCurrency__计。",
   "all_projects": "所有项目",
   "all_projects_will_be_transferred_immediately": "所有的项目将立即移交给新的拥有者。",
@@ -225,7 +224,7 @@
   "by_joining_labs": "加入实验室即表示您同意接收 Overleaf 不定期发送的电子邮件和更新信息(例如,征求您的反馈)。您还同意我们的<0>服务条款</0>和<1>隐私声明</1>。",
   "by_registering_you_agree_to_our_terms_of_service": "注册即表示您同意我们的 <0>服务条款</0> 和 <1>隐私条款</1>。",
   "by_subscribing_you_agree_to_our_terms_of_service": "订阅即表示您同意我们的<0>服务条款</0>。",
-  "can_link_institution_email_acct_to_institution_acct": "您现在可以将您的 <b>__appName__</b> 账户 <b>__email__</b> 与您的  <b>__institutionName__</b> 机构账户关联。",
+  "can_link_institution_email_acct_to_institution_acct": "您现在可以将您的 <b>__appName__</b> 账户 <b>__email__</b> 与您的 <b>__institutionName__</b> 机构账户关联。",
   "can_link_institution_email_by_clicking": "您可以通过单击 <b>__clickText__</b> 将您的 <b>__email__</b> <b>__appName__</b> 账户链接到您的 <b>__institutionName__</b> 帐户。",
   "can_link_institution_email_to_login": "您可以将您的 <b>__email__</b> <b>__appName__</b> 账户链接到你的 <b>__institutionName__</b> 账户,这将允许您通过机构门户登录到<b>__appName__</b> 。",
   "can_link_your_institution_acct_2": "您可以现在 <0>链接</0> 您的 <0>__appName__</0> 账户到您的<0>__institutionName__</0> 机构账户。",
@@ -294,7 +293,7 @@
   "clear_sessions_description": "这是您的账户中当前活跃的会话信息(不包含当前会话)。点击“清理会话”按钮可以退出这些会话。",
   "clear_sessions_success": "其他会话已清理",
   "clearing": "正在清除",
-  "click_here_to_view_sl_in_lng": "点击以<0>__lngName__</0> 使用  __appName__",
+  "click_here_to_view_sl_in_lng": "点击以<0>__lngName__</0> 使用 __appName__",
   "click_link_to_proceed": "单击下面的 <b>__clickText__</b> 继续。",
   "click_to_unpause": "单击即可取消暂停并重新激活您的 Overleaf 高级功能。",
   "clicking_delete_will_remove_sso_config_and_clear_saml_data": "点击<0>删除</0>将删除您的 SSO 配置并取消所有用户的链接。 仅当您的组设置中禁用 SSO 时,您才能执行此操作。",
@@ -304,7 +303,6 @@
   "clsi_unavailable": "抱歉,项目的编译服务器暂时不可用。请稍后再试。",
   "cn": "中文 (简体)",
   "code_check_failed_explanation": "您的代码有问题,无法自动编译",
-  "code_editor": "源代码编辑器",
   "collaborate_easily_on_your_projects": "轻松协作您的项目。处理更长或更复杂的文档。",
   "collaborate_online_and_offline": "使用自己的工作流进行在线和离线协作",
   "collaboration": "合作",
@@ -381,7 +379,7 @@
   "continue_to": "返回 __appName__",
   "continue_using_free_features": "继续使用我们的免费功能",
   "continue_with_free_plan": "继续使用免费计划",
-  "continue_with_service": "以  __service__ 继续",
+  "continue_with_service": "以 __service__ 继续",
   "copied": "已复制",
   "copy": "复制",
   "copy_project": "复制项目",
@@ -587,7 +585,6 @@
   "email": "电子邮件",
   "email_address": "邮件地址",
   "email_address_is_invalid": "电子邮箱地址无效",
-  "email_already_associated_with": "<b>__email1__</b>已与<b>__email2__</b> <b>__appName__</b>帐户相关联。",
   "email_already_registered": "此邮箱已被注册",
   "email_already_registered_secondary": "此电子邮件已注册为辅助电子邮件",
   "email_already_registered_sso": "此电子邮件已注册。请以另一种方式登录您的帐户,并通过您的帐户设置将您的帐户链接到新的提供商。",
@@ -1145,11 +1142,9 @@
   "log_in": "登录",
   "log_in_and_link": "登录并链接",
   "log_in_and_link_accounts": "登录并链接帐户",
-  "log_in_first_to_proceed": "您需要先<b>登录</b>才能继续。",
   "log_in_now": "现在登录",
   "log_in_with": "用 __provider__ 账户登陆",
   "log_in_with_a_different_account": "以另外一个账户登录",
-  "log_in_with_email": "使用 __email__ 登录",
   "log_in_with_existing_institution_email": "请使用您现有的 <0>__appName__</0> 帐户登录,以便将您的<0>__appName__</0>和<0>__institutionName__</0> 机构帐户关联起来。",
   "log_in_with_primary_email_address": "如果您使用电子邮件地址和密码登录,这将是要使用的电子邮件地址。 重要的 __appName__ 通知将发送到此电子邮件地址。",
   "log_in_with_sso": "通过 SSO 登录",
@@ -1590,7 +1585,7 @@
   "publish_as_template": "管理模版",
   "publisher_account": "发布者帐户",
   "publishing": "正在发表",
-  "pull_github_changes_into_sharelatex": "将GitHub中的更改调入  __appName__",
+  "pull_github_changes_into_sharelatex": "将GitHub中的更改调入 __appName__",
   "push_sharelatex_changes_to_github": "将 __appName__ 中的更改推送到GitHub",
   "push_to_github_pull_to_overleaf": "推送到 GitHub,拉取到 __appName__",
   "quoted_text": "引用文本",
@@ -1603,7 +1598,7 @@
   "read_only_token": "只读令牌",
   "read_write_token": "可读写令牌",
   "ready_to_join_x": "您已加入 __inviterName__",
-  "ready_to_join_x_in_group_y": "您已加入  __groupName__ 团队的 __inviterName__",
+  "ready_to_join_x_in_group_y": "您已加入 __groupName__ 团队的 __inviterName__",
   "ready_to_set_up": "准备好设置",
   "ready_to_use_templates": "现成的模板",
   "real_time_track_changes": "实时<0>跟踪更改</0>",
@@ -1763,7 +1758,7 @@
   "save_or_cancel-cancel": "取消",
   "save_or_cancel-or": "或者",
   "save_or_cancel-save": "保存",
-  "save_x_or_more": "节省 __percentage__  或更多",
+  "save_x_or_more": "节省 __percentage__ 或更多",
   "saving": "正在保存",
   "saving_notification_with_seconds": "保存 __docname__... (剩余 __seconds__ 秒)",
   "search": "搜索",
@@ -1875,7 +1870,7 @@
   "show_x_more_projects": "再显示 __count__ 个项目",
   "showing_x_out_of_n_projects": "显示 __x__ 个项目(共 __n__ 个)",
   "showing_x_results": "显示 __count__ 结果",
-  "showing_x_results_of_total": "显示  __count__ 个结果(共计__total__ )",
+  "showing_x_results_of_total": "显示 __count__ 个结果(共计__total__ )",
   "sign_up": "注册",
   "sign_up_for_free": "免费注册",
   "sign_up_for_free_account": "注册免费帐户并接收定期更新",
@@ -1950,7 +1945,7 @@
   "sso_login": "SSO 登录",
   "sso_logs": "单点登录日志",
   "sso_not_active": "单点登录未开启",
-  "sso_not_linked": "您尚未将帐户绑定到 __provider__。请以另一种方式登录到您的帐户,并通过您的帐户设置绑定您的 __provider__  帐户。",
+  "sso_not_linked": "您尚未将帐户绑定到 __provider__。请以另一种方式登录到您的帐户,并通过您的帐户设置绑定您的 __provider__ 帐户。",
   "sso_reauth_request": "SSO 二次身份验证请求已发送至 <0>__email__</0>",
   "sso_test_interstitial_info_1": "<0>开始此测试</0>之前,请确保您已<1>将 Overleaf 配置为 IdP 中的服务提供商</1>,并授权访问 Overleaf 服务。",
   "sso_test_interstitial_info_2": "点击<0>测试配置</0>会将您重定向到 IdP 的登录屏幕。 <1>阅读我们的文档</1>,了解测试期间发生的情况的完整详细信息。 如果您遇到困难,请查看我们的<2>SSO 故障排除建议</2>。",
@@ -1990,7 +1985,6 @@
   "subscribe": "订阅",
   "subscribe_now": "立即订阅",
   "subscribe_to_find_the_symbols_you_need_faster": "订阅以更快地找到您需要的符号",
-  "subscribe_to_plan": "订阅 __planName__",
   "subscription": "订购",
   "subscription_admins_cannot_be_deleted": "订阅时不能删除您的帐户。请取消订阅并重试。如果您一直看到此消息,请与我们联系。",
   "subscription_canceled": "订阅已取消",
@@ -2156,7 +2150,6 @@
   "too_many_search_results": "有超过 100 个结果。 请细化您的搜索。",
   "too_recently_compiled": "此项目是最近编译的,所以已跳过此编译。",
   "took_a_while": "这会花一段时间...",
-  "toolbar_choose_section_heading_level": "选择章节标题级别",
   "toolbar_code_visual_editor_switch": "代码和可视化编辑器切换",
   "toolbar_decrease_indent": "减少缩进",
   "toolbar_editor": "编辑器工具",
@@ -2181,7 +2174,7 @@
   "toolbar_selected_projects_management_actions": "选定的项目管理方法",
   "toolbar_selected_projects_remove": "删除选定的项目",
   "toolbar_selected_projects_restore": "恢复选定的项目",
-  "toolbar_table_insert_size_table": "插入 __size__  表格",
+  "toolbar_table_insert_size_table": "插入 __size__ 表格",
   "toolbar_table_insert_table_lowercase": "插入表格",
   "toolbar_text_formatting": "文本格式",
   "toolbar_text_style": "文本样式",
@@ -2367,7 +2360,6 @@
   "viewer": "查看者(只读)",
   "viewing": "视图",
   "viewing_x": "正在查看<0>__endTime__</0>",
-  "visual_editor": "可视化编辑器",
   "want_access_to_overleaf_premium_features_through_your_university": "想要通过您的大学访问__appName__高级功能吗?",
   "want_change_to_apply_before_plan_end": "如果您希望在当前计费周期结束前应用此更改,请与我们联系。",
   "we_are_unable_to_opt_you_into_this_experiment": "目前我们无法让您加入此实验,请确保您的组织已允许此功能,或稍后重试。",
@@ -2424,11 +2416,11 @@
   "yes_that_is_correct": "是正确的",
   "you": "你",
   "you_already_have_a_subscription": "你已经有一个订阅啦",
-  "you_are_a_manager_and_member_of_x_plan_as_member_of_group_subscription_y_administered_by_z": "您是由 <1>__adminEmail__</1> 管理的 <1>__groupName__</1>  团队的、<0>__planName__</0> 计划的 <1>管理员</1>和<1>成员</1>",
+  "you_are_a_manager_and_member_of_x_plan_as_member_of_group_subscription_y_administered_by_z": "您是由 <1>__adminEmail__</1> 管理的 <1>__groupName__</1> 团队的、<0>__planName__</0> 计划的 <1>管理员</1>和<1>成员</1>",
   "you_are_a_manager_and_member_of_x_plan_as_member_of_group_subscription_y_administered_by_z_you": "您是<1>您 (__adminEmail__)</1>管理的<0>__planName__</0>团体订阅<1>__groupName__</1>的<1>管理员</1>和<1>成员</1>。",
   "you_are_a_manager_of_commons_at_institution_x": "您是 <0>__institutionName__</0> 的 Overleaf Commons 订阅的<0>管理者</0>",
   "you_are_a_manager_of_publisher_x": "您是 <0>__publisherName__</0> 的<0>管理者</0>",
-  "you_are_a_manager_of_x_plan_as_member_of_group_subscription_y_administered_by_z": "您是由 <1>__adminEmail__</1> 管理的 <1>__groupName__</1>  团队的、<0>__planName__</0> 计划的 <1>管理员</1>",
+  "you_are_a_manager_of_x_plan_as_member_of_group_subscription_y_administered_by_z": "您是由 <1>__adminEmail__</1> 管理的 <1>__groupName__</1> 团队的、<0>__planName__</0> 计划的 <1>管理员</1>",
   "you_are_a_manager_of_x_plan_as_member_of_group_subscription_y_administered_by_z_you": "您是<1>您 (__adminEmail__</1>) 管理的<0>__planName__</0>团体订阅<1>__groupName__</1>的<1>管理员</1>。",
   "you_are_currently_logged_in_as": "您当前以 <b>__email__</b> 身份登录。",
   "you_are_on_a_paid_plan_contact_support_to_find_out_more": "您使用的是 __appName__ 付费计划。 <0>联系支持人员</0>以了解更多信息。",

+ 1 - 0
services/web/package.json

@@ -213,6 +213,7 @@
     "@codemirror/view": "6.38.6",
     "@eslint/compat": "^2.1.0",
     "@eslint/js": "^10.0.1",
+    "@eslint/json": "^2.0.0",
     "@floating-ui/react": "^0.27.5",
     "@istanbuljs/esm-loader-hook": "^0.3.0",
     "@juggle/resize-observer": "^3.3.1",

+ 0 - 1
services/web/scripts/esm-check-migration.mjs

@@ -20,7 +20,6 @@ const converted = new Set([
   'scripts/translations/insertHTMLFragments.js',
   'scripts/translations/replaceLinkFragments.js',
   'scripts/translations/sanitize.js',
-  'scripts/translations/sort.js',
   'scripts/translations/transformLocales.js',
   'scripts/translations/translateLocales.js',
   'scripts/translations/upload.js',

+ 0 - 9
services/web/scripts/translations/sort-locale-file.js

@@ -1,9 +0,0 @@
-import fs from 'fs'
-
-const file = process.argv[2]
-if (!file) {
-  console.error('Usage: node sort-locale-file.js <path>')
-  process.exit(1)
-}
-const obj = JSON.parse(fs.readFileSync(file, 'utf8'))
-fs.writeFileSync(file, JSON.stringify(obj, Object.keys(obj).sort(), 2) + '\n')

+ 0 - 49
services/web/scripts/translations/sort.js

@@ -1,49 +0,0 @@
-import fs from 'fs'
-import Path from 'path'
-import { fileURLToPath } from 'url'
-
-const __dirname = fileURLToPath(new URL('.', import.meta.url))
-
-const LOCALES = Path.join(__dirname, '../../locales')
-const CHECK = process.argv.includes('--check')
-
-async function main() {
-  await sortFile(
-    Path.join(__dirname, '../../frontend/extracted-translations.json')
-  )
-  for (const locale of await fs.promises.readdir(LOCALES)) {
-    if (locale === 'README.md') continue
-    const path = Path.join(LOCALES, locale)
-    await sortFile(path)
-  }
-}
-
-async function sortFile(path) {
-  const name = Path.basename(path)
-  const input = await fs.promises.readFile(path, 'utf-8')
-  const parsed = JSON.parse(input)
-  const sorted = JSON.stringify(parsed, Object.keys(parsed).sort(), 2) + '\n'
-  if (input === sorted) {
-    return
-  }
-  if (CHECK) {
-    console.warn('---')
-    console.warn(
-      name,
-      'is not sorted. Try running:\n\n',
-      '  web$ make sort_locales',
-      '\n'
-    )
-    console.warn('---')
-    throw new Error(name + ' is not sorted')
-  }
-  console.log('Storing sorted version of', name)
-  await fs.promises.writeFile(path, sorted)
-}
-
-try {
-  await main()
-} catch (error) {
-  console.error(error)
-  process.exit(1)
-}

+ 21 - 0
yarn.lock

@@ -3602,6 +3602,18 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@eslint/json@npm:^2.0.0":
+  version: 2.0.0
+  resolution: "@eslint/json@npm:2.0.0"
+  dependencies:
+    "@eslint/core": "npm:^1.2.1"
+    "@eslint/plugin-kit": "npm:^0.7.1"
+    "@humanwhocodes/momoa": "npm:^3.3.10"
+    natural-compare: "npm:^1.4.0"
+  checksum: 10c0/b4b6dcc06dcc981cbc0e582d6126890f238d115ba876f5dccf95e907cafc8c4952d20ea236249d831e79f94d208602253a131cfeabd5d30e0181581fa4fc643e
+  languageName: node
+  linkType: hard
+
 "@eslint/object-schema@npm:^3.0.5":
   version: 3.0.5
   resolution: "@eslint/object-schema@npm:3.0.5"
@@ -4107,6 +4119,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@humanwhocodes/momoa@npm:^3.3.10":
+  version: 3.3.10
+  resolution: "@humanwhocodes/momoa@npm:3.3.10"
+  checksum: 10c0/b80a99f517195ca11d9c3c19431ec5b1b9f4cd21437ed463b2db7e5244a0cdde70148b419e4721baa689b86b8bef2adbb6d1b2c1d140938f1af4640682e3b6d6
+  languageName: node
+  linkType: hard
+
 "@humanwhocodes/retry@npm:^0.4.0, @humanwhocodes/retry@npm:^0.4.2":
   version: 0.4.3
   resolution: "@humanwhocodes/retry@npm:0.4.3"
@@ -6613,6 +6632,7 @@ __metadata:
   version: 0.0.0-use.local
   resolution: "@overleaf/eslint-plugin@workspace:libraries/eslint-plugin"
   dependencies:
+    "@eslint/json": "npm:^2.0.0"
     "@typescript-eslint/parser": "npm:^8.59.4"
     lodash: "npm:^4.18.1"
   peerDependencies:
@@ -7248,6 +7268,7 @@ __metadata:
     "@customerio/cdp-analytics-node": "npm:^0.3.9"
     "@eslint/compat": "npm:^2.1.0"
     "@eslint/js": "npm:^10.0.1"
+    "@eslint/json": "npm:^2.0.0"
     "@floating-ui/react": "npm:^0.27.5"
     "@google-cloud/bigquery": "npm:^8.1.1"
     "@google-cloud/storage": "npm:^7.19.0"