Jump to content

I18n Hack

From WikiVlist


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 = 14 test 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|1 isolates the page name without the suffix; index 1|2 isolates 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:

  1. LocalSettings.php: Add `de` to the detection regex and the `InterwikiLoadPrefix` Hook.
  2. Template:Langues: Add an condition.
  3. 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_Page and My_Page/en.
  • The Tool: A small template at the top of your pages.
  • Advantages: No complex tags, it never breaks, total freedom.
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

  1. Full chat session with Gemini accessed January 16, 2026