From 87430801ef37a3ca7d9df93f07a98878458a83c8 Mon Sep 17 00:00:00 2001 From: Brandon Fowler Date: Wed, 16 Sep 2026 14:17:25 -0400 Subject: [PATCH 1/4] Add i18n support using Intuition --- api/index.php | 2 ++ composer.json | 3 +- composer.lock | 51 +++++++++++++++++++++++++++-- i18n/en.json | 34 ++++++++++++++++++++ i18n/fr.json | 34 ++++++++++++++++++++ includes/Footer.php | 6 ++-- includes/Form.php | 8 ++--- includes/Global.php | 2 -- includes/LanguageSelector.php | 42 ++++++++++++++++++++++++ includes/LinkCount.php | 57 ++++++++++++--------------------- includes/UI.php | 20 ++++++++++++ index.php | 21 +++++++++--- output/index.php | 2 ++ setlang/index.php | 15 +++++++++ static/NamespaceLookupWidget.js | 2 +- static/index.css | 24 ++++++++++++-- static/index.js | 4 +++ 17 files changed, 269 insertions(+), 58 deletions(-) create mode 100644 i18n/en.json create mode 100644 i18n/fr.json create mode 100644 includes/LanguageSelector.php create mode 100644 includes/UI.php create mode 100644 setlang/index.php diff --git a/api/index.php b/api/index.php index 7da7d6c..d0e4a4d 100644 --- a/api/index.php +++ b/api/index.php @@ -10,6 +10,8 @@ exit; } +UI::init('en'); // For footer + $description = "API for getting the number of links to any page on any Mediawiki project." ?> diff --git a/composer.json b/composer.json index da9d0b9..1a2b453 100644 --- a/composer.json +++ b/composer.json @@ -1,7 +1,8 @@ { "require": { "oojs/oojs-ui": "^0.53.1", - "php": ">=8.3" + "php": ">=8.3", + "krinkle/intuition": "^2.3" }, "scripts": { "test-win": "vendor\\bin\\phpunit.bat tests", diff --git a/composer.lock b/composer.lock index f7b27e0..2d4cce9 100644 --- a/composer.lock +++ b/composer.lock @@ -4,8 +4,53 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "6dbf8293831de4be746974e0ccd5b1f3", + "content-hash": "b4ff92e2e3d00c50c34424f174caada1", "packages": [ + { + "name": "krinkle/intuition", + "version": "v2.3.6", + "source": { + "type": "git", + "url": "https://github.com/wikimedia/labs-tools-intuition.git", + "reference": "5871a02c009621cf7c1bc1a240fab30688b9db48" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/wikimedia/labs-tools-intuition/zipball/5871a02c009621cf7c1bc1a240fab30688b9db48", + "reference": "5871a02c009621cf7c1bc1a240fab30688b9db48", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "require-dev": { + "mediawiki/mediawiki-codesniffer": "32.0.0", + "php-coveralls/php-coveralls": "^2.4", + "php-parallel-lint/php-parallel-lint": "^1.0.0", + "phpunit/phpunit": "^8.5.13" + }, + "type": "library", + "autoload": { + "files": [ + "src/defines.php" + ], + "psr-0": { + "MessagesFunctions": "language/" + }, + "psr-4": { + "Krinkle\\Intuition\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Framework for localisation in PHP.", + "support": { + "source": "https://github.com/wikimedia/labs-tools-intuition/tree/v2.3.6" + }, + "time": "2024-03-28T22:32:35+00:00" + }, { "name": "oojs/oojs-ui", "version": "v0.53.1", @@ -1901,12 +1946,12 @@ ], "aliases": [], "minimum-stability": "stable", - "stability-flags": {}, + "stability-flags": [], "prefer-stable": false, "prefer-lowest": false, "platform": { "php": ">=8.3" }, - "platform-dev": {}, + "platform-dev": [], "plugin-api-version": "2.6.0" } diff --git a/i18n/en.json b/i18n/en.json new file mode 100644 index 0000000..682eccf --- /dev/null +++ b/i18n/en.json @@ -0,0 +1,34 @@ +{ + "@metadata": { + "authors": [ + "Brandon Fowler" + ] + }, + "description": "View the number of links (wikilinks, redirects, transclusions, file links, and category links) to any page on any Wikimedia project.", + "nav-skip": "Skip to content", + "lang-select-label": "Select interface language", + "form-project": "Project", + "form-page": "Page", + "form-namespaces": "Namespaces", + "form-submit": "Submit", + "table-header-type": "Type", + "table-header-all": "All", + "table-header-direct": "Direct", + "table-header-direct-tooltip": "Number of pages that link to the page through a redirect", + "table-header-indirect": "Indirect", + "table-header-indirect-tooltip": "Number of pages that link to page using the actual page name", + "action-link-to-row": "Link to row", + "link-type-filelinks": "File links", + "link-type-categorylinks": "Category links", + "link-type-wikilinks": "Wikilinks", + "link-type-redirects": "Redirects", + "link-type-transclusions": "Transclusions", + "link-what-links-here": "What links here", + "footer-form": "Form", + "footer-api": "API", + "footer-created-by": "Created by $1", + "error": "Error", + "error-page-required": "Page name is required.", + "error-invalid-namespace-ids": "Invalid namespace IDs.", + "error-nonexistent-project": "That project does not exist..." +} diff --git a/i18n/fr.json b/i18n/fr.json new file mode 100644 index 0000000..5d937ec --- /dev/null +++ b/i18n/fr.json @@ -0,0 +1,34 @@ +{ + "@metadata": { + "authors": [ + "Brandon Fowler" + ] + }, + "description": "Voir le nombre de liens (wikiliens, redirections, transclusions, liens de fichiers et liens de catégories) vers n'importe quelle page d'un projet Wikimedia.", + "nav-skip": "Passer au contenu", + "lang-select-label": "Sélectionner la langue de l'interface", + "form-project": "Projet", + "form-page": "Page", + "form-namespaces": "Espaces de noms", + "form-submit": "Soumettre", + "table-header-type": "Type", + "table-header-all": "Tous", + "table-header-direct": "Direct", + "table-header-direct-tooltip": "Nombre de pages liées via une redirection", + "table-header-indirect": "Indirect", + "table-header-indirect-tooltip": "Nombre de pages liées en utilisant le nom réel de la page", + "action-link-to-row": "Lien vers la ligne", + "link-type-filelinks": "Liens de fichiers", + "link-type-categorylinks": "Liens de catégories", + "link-type-wikilinks": "Wikiliens", + "link-type-redirects": "Redirections", + "link-type-transclusions": "Transclusions", + "link-what-links-here": "Pages liées", + "footer-form": "Formulaire", + "footer-api": "API", + "footer-created-by": "Créé par $1", + "error": "Erreur", + "error-page-required": "Le nom de la page est requis.", + "error-invalid-namespace-ids": "Identifiants d'espace de noms invalides.", + "error-nonexistent-project": "Ce projet n'existe pas...." +} diff --git a/includes/Footer.php b/includes/Footer.php index f8baacf..f21b415 100644 --- a/includes/Footer.php +++ b/includes/Footer.php @@ -22,10 +22,10 @@ public function getHtml() { ])->appendContent('BrandonXLF'); $parts = [ - (new OOUI\Tag('a'))->setAttributes(['href' => "./{$this->rel}/"])->appendContent('Form'), - (new OOUI\Tag('a'))->setAttributes(['href' => "./{$this->rel}/api/"])->appendContent('API'), + (new OOUI\Tag('a'))->setAttributes(['href' => "./{$this->rel}/"])->appendContent(_('footer-form')), + (new OOUI\Tag('a'))->setAttributes(['href' => "./{$this->rel}/api/"])->appendContent(_('footer-api')), "$gitHubLink ($revLink)", - "Created by $authorLink" + _html('footer-created-by', [ 'variables' => [ $authorLink ], 'raw-variables' => true ]) ]; $content = new OOUI\HtmlSnippet(implode(' | ', $parts)); diff --git a/includes/Form.php b/includes/Form.php index 11ddff4..cd1e689 100644 --- a/includes/Form.php +++ b/includes/Form.php @@ -13,7 +13,7 @@ public function getHtml() { 'infusable' => true ]), [ 'align' => 'top', - 'label' => 'Project' + 'label' => _('form-project') ] ), new OOUI\FieldLayout( @@ -25,7 +25,7 @@ public function getHtml() { 'infusable' => true ]), [ 'align' => 'top', - 'label' => 'Page' + 'label' => _('form-page') ] ), new OOUI\FieldLayout( @@ -37,14 +37,14 @@ public function getHtml() { 'infusable' => true ]), [ 'align' => 'top', - 'label' => 'Namespaces' + 'label' => _('form-namespaces') ] ), new OOUI\FieldLayout( new OOUI\ButtonInputWidget([ 'id' => 'submit', 'type' => 'submit', - 'label' => 'Submit', + 'label' => _('form-submit'), 'flags' => ['primary', 'progressive'], 'infusable' => true ]), [ diff --git a/includes/Global.php b/includes/Global.php index 9be1e7b..021e5b8 100644 --- a/includes/Global.php +++ b/includes/Global.php @@ -1,7 +1,5 @@ getAvailableLangs(); + + $dropdown = (new OOUI\DropdownInputWidget([ + 'name' => 'lang', + 'value' => $I18N->getLang(), + 'options' => array_map(function($name, $key) { + return [ + 'data' => $key, + 'label' => $name, + ]; + }, $langs, array_keys($langs)) + ]))->addClasses(['lang-select-cnt']); + + $params = $_GET; + unset($params['userlang']); + + return (new OOUI\Tag('form'))->setAttributes([ + 'action' => './setlang/?' . http_build_query($params), + 'method' => 'post', + ])->addClasses(['lang-select-form'])->appendContent( + (new OOUI\LabelWidget([ + 'label' => new OOUI\HtmlSnippet(' + + ') + ]))->setAttributes([ + 'title' => _('lang-select-label'), + 'aria-label' => _('lang-select-label'), + 'for' => $dropdown->getInputId(), + ]), + $dropdown + )->toString(); + } +} diff --git a/includes/LinkCount.php b/includes/LinkCount.php index 6baa94a..2acd1a7 100644 --- a/includes/LinkCount.php +++ b/includes/LinkCount.php @@ -1,8 +1,6 @@ [ - 'name' => 'File links', - 'url' => '/wiki/Special:WhatLinksHere/PAGE?hidetrans=1&hidelinks=1' - ], - 'categorylinks' => [ - 'name' => 'Category links', - 'url' => '/wiki/PAGE' // WhatLinksHere doesn't show category links - ], - 'wikilinks' => [ - 'name' => 'Wikilinks', - 'url' => '/wiki/Special:WhatLinksHere/PAGE?hidetrans=1&hideimages=1' - ], - 'redirects' => [ - 'name' => 'Redirects', - 'url' => '/wiki/Special:WhatLinksHere/PAGE?hidelinks=1&hidetrans=1&hideimages=1' - ], - 'transclusions' => [ - 'name' => 'Transclusions', - 'url' => '/wiki/Special:WhatLinksHere/PAGE?hidelinks=1&hideimages=1' - ] + private $typeLinks = [ + 'filelinks' => '/wiki/Special:WhatLinksHere/PAGE?hidetrans=1&hidelinks=1', + 'categorylinks' => '/wiki/PAGE', // WhatLinksHere doesn't show category links + 'wikilinks' => '/wiki/Special:WhatLinksHere/PAGE?hidetrans=1&hideimages=1', + 'redirects' => '/wiki/Special:WhatLinksHere/PAGE?hidelinks=1&hidetrans=1&hideimages=1', + 'transclusions' => '/wiki/Special:WhatLinksHere/PAGE?hidelinks=1&hideimages=1', ]; public function __construct(string $page, string $project, $namespaces = '') { @@ -39,7 +22,7 @@ public function __construct(string $page, string $project, $namespaces = '') { } if (!$page) { - $this->error = 'Page name is required.'; + $this->error = _('error-page-required'); return; } @@ -49,7 +32,7 @@ public function __construct(string $page, string $project, $namespaces = '') { foreach ($namespaces ? explode(',', $namespaces) : [] as $rawNamespace) { if (!is_numeric($rawNamespace)) { - $this->error = 'Invalid namespace IDs.'; + $this->error = _('error-invalid-namespace-ids'); return; } } @@ -57,7 +40,7 @@ public function __construct(string $page, string $project, $namespaces = '') { $projectInfo = ProjectLookup::lookupProject($project); if (!$projectInfo) { - $this->error = 'That project does not exist...'; + $this->error = _('error-nonexistent-project'); return; } @@ -125,7 +108,7 @@ public function getHtml() { } if (!isset($this->counts)) { - return LinkCount::$description; + return ''; } $validCounts = array_filter($this->counts, function($val) { @@ -143,18 +126,18 @@ public function getHtml() { ])->appendContent( (new OOUI\Tag('div'))->setAttributes([ 'role' => 'columnheader' - ])->appendContent('Type'), + ])->appendContent(_('table-header-type')), (new OOUI\Tag('div'))->setAttributes([ 'role' => 'columnheader' - ])->appendContent('All'), + ])->appendContent(_('table-header-all')), (new OOUI\Tag('abbr'))->setAttributes([ - 'title' => 'Number of pages that link to page using the actual page name', + 'title' => _('table-header-direct-tooltip'), 'role' => 'columnheader' - ])->appendContent('Direct'), + ])->appendContent(_('table-header-direct')), (new OOUI\Tag('abbr'))->setAttributes([ - 'title' => 'Number of pages that link to the page through a redirect', + 'title' => _('table-header-indirect-tooltip'), 'role' => 'columnheader' - ])->appendContent('Indirect') + ])->appendContent(_('table-header-indirect')) ) ); @@ -164,12 +147,12 @@ public function getHtml() { $singleCount = is_int($count); $label = (new OOUI\Tag('a'))->setAttributes([ - 'href' => $this->projectURL . str_replace('PAGE', $encodedPage, $this->typeInfo[$key]['url']) - ])->appendContent($this->typeInfo[$key]['name']); + 'href' => $this->projectURL . str_replace('PAGE', $encodedPage, $this->typeLinks[$key]) + ])->appendContent(_('link-type-' . $key)); $link = (new OOUI\Tag('a'))->addClasses(['hash-link'])->setAttributes([ 'href' => '#' . $key, - 'title' => 'Link to row' + 'title' => _('action-link-to-row') ])->appendContent('(#)'); $all = number_format($singleCount ? $count : $count['all']); @@ -200,7 +183,7 @@ public function getHtml() { $links = (new OOUI\Tag('div'))->addClasses(['links'])->appendContent( (new OOUI\Tag('a'))->setAttributes([ 'href' => $this->projectURL . '/wiki/Special:WhatLinksHere/' . $encodedPage - ])->appendContent('What links here') + ])->appendContent(_('link-what-links-here')), ); return $out . $links; diff --git a/includes/UI.php b/includes/UI.php new file mode 100644 index 0000000..3829cc7 --- /dev/null +++ b/includes/UI.php @@ -0,0 +1,20 @@ + 'linkcount', + 'globalfunctions' => true, + 'lang' => $lang, + ]); + + $I18N->registerDomain('linkcount', __DIR__ . '/../i18n'); + } +} diff --git a/index.php b/index.php index bac393f..8c440e2 100644 --- a/index.php +++ b/index.php @@ -1,25 +1,36 @@ - + <?php echo $linkCount->getTitle(); ?> - + - +
-

