I18n Hack
Apart from an inconclusive attempt several years ago, I have no particular knowledge of MediaWiki. Unwilling to face its complexity for a personal project, I called upon Gemini (Google) to get the project on track.
The full version of the session with Gemini is available [1].
Principles
Pages are divided into "root" pages and "leaf" pages suffixed with their language code.
The page you are currently reading is structured as follows:
WikiVlist:I18n_Hack <-- The Root (Redirection Hub) ├── WikiVlist:I18n_Hack/fr <-- French Leaf (FR Content) └── WikiVlist:I18n_Hack/en <-- English Leaf (EN Content)
The root is not intended to be viewed directly; it redirects to the leaf corresponding to the language defined by the browser or chosen by the user. Selection is handled by ULS, with the content language synchronized with the site language.
A similar mechanism is used for categories:
Category:XML <-- The Root (Redirection Hub) ├── Category:XML/fr <-- French Leaf (FR Content) └── Category:XML/en <-- English Leaf (EN Content)
Implementation
The implementation relies on PHP code for handling redirections and a template managing language choice and automatic category insertion.
PHP (LocalSettings.php)
This code handles browser language detection, redirection to `/fr` or `/en` leaves, and hijacks interwiki prefixes for local use.
// 1. LANGUAGE DETECTION & AUTOMATIC REDIRECTION
$scriptName = $_SERVER['SCRIPT_NAME'] ?? '';
$uri = $_SERVER['REQUEST_URI'] ?? '';
$queryString = $_SERVER['QUERY_STRING'] ?? '';
$acceptLang = $_SERVER['HTTP_ACCEPT_LANGUAGE'] ?? '';
if ( str_ends_with($scriptName, 'index.php') && empty($queryString) ) {
$allowedNamespaces = ['', 'Témoignage:', 'Projet:', 'Aide:', 'Utilisateur:', 'WikiVlist:', 'Category:', 'Catégorie:'];
$path = ltrim($uri, '/');
$firstColon = strpos($path, ':');
$currentNS = ($firstColon !== false) ? substr($path, 0, $firstColon + 1) : '';
if ( in_array($currentNS, $allowedNamespaces) ) {
// Redirect if not already on a language leaf
if ( !preg_match('/\/(fr|en)$/', $uri) && $uri !== '/' && $uri !== '' ) {
$cleanLang = strtolower(substr($acceptLang, 0, 2));
$target = '';
if ( $cleanLang === 'fr' ) { $target = $uri . (str_ends_with($uri, '/') ? 'fr' : '/fr'); }
elseif ( $cleanLang === 'en' ) { $target = $uri . (str_ends_with($uri, '/') ? 'en' : '/en'); }
if ( $target ) { header("Location: $target", true, 302); exit; }
}
}
}
// 2. FORCE INTERFACE LANGUAGE BASED ON SUFFIX
if ( preg_match( '/\/(en|fr)(?=[&#?]|$)/', $uri, $matches ) ) {
$forcedLang = $matches[1];
$wgLanguageCode = $forcedLang;
$wgDefaultUserOptions['language'] = $forcedLang;
$wgHooks['UserGetLanguageObject'][] = function ( $user, &$langCode, $context ) use ( $forcedLang ) {
$langCode = $forcedLang;
return true;
};
}
// 3. HANDLE LANGUAGE PREFIXES AS LOCAL LINKS
$wgHooks['InterwikiLoadPrefix'][] = function ( $prefix, &$data ) {
if ( $prefix === 'en' || $prefix === 'fr' ) {
$data = ['iw_url' => "/$1", 'iw_local' => 1, 'iw_trans' => 0];
return false;
}
return true;
};
Template:Langues
The template analyzes the page title via parser functions to identify whether it is on a root or a leaf, adapting the display and metadata accordingly.
<includeonly>{{#ifexpr: {{NAMESPACENUMBER}} = 14
| <div style="margin: 1em 0; padding: 8px 12px; background-color: #f8f9fa; border: 1px solid #c8ccd1; border-radius: 4px; display: inline-block;">🌐 {{#ifeq: {{#titleparts:{{PAGENAME}}|1|2}} | fr | [[:Category:{{#titleparts:{{PAGENAME}}|1|1}}|Root]] · '''Français''' · [[:en:Category:{{#titleparts:{{PAGENAME}}|1|1}}/en|English]] | {{#ifeq: {{#titleparts:{{PAGENAME}}|1|2}} | en | [[:Category:{{#titleparts:{{PAGENAME}}|1|1}}|Root]] · [[:fr:Category:{{#titleparts:{{PAGENAME}}|1|1}}/fr|Français]] · '''English''' | '''Root''' · [[:fr:{{FULLPAGENAME}}/fr|Français]] · [[:en:{{FULLPAGENAME}}/en|English]] }} }}</div>
| {{#if: {{#titleparts:{{FULLPAGENAME}}|1|2}}
| {{#ifeq: {{#titleparts:{{FULLPAGENAME}}|1|2}} | en
| {{DISPLAYTITLE:{{{en|{{{1|{{#titleparts:{{PAGENAME}}|1|1}}}}}}}}}}[[en:{{#titleparts:{{FULLPAGENAME}}/en}}]][[Category:English pages|{{#titleparts:{{PAGENAME}}|1|1}}]]
| {{DISPLAYTITLE:{{{fr|{{{1|{{#titleparts:{{PAGENAME}}|1|1}}}}}}}}}}[[fr:{{#titleparts:{{FULLPAGENAME}}/fr}}]][[Category:Pages en français|{{#titleparts:{{PAGENAME}}|1|1}}]]
}}
| <div style="margin: 1em 0; padding: 8px 12px; background-color: #f8f9fa; border: 1px solid #c8ccd1; border-radius: 4px; display: inline-block;">🌐 '''[[{{FULLPAGENAME}}/fr|Français]]''' · '''[[{{FULLPAGENAME}}/en|English]]'''</div>[[Category:Multilingual roots]]
}}
}}</includeonly>
Key implementation points
The architecture of the template relies on several technical subtleties to compensate for the absence of the Translate extension:
- Namespace 14 Management (Categories):
- The
{{#ifexpr: 3000 = 14test is crucial. To create simple navigation links between versions of a category without creating an infinite hierarchy, the template forces the use of the:prefix (e.g.,Category:Name/fr).
- Manipulation via
titleparts:
index 1|1isolates the page name without the suffix;index 1|2isolates the suffix itself (fr or en). This allows the template to automatically identify the language.
- Creative use of
DISPLAYTITLE:
- This allows "My Article/en" to be displayed simply as "My Article", hiding the technical structure of subpages from the end reader.
- Leaf Independence and Interwiki Languages:
- By using the
[[en:Name/en]]syntax (without a leading colon), the link is treated as a Wikipedia-style language link: it appears in the sidebar instead of cluttering the page body, following standard navigation patterns.
- Automatic Categorization:
- The template dynamically injects
[[Category:Pages en français]]or[[Category:English pages]].
Maintenance: Adding a Language
The system is extensible (e.g., for German `/de`) in three steps:
- LocalSettings.php: Add `de` to the detection regex and the `InterwikiLoadPrefix` Hook.
- Template:Langues: Add an
condition. - Categories: Create the maintenance category
[[Category:Deutsche Seiten]].
Technical Choices
Summary
The choice was influenced by the desire for simplicity and to avoid privileging a "pivot" language.
| Criterion | Translate Extension (Discarded) | The I18n_Hack (Retained) |
|---|---|---|
| Philosophy | Centralization: A pivot (source) language dictates the content of other versions. | Autonomy: Each version can evolve independently. |
| Editing Workflow | Rigid: Requires a "Edit → Mark → Translate" cycle. | Fluid: Direct editing of Wikitext. |
| Maintenance | Automated: Detects changes and marks translations as "outdated". | Manual: Consistency relies on the author's diligence. |
| Text Semantics | Proprietary <translate> tags.
|
Native MediaWiki structure (portability). |
| Interface (UI) | Handled by ULS. | Handled by ULS alone. |
Detailed History
Here is the simplified record of the exchanges that led to the design of this system.
---
With Gemini's help, the basic installation was quick, but things started getting complicated when I addressed the "multilingualism" aspect:
- Eric
- To manage multiple languages, can I install it in French and add other languages later?
- Gemini
- Yes, MediaWiki is natively designed for multilingualism. Here is how to handle it for your project:
- 1. Initial Installation
- Choose French as the default language.
- 2. Manage Content
- Option A: Subpage system (Recommended).
$wgNamespacesWithSubpages[NS_MAIN] = true;
- Option B: "Translate" extension (Too complex for Docker).
---
At that point, I was starting to feel confident in the project's feasibility, and I postponed this choice to fix other issues:
- Eric
- OK, it's going a bit all over the place, but we can consider we have a "POC" that shows feasibility... Let's make an action plan...
- Gemini
- That's great news! Here is your action plan:
- 1. Mobile Version
- Install
MinervaNeue. - 2. Multilingual Aspect
- Use the Translate extension. It manages translations by blocks.
- 3. Charter and Terminology
- The term "Selfpedia" is powerful: autobiography treated with the rigor of an encyclopedia.
---
I only understood later what Gemini meant by "copy-paste hell," and we went with the "Translate" option.
- Eric
- I'd rather see the multilingual question first...
- Gemini
- It's a pragmatic choice. The reference extension is Translate, along with UniversalLanguageSelector (ULS).
- Dockerfile Update
# Installation of ULS and Translate via curl/tar into /extensions/- Configuration in LocalSettings.php
wfLoadExtension( 'UniversalLanguageSelector' ); wfLoadExtension( 'Translate' );
- The Critical Point
- The "Job Runner"
- MediaWiki processes translations in the background. On Docker, you will need a separate container for
runJobs.php.
---
After struggling, we managed to install and run the Translate extension, only to discover its cumbersomeness: every section must be translated independently, and a tagging error breaks the mechanism.
- Eric
- This system is a nightmare; isn't there another option?
- Gemini
- I completely understand your frustration. Here is the alternative:
- Option 1
- The "Manual" (Simple, Clean, Flexible)
- We use manual subpages and a navigation template.
- The Principle: You create
My_PageandMy_Page/en. - The Tool: A small
template at the top of your pages. - Advantages: No complex tags, it never breaks, total freedom.
- The Principle: You create
- Eric
- Let's try Option 1.
- Gemini
- That's an excellent decision. For your Selfpedia, Option 1 is much healthier: it gives you back total control.
---
This is how we ended up back with a more "DIY" solution, which isn't perfect and required several successive adjustments but works quite well.
References
- ↑ Full chat session with Gemini accessed January 16, 2026