Skip to content

Pedagogical enigma internationalization

PerfShop's pedagogical journey is built around 100 enigmas spread across five levels (BAC1 to BAC5, 20 enigmas per level) and a pool of 25 logic questions drawn at random during some journeys. All of this content is fully internationalized through dedicated JSON files.

Sources

backend/src/main/resources/i18n/enigmes/bac1/ to bac5/, backend/src/main/resources/i18n/logique/

Directory structure

backend/src/main/resources/i18n/
├── enigmes/
│   ├── bac1/
│   │   ├── enigmes_fr.json   ← complete (~5.9 KB)
│   │   ├── enigmes_en.json   ← complete (~5.4 KB)
│   │   ├── enigmes_de.json   ← placeholder
│   │   ├── enigmes_es.json   ← placeholder
│   │   ├── enigmes_it.json   ← placeholder
│   │   ├── enigmes_pt.json   ← placeholder
│   │   └── enigmes_zh.json   ← placeholder
│   ├── bac2/
│   ├── bac3/
│   ├── bac4/
│   └── bac5/
└── logique/
    ├── logique_fr.json       ← 25 questions
    └── logique_en.json       ← 25 questions

Each BAC1-BAC5 level has seven files — one per supported language. At the time of writing, only French and English are complete. The five others (German, Spanish, Italian, Portuguese, Chinese) are present as structural placeholders, ready to receive their translations.

The logic pool is organized differently: only two files (FR and EN), with no level structure, because logic-question drawing is independent of the journey level.

Enigma file format

Each enigmes_<lang>.json file is a flat JSON object whose keys follow the convention BAC<level>-<step>.<property>:

{
  "BAC1-1.text": "📡 SIGNAL RECEIVED. Welcome, agent.\n\nThe catalog contains 994 products.\nYour mission: find the product whose price matches the sum of the digits of this number.\n\nSet both the Min Price AND Max Price filters to this amount in euros.\nEnter this price.",
  "BAC1-1.hint": "Add the three digits: 9 + 9 + 4",
  "BAC1-1.culturalNote": "October 22, 1991 — Linus Torvalds releases Linux 0.0.1 on the internet. He is 21 and warns that it won't amount to much. Today, 96% of web servers run on Linux.",

  "BAC1-2.text": "📡 Well done. You found the 22€ USB-C cable.\n...",
  "BAC1-2.hint": "After 5 days: 60 − 35 = 25 (still above). After 6 days: 60 − 42 = ?"
}

The three properties per enigma

Key Role Required
BAC<N>-<step>.text Statement displayed to the student in the pedagogical overlay ✅ yes
BAC<N>-<step>.hint Hint revealed on demand (if hints are enabled) ✅ yes
BAC<N>-<step>.culturalNote Historical, scientific or cultural anecdote displayed after successful validation ⭕ optional, present on most enigmas

The answers are not in these files. They are hard-coded in the PedagogiqueEnigmeBacN Java classes and are universal: a number, a fixed string, an exact price. They never change from one language to another. This separation guarantees that a student following BAC3 in English and another in French solve exactly the same mathematical puzzle, with only the display language differing.

Placeholders and substitutions

Unlike the Spring Boot backend which uses MessageFormat with {0}, {1}, the enigma format is static: no on-the-fly substitution. Displayed values (product count, exact price, agent code) are either hard-written in the statement (like 994 products or 22€), or built by the React component that renders the overlay.

This simplicity is intentional: pedagogical content changes rarely and dynamic placeholders would needlessly complicate translation.

Loading by PedagogiqueEnigme

The PedagogiqueEnigme service (Java side) loads translations at startup via a loadTranslations(level) method that:

  1. Determines the file to read: i18n/enigmes/bac<level>/enigmes_<PERFSHOP_LANG>.json
  2. If the file does not exist or is empty (case of the DE/ES/IT/PT/ZH placeholders), falls back to enigmes_fr.json
  3. Parses the JSON with a minimal parser (zero additional Jackson dependency)
  4. Stores the key → value map in memory
  5. Exposes a t(level, key) method that resolves BAC<level>-<step>.<property>

The fallback to French is the reason the DE/ES/IT/PT/ZH files can remain as empty placeholders: the user experience stays functional in every language, even those not yet translated. Only the display is in French instead of the requested language.

Per-level architecture

Each BAC1 to BAC5 level has its own dedicated Java class:

backend/src/main/java/com/perfshop/pedagogique/
├── PedagogiqueEnigme.java         ← Common superclass
├── PedagogiqueEnigmeBac1.java     ← BAC1 logic + answers
├── PedagogiqueEnigmeBac2.java
├── PedagogiqueEnigmeBac3.java
├── PedagogiqueEnigmeBac4.java
└── PedagogiqueEnigmeBac5.java

