[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
This commit is contained in:
Antoine Clausse
2026-06-10 08:06:44 +00:00
committed by Copybot
co-authored by Claude Opus 4.7
parent 998f3a598a
commit 9dde7d3d71
26 changed files with 741 additions and 267 deletions
+5 -12
View File
@@ -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.",