Link Count

- + +
+

Link Count

+ getHtml(); ?> +
+
getHtml(); ?>
getHtml(); ?>
diff --git a/output/index.php b/output/index.php index 18b02af..98e497e 100644 --- a/output/index.php +++ b/output/index.php @@ -2,6 +2,8 @@ require '../vendor/autoload.php'; +UI::init(); + $linkCount = new LinkCount(get('page'), get('project'), get('namespaces')); header('Content-Type: application/json'); diff --git a/setlang/index.php b/setlang/index.php new file mode 100644 index 0000000..a860339 --- /dev/null +++ b/setlang/index.php @@ -0,0 +1,15 @@ +setCookie('userlang', $_POST['lang'] ?? ''); + +$qs = http_build_query($_GET); +header('Location: ../' . ($qs ? '?' . $qs : '')); diff --git a/static/NamespaceLookupWidget.js b/static/NamespaceLookupWidget.js index 47b05e1..39be676 100644 --- a/static/NamespaceLookupWidget.js +++ b/static/NamespaceLookupWidget.js @@ -51,7 +51,7 @@ NamespaceLookupWidget.prototype.setDomain = function(domain) { options.push({ data: info.id.toString(), - label: info.name || '(Article)' + label: info.name || '(Main)' }); } diff --git a/static/index.css b/static/index.css index 60c1953..edc7d0d 100644 --- a/static/index.css +++ b/static/index.css @@ -24,6 +24,25 @@ abbr { padding: 4px; } +header { + display: flex; + justify-content: space-between; + flex-wrap: wrap; + gap: 1rem; + margin: 1.5rem 0; +} + +h1 { + margin: 0; +} + +.lang-select-form { + display: flex; + align-items: center; + gap: 0.5rem; + line-height: 1; +} + main, footer { max-width: 700px; @@ -34,8 +53,9 @@ footer { margin-top: 8px; } -footer, -#out { +#form, +#out, +footer { margin-top: 1.5em; } diff --git a/static/index.js b/static/index.js index 6b8b804..72d331d 100644 --- a/static/index.js +++ b/static/index.js @@ -91,4 +91,8 @@ $('#skip').on('click', function(e) { out.trigger('focus'); }); +$('.lang-select-cnt select').on('change', function() { + this.form.submit(); +}); + namespacesInput.$element.replaceWith(namespacesSelect.$element); From a07e93b4367d2cfee20bd1c2faa42cc226ded10d Mon Sep 17 00:00:00 2001 From: Brandon Fowler Date: Wed, 16 Sep 2026 21:39:06 -0400 Subject: [PATCH 2/4] Add logo --- index.php | 5 ++++- static/index.css | 10 ++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/index.php b/index.php index 8c440e2..425f517 100644 --- a/index.php +++ b/index.php @@ -27,7 +27,10 @@
-

Link Count

+
+

Link Count

+ Link Count logo +
getHtml(); ?>
diff --git a/static/index.css b/static/index.css index edc7d0d..2b19c00 100644 --- a/static/index.css +++ b/static/index.css @@ -32,6 +32,12 @@ header { margin: 1.5rem 0; } +hgroup { + display: flex; + align-items: center; + gap: 0.75rem; +} + h1 { margin: 0; } @@ -43,6 +49,10 @@ h1 { line-height: 1; } +.lang-select-cnt { + max-width: 8.5rem; +} + main, footer { max-width: 700px; From 7e9538d19104977315ecc016fc03c9aa0c2d1767 Mon Sep 17 00:00:00 2001 From: Brandon Fowler Date: Wed, 16 Sep 2026 22:42:09 -0400 Subject: [PATCH 3/4] Add qqq.json --- i18n/en.json | 2 +- i18n/fr.json | 2 +- i18n/qqq.json | 34 ++++++++++++++++++++++++++++++++++ 3 files changed, 36 insertions(+), 2 deletions(-) create mode 100644 i18n/qqq.json diff --git a/i18n/en.json b/i18n/en.json index 682eccf..71e0e39 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -5,7 +5,7 @@ ] }, "description": "View the number of links (wikilinks, redirects, transclusions, file links, and category links) to any page on any Wikimedia project.", - "nav-skip": "Skip to content", + "nav-skip": "Skip to results", "lang-select-label": "Select interface language", "form-project": "Project", "form-page": "Page", diff --git a/i18n/fr.json b/i18n/fr.json index 5d937ec..bea9dba 100644 --- a/i18n/fr.json +++ b/i18n/fr.json @@ -5,7 +5,7 @@ ] }, "description": "Voir le nombre de liens (wikiliens, redirections, transclusions, liens de fichiers et liens de catégories) vers n'importe quelle page d'un projet Wikimedia.", - "nav-skip": "Passer au contenu", + "nav-skip": "Passer aux résultats", "lang-select-label": "Sélectionner la langue de l'interface", "form-project": "Projet", "form-page": "Page", diff --git a/i18n/qqq.json b/i18n/qqq.json new file mode 100644 index 0000000..9d9f8be --- /dev/null +++ b/i18n/qqq.json @@ -0,0 +1,34 @@ +{ + "@metadata": { + "authors": [ + "Brandon Fowler" + ] + }, + "description": "Description of Link Count's functionality. Shown as a short description at the top of the page and in search results.", + "nav-skip": "Hidden link that allows keyboard users to skip to the tool's results.", + "lang-select-label": "Tooltip for the language selection dropdown that controls the interface language.", + "form-project": "Label for the project input field in the form. This is where users enter the URL of the Wikimedia project they want to analyze.", + "form-page": "Label for the page input field in the form. This is where users enter the name of the page they want to analyze.", + "form-namespaces": "Label for the namespaces input field in the form. This is where users can specify which namespaces to include in the analysis.", + "form-submit": "Text for the submit button in the form.", + "table-header-type": "Header for the 'Type' column in the results table. This column indicates the type of link (wikilink, redirect, etc.).", + "table-header-all": "Header for the 'All' column in the results table. This column shows the total number of links for each type.", + "table-header-direct": "Header for the 'Direct' column in the results table. This column shows the number of pages that link to the analyzed page through a redirect.", + "table-header-direct-tooltip": "Tooltip that explains the 'Direct' column in the results table.", + "table-header-indirect": "Header for the 'Indirect' column in the results table. This column shows the number of pages that link to the analyzed page directly using it's name (rather than through a redirect).", + "table-header-indirect-tooltip": "Tooltip that explains the 'Indirect' column in the results table.", + "action-link-to-row": "Tooltip for a small button that allows users to add a link to the current table row to the clipboard.", + "link-type-filelinks": "Label for the row that shows the number of file links. Should be concise.", + "link-type-categorylinks": "Label for the row that shows the number of category links. Should be concise.", + "link-type-wikilinks": "Label for the row that shows the number of wikilinks (standard internal links). Should be concise.", + "link-type-redirects": "Label for the row that shows the number of redirects. Should be concise.", + "link-type-transclusions": "Label for the row that shows the number of transclusions (pages that use this page as a template). Should be concise.", + "link-what-links-here": "Link to the MediaWiki '{{msg-mw|whatlinkshere-title}}' special page for the analyzed page. Should probably be '{{msg-mw|whatlinkshere}}'.", + "footer-form": "Link to the form page for Link Count.", + "footer-api": "Link to the API page for Link Count.", + "footer-created-by": "Text indicating who authored/created Link Count. $1 is a placeholder for the author's name.", + "error": "Title for an error state.", + "error-page-required": "Error shown when the user submits the form without entering a page name.", + "error-invalid-namespace-ids": "Error shown when the user submits the form with invalid namespace IDs.", + "error-nonexistent-project": "Error shown when the user submits the form with a non-existent MediaWiki project." +} From 3c99945395d80935abc8db42d55bc2fa7dbb1b43 Mon Sep 17 00:00:00 2001 From: Brandon Fowler Date: Wed, 16 Sep 2026 22:46:34 -0400 Subject: [PATCH 4/4] Remove qqq from dropdown --- includes/LanguageSelector.php | 1 + 1 file changed, 1 insertion(+) diff --git a/includes/LanguageSelector.php b/includes/LanguageSelector.php index 03fce56..9d4fc8d 100644 --- a/includes/LanguageSelector.php +++ b/includes/LanguageSelector.php @@ -8,6 +8,7 @@ public function getHtml() { global $I18N; $langs = $I18N->getAvailableLangs(); + unset($langs['qqq']); $dropdown = (new OOUI\DropdownInputWidget([ 'name' => 'lang',