The PedagogiqueEnigme superclass exposes JSON loading and key resolution. Each subclass provides:

  • The list of journey steps (20 per level)
  • The validation function for each step (mathematical computation, string comparison, expected hash)
  • The progression rule and any conditional jumps

For the engine details and the 20 steps per level, see BAC1 to BAC5 levels and Enigma system.

File volumetry

The text volume varies with enigma complexity:

Level FR size EN size Key count
BAC1 ~5.9 KB ~5.4 KB ~50
BAC2 ~6.5 KB ~6.0 KB ~55
BAC3 ~8.0 KB ~7.5 KB ~60
BAC4 ~10.0 KB ~9.2 KB ~65
BAC5 ~12.1 KB ~11.2 KB ~75

The BAC5 level is the longest because it combines detailed statements (cryptography, multi-step computations) with richer cultural notes. 20 steps × 5 levels × 2 complete languages represent around 100 KB of carefully written pedagogical content.

Logic pool format

The logique/logique_fr.json file is a JSON array of 25 objects:

[
  {"text": "Sequence: 2, 6, 18, 54, ?", "hint": "Each term is multiplied by 3"},
  {"text": "If A=1, B=2, ..., Z=26: what is M + A + T + H?", "hint": "M=13, A=1, T=20, H=8"},
  {"text": "How many diagonals does a hexagon (6-sided polygon) have?", "hint": "Formula: n×(n−3) / 2 with n=6"}
]

Two properties per question:

  • text — question statement
  • hint — optional hint

The answers are stored in a Java array parallel to the JSON pool, indexed in the same order. The 25 questions' indices are readable on the backend side; the drawing uses an LCG (linear congruential generator) seeded by the session's X-Student-Token to draw 5 reproducible questions per journey.

The 25 questions are deliberately at a general mathematics level — additions, sequences, elementary geometry, factorials, prime numbers. They do not depend on computer science knowledge and are solvable by any post-high-school student. Random (but deterministic per session) drawing prevents rote memorization.

Loading logic questions

The ThemeLogique service loads the FR/EN pool at startup and exposes:

public List<LogiqueQuestion> drawQuestions(String studentToken, int count)

The algorithm uses an LCG seeded by the token hash to draw count distinct indices in [0, 25), then returns the corresponding questions in the active language. The drawn indices are stored in the database in the logique_question_indices column of pedagogique_sessions (see Database schema) to guarantee reproducibility between the server and the client.

Previously, the LCG algorithm was duplicated on the frontend side — this approach was dropped because it generated desynchronizations when the token changed. From now on, only the backend draws the questions, and the frontend retrieves the final list via GET /pedagogique/logique/questions.

FR/EN symmetry

At the time of writing, the FR and EN files are in perfect symmetry for each BAC1-BAC5 level and for the logic pool. A "3 amigos" audit validated this parity before writing this documentation.

A simple script can verify symmetry:

jq 'keys | sort' enigmes_fr.json > /tmp/fr.keys
jq 'keys | sort' enigmes_en.json > /tmp/en.keys
diff /tmp/fr.keys /tmp/en.keys

For the DE/ES/IT/PT/ZH placeholder files, minimal content like {} is enough — the FR fallback applies automatically.

Adding a new language

The process is the following for each level:

  1. Copy enigmes_fr.json to enigmes_<lang>.json in the level folder
  2. Translate only the values (leave the BAC<N>-<step>.text etc. keys as-is)
  3. Repeat the same operation for every BAC1-BAC5 level
  4. Copy and translate logique/logique_fr.json to logique/logique_<lang>.json
  5. Deploy with PERFSHOP_LANG=<lang>

No Java code modification is necessary. The FR fallback will handle any keys that happen to be missing during the translation period.

Points of caution

  • The numbers in the statements are untouchable. If an enigma says "994 products" because the catalog contains exactly 994 products, this value must be preserved as-is in all languages. Same for prices (22€, 64€, 255€, 34.99€) that correspond to the pedagogical products created by the V10 migration.
  • Product names are sometimes in French. For example "Câble USB-C basique 1m" appears in the BAC1-2 statement. In English, the translation can use the English name ("Basic USB-C Cable 1m") but that name must match the real product name in the database — or accept that the English statement quotes the French name in quotes.
  • Agent credentials are universal. The passwords Cable22Go, Code64Exp6, Docker8080Get, Lorem255FF42, Rsa3Xor51Pi14 are fixed strings that must never be translated.
  • Cultural notes can be replaced. An anecdote about Linus Torvalds for the French-speaking audience can be replaced by a more relevant anecdote for a German or Spanish-speaking audience. This is not a simple translation — it is a cultural adaptation.

See also