Skip to content

Internationalization support - #1131

Open
ltfish wants to merge 15 commits into
pwncollege:masterfrom
ltfish:feat/i18n
Open

Internationalization support#1131
ltfish wants to merge 15 commits into
pwncollege:masterfrom
ltfish:feat/i18n

Conversation

@ltfish

@ltfish ltfish commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

This PR adds internationalization (multi-language) support to dojo, module, and challenge descriptions. All descriptions in another language are stored along side the dojo in i18n/<la>/dojo/module/challenges where <la> is a two-letter language code, e.g., ko for Korean.

This PR does not alter the schema of the database.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4c96ebc348

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread dojo_plugin/utils/dojo.py Outdated
Comment thread dojo_plugin/models/__init__.py
Comment thread db/migrate-i18n.sh Outdated
ltfish added 11 commits August 24, 2026 22:47
Dojo, module, challenge, and resource names/descriptions can now carry
translations, either inline via a `translations:` block or through a parallel
`i18n/<lang>/` tree in the dojo repo. Translations live in the existing JSONB
`data` column, so no schema migration is needed, and a missing translation falls
back to the source text.

The navbar gains a language switcher; the choice is kept in a cookie and, for
logged-in users, in a new `dojo_user_preferences` table.
Adds docs/i18n.md covering the i18n/ tree, the resource locator rules, and how a
language is chosen, plus an integration test dojo exercising translations at the
dojo, module, resource, header, and challenge levels.
Search now matches the name and description subfields of a translation rather
than the whole JSON blob, so a query cannot match on JSON key names. The
language cookie inherits the request's Secure flag, and the request language is
resolved lazily so requests that render no content skip the preference lookup.
An imported challenge or module that carried any translation of its own discarded
every translation it would otherwise inherit from its source -- so a consolidated
dojo translating just a challenge name lost the source's translated description.
Translations now merge per language and per field, with the local value winning.
A repeated resource name -- common in consolidated dojos, where several source
modules each contribute a "Resources" block -- silently sent every translation to
the first match. Ambiguous locators now fail and point the author at `index`.
Jinja imports do not carry the render context, so the switcher macro could not
see the languages, current_language, language_switch_next, and Session variables
its own template supplies -- every page raised UndefinedError on 'languages'.

Also returns a safe default from language_switch_next() outside a request
context, where there is no request to read the current path from.
The switcher's label lives in a display:block span, so bootstrap's inline-block
.dropdown-toggle::after caret wrapped onto a second row. Suppress that caret and
put an explicit fa-caret-down inside the label span, matching how the navbar's
Hide Navbar item already renders its caret.
base.html includes the navbar, so an undefined `languages` raised out of the
switcher on every template that extends it -- error.html included, meaning the
error page could not render either. That is the symptom when templates are
swapped under a still-running plugin, since dojo_plugin and dojo_theme are both
bind-mounted but load() only runs at process start.

Skip the switcher when the context processor has not run instead of raising.
The switcher wrote each user's language to a new `dojo_user_preferences` table, which
made the feature require a schema change for the sake of one string. Drop the table and
the model: the `dojo_language` cookie already carries the choice, and `Accept-Language`
covers a browser that has never used the switcher.

The tradeoff is that the choice no longer follows an account to another browser. That is
worth a table this feature does not otherwise need, and the fallback chain still lands
non-English visitors in their own language on a first visit.
…slate

The loader passed `translations=... or {}` for every dojo, module, challenge, and
resource, and the import paths wrote a merged blob unconditionally, so every row grew a
`translations` key -- usually an empty one -- on the next dojo update. That is a lot of
churn for a feature most dojos do not use.

Write the key only when there is a translation to store, so an untranslated dojo's `data`
is exactly what a build without i18n produces. A dojo update that removes translations
still clears the ones already stored.
Add `zh-CN`, `zh-TW`, and `it` to the switcher, along with their translations of the
`Challenges` header the platform synthesizes for modules that use the `challenges:`
shorthand -- otherwise that one heading stays English inside an otherwise translated
module.

Chinese needs a nudge that Korean did not: browsers name it by script (`zh-Hans-CN`) or
generically (`zh`) as often as by region, and truncating those from the right never
reaches `zh-CN` or `zh-TW`. LANGUAGE_ALIASES maps what browsers send onto what is
offered. It only steers the choice of language, not how a translation is looked up once
the language is chosen, so a dojo still files its translations under the offered tags.
conftest raises a collection-time UsageError for any test whose file is missing from
TEST_FILE_TIERS, so the whole file failed with "Test tier is not configured" rather than
running. The tier map arrived with the test revamp on master after this branch was
written, and a rebase has no way to notice that a new test file needs an entry in it.

Semantic, alongside test_dojos.py and test_module_resources.py: these drive dojo pages
and their JSON over HTTP.
@codecov

codecov Bot commented Aug 25, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 70.83333% with 63 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
dojo_plugin/utils/dojo.py 29.87% 54 Missing ⚠️
dojo_plugin/i18n.py 87.69% 8 Missing ⚠️
dojo_plugin/models/__init__.py 97.36% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

ltfish added 3 commits August 25, 2026 01:20
…itcher

The switcher's `next` field was `request.path` plus the request's own query string, and
the navbar renders on every page -- error pages included. The API blueprint's exception
handler logs and re-raises, so an abort() inside a flask-restx resource falls through to
Flask's HTML error page rather than a JSON body, and that page then echoed back whatever
the caller passed.

test_workspace_api_unknown_user_is_not_an_oracle caught it: two 403s that must be
byte-identical differed by the `user=` each request had asked about.

Send the path alone. Switching language still returns to the page you were on, and
dropping the query also drops any `lang`, which was the reason the query was being
filtered in the first place.
An imported module or challenge merges its translations with the source's per language
and per field, but the dojo level picked one mapping or the other outright. A dojo that
imported another and translated only its own name therefore threw away the source's
translated description, and the page fell back to English for it.

Also stop storing an empty translated value. An empty DESCRIPTION.md in an i18n tree is a
stub a translator has not filled in yet, not a request to render nothing, and storing the
empty string left it ambiguous.

Adds test_i18n.py coverage for dojo-level import, which had none.
Three tests read a dojo page as somebody other than an admin -- two anonymously and one
as a freshly registered user -- because that is the point of a per-session language: it
has to work without an account. The fixture dojo had no `type`, so it was private and
dojo_accessible() 404'd for all three.

They passed whenever i18n_import_dojo happened to be built first, since promoting the
dojo to official makes it viewable as a side effect. Under xdist that depends on how the
file is distributed. Declaring the dojo public makes it hold on its own.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant