adamjenkins / moodle-local_langcrowd
Language Crowdsourcing for Moodle — users collaboratively build and vote on language pack translations inline in the Moodle interface.
Package info
github.com/adamjenkins/moodle-local_langcrowd
Type:moodle-local
pkg:composer/adamjenkins/moodle-local_langcrowd
Requires
- moodle/composer-installer: ^1.0
- moodle/moodle: ^5.2
Requires (Dev)
None
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
A Moodle 5.2+ local plugin that lets your users collaboratively build and vote on language pack translations directly inside the Moodle interface — no external tools required.
How it works
When crowdsourcing is enabled, logged-in users see a floating Improve translations button. Turning it on ("translate mode") annotates the language strings on screen with two small inline badge buttons — so the buttons only appear when the user opts in, not on every page automatically. The choice is remembered for the rest of the session.
- ✓ (tick) — the user approves the current translation. Its tooltip shows the progress towards the lock threshold (e.g. "5/10 approvals"). Once a string reaches the configured vote threshold it is locked and served immediately as the active translation.
- ✗ (cross) — the user disagrees with the current translation and is prompted (in a themed dialog showing the English source) to type an alternative. Their suggestion is queued for admin review.
After voting on a string the buttons disappear, but a short-lived Undo link lets the user withdraw the vote. On touch devices (which have no hover) the buttons are always visible.
The target language is the user's current Moodle interface language. Switching interface language switches the translation target automatically.
The overlay can be restricted to specific roles, specific installed language packs, and specific components via admin settings.
Requirements
- Moodle 5.2 – 5.3 (version ≥ 2026042000, supported branches 502–503)
- PHP 8.3+ (Moodle 5.2 requires PHP 8.3)
- A writable
config.php(one line must be added — see Installation)
Installation
1. Copy the plugin
cp -r moodle-local_langcrowd /path/to/moodle/local/langcrowd
Or clone directly:
cd /path/to/moodle/local
git clone https://github.com/adamjenkins/moodle-local_langcrowd langcrowd
Or install with Composer (the moodle/composer-installer
places it under local/langcrowd automatically):
composer require adamjenkins/moodle-local_langcrowd
2. Add the custom string manager to config.php
Open your Moodle config.php and add the following line before the require_once at the end of the file:
$CFG->customstringmanager = '\local_langcrowd\string_manager';
This intercepts every get_string() call during web requests so the footer hook knows which strings appear on each page. The plugin displays a warning on its settings page if this line is missing.
3. Run the Moodle upgrade
sudo -u www-data php admin/cli/upgrade.php --non-interactive
Or visit Site administration → Notifications in your browser.
4. Enable the plugin
Go to Site administration → Plugins → Local plugins → Language Crowdsourcing → Language Crowdsourcing Settings and tick Enable crowdsourcing.
Uninstalling
When uninstalling the plugin, also remove the $CFG->customstringmanager line
from config.php. Moodle degrades gracefully if it is left behind (it logs a
debugging notice and falls back to the standard string manager), but the stale
line references a class that no longer exists and should not stay in place.
Admin settings
Navigate to Site administration → Plugins → Local plugins → Language Crowdsourcing → Language Crowdsourcing Settings (requires moodle/site:config).
| Setting | Description | Default |
|---|---|---|
| Enable crowdsourcing | Master on/off switch. | Off |
| Lock translate mode on | Hides the floating "Improve translations" toggle and keeps the overlay always on for everyone who can use it. | Off |
| Show admin link in navbar | Adds a "Language Crowdsourcing" link to the primary nav, for users with local/langcrowd:admin. |
Off |
| Admin approve vote locks immediately | When enabled, an approve vote from a site administrator locks the string at once, bypassing the vote threshold. Reject votes and non-admin voters are unaffected. | Off |
| Approval threshold | Approve-votes needed to lock a string in. | 10 |
| Max strings per page | Cap on annotated strings per page. Raise for complex admin pages. | 5000 |
| Button display mode | Hover hides buttons until the user mouses over a string; Always keeps them visible. Ignored on touch devices, where buttons are always visible. |
Hover |
| String highlight colour | Colour of the outline drawn around a string on button hover. | #fff3cd |
| Roles allowed to vote | Restrict voting to specific roles. Empty = all logged-in users. | (all) |
| Languages to enable crowdsourcing for | Restrict the overlay to specific installed language packs. Empty = all languages. | (all) |
| Components to enable crowdsourcing for | Restrict the overlay to specific components (e.g. mod_forum, core). Empty = all. Every installed component is listed, including ones whose strings have not been seen yet. Names are normalised, so a plugin that calls get_string() with both forum and mod_forum is one component. |
(all) |
The overlay is only shown to users who hold the
local/langcrowd:votecapability, and the role/language/component restrictions are enforced both in the UI and in the web services (including votes and suggestions on strings recorded before a component was excluded).
Admin reports
Accessible at Site administration → Plugins → Local plugins → Language Crowdsourcing to anyone with local/langcrowd:admin (managers by default — moodle/site:config is not needed for the reports and the exporter).
Overview
The landing page. Shows at-a-glance counts (Pending / Pushed / Locked / total strings tracked / pending suggestions), the most-voted strings still open for voting, and the most recent suggestions, with quick links to the reports and the exporter.
Voting Report
Shows all strings across the three active statuses in one view:
| Status | Badge | Meaning |
|---|---|---|
| Pending | grey | Seen by users and open for voting; not yet promoted. |
| Pushed | blue | Suggestion accepted by admin and served immediately; still open for community voting. |
| Locked | green | Locked in as the active translation (admin override or vote threshold reached). |
Filters: Language, Component, Status, and an Include strings with no votes checkbox (hidden by default so the view focuses on strings with voting activity).
Columns: the English source is shown alongside the current translation. Click any column header to sort; click again to reverse direction.
Actions (per row, or in bulk by ticking the checkboxes and using the action bar below the table):
- Lock (Pending rows) — admin override that locks the string immediately without waiting for the vote threshold.
- Remove (Locked / Pushed rows) — reverts the string to Pending, resets the vote count to zero, deletes the accumulated votes, and stops serving it as the active translation.
Both locked and pushed strings are served immediately as the active translation without requiring an export step — the custom string manager intercepts get_string() and returns the stored value.
User Suggestions
Lists all pending alternative translations submitted by users. Each row shows the English source and the current translation alongside the suggestion and who submitted it. Actions are available per row or in bulk (tick the checkboxes and use the action bar):
- Approve — admin-locks the string immediately (locked status), resets the vote count to zero, and purges caches. Use when you are confident the suggestion is correct and want no further community input.
- Push to language pack — makes the suggestion live right now (pushed status) while keeping the string open for community voting. Votes reset to zero so users vote fresh on the new value. Once votes cross the threshold the string locks automatically. Use when you want the improvement served immediately but still want community validation.
- Reject — dismisses the suggestion without changing the active translation.
Each user has at most one pending suggestion per string; submitting again replaces it.
Text rules. Moodle prints language strings unescaped, including inside HTML attributes and inline JavaScript, so a translation must not contain characters that can break out of them. Suggestions are checked on the server (the browser's length limit is not relied on): straight quotes are converted to typographic ones (' → ’, " → “ ”), and angle brackets, backslashes, backticks and HTML character references (such as ") are refused, as is anything longer than 4096 characters. Suggestions stored by an older version that break these rules are marked Cannot be applied and are skipped by Approve/Push; the notification says how many rows were skipped.
Exporting language packs
Go to Site administration → Plugins → Local plugins → Language Crowdsourcing → Export Language Pack.
Select a language (or All languages to export every language in one archive), optionally filter by component — the components chosen in the Components to enable crowdsourcing for setting are pre-selected by default — choose whether to export locked strings only or all strings with translations, then click Download language pack. You receive a .zip file structured as a standard Moodle language pack:
{lang}/
moodle.php (core strings)
admin.php (core subsystems drop their core_ prefix)
forum.php (activity modules drop their mod_ prefix)
block_html.php (other plugin types keep their full name)
...
A value that could break out of an HTML attribute or a JavaScript string is exported only when it is identical to what the installed language pack already says; an installed pack is served by core without any of this plugin's checks.
The zip holds only the crowd-sourced strings, not complete language-pack files, so never copy it over an installed pack (that would replace each whole file). Two ways to use it:
- On another site, as a local override: copy each
{lang}/{file}.phpto$CFG->dataroot/lang/{lang}_local/{file}.php(defaultmoodledata/lang/ja_local/forum.php, for example), then purge caches. Moodle loads{lang}_localfiles over the installed pack (core_string_manager_standard), the same place Site administration → Language → Language customisation saves to — if that tool already has a file for the same component, merge the$stringentries instead of overwriting it. - Upstream: contribute the translations to the official language packs through AMOS.
Site administration → Language → Language packs only downloads packs from moodle.org; it cannot install this zip.
Capabilities
| Capability | Default role | Description |
|---|---|---|
local/langcrowd:vote |
Authenticated user | Cast approve/reject votes |
local/langcrowd:suggest |
Authenticated user | Submit alternative translations |
local/langcrowd:admin |
Manager | Access reports, approve suggestions and export. Carries RISK_CONFIG and RISK_XSS: approving a suggestion turns user-written text into a site-wide language string. |
Scheduled task
A task named Aggregate crowdsourced votes runs hourly to recalculate vote totals (so they heal after votes are deleted, e.g. by a privacy request) and apply the threshold lock. It can be triggered manually:
sudo -u www-data php admin/cli/scheduled_task.php \
--execute='\local_langcrowd\task\aggregate_votes'
Shipped language packs
The plugin ships with the English language pack (en) only. Other languages,
including Thai and Japanese, come from the Moodle language packs.
Architecture notes
-
Custom string manager (
classes/string_manager.php): extendscore_string_manager_standard. Interceptsget_string()to (a) serve promoted/locked translations from DB without requiring an export (in web and AJAX contexts), and (b) collect plain-text strings for the footer hook. Records parameterised strings by their rendered value and serves promoted templates with the placeholders filled in (a translation must keep exactly the source's placeholders). Filters out HTML-containing strings, strings with embedded newlines, strings shorter than 3 characters, and components excluded by the component filter. Component names are normalised (classes/local/components.php) so one string is one row however its caller spells the component, and a promoted value is served only if it passesclasses/local/text_safety.php— otherwise the lang pack's own value is served. -
Access gate (
classes/access.php): the single source of truth for the enabled / role / language / component checks, used by both the footer hook and the web services so the restrictions can't be bypassed by calling the services directly. -
Footer hook (
classes/hook_callbacks.php): injectswindow.langcrowdInitJSON and schedules thelocal_langcrowd/votingAMD call. Only runs when the user passes the access gate and holdslocal/langcrowd:vote. -
AMD voting module (
amd/src/voting.js): renders the opt-in "translate mode" toggle. On activation it calls theget_string_idsweb service, then a DOM TreeWalker scans text nodes and annotates matches. Strings inside<a>links are handled by wrapping the entire anchor element so buttons live outside it (avoids Moodle's capture-phase link navigation). A MutationObserver re-annotates content added by reactive frameworks. Activity names, form controls and fixed/sticky regions (navbars, drawers, footers) are excluded. The suggestion dialog usescore/modalso it inherits the site theme, dark mode and RTL. -
Manager (
classes/manager.php): centralises the admin state transitions (lock / revert / apply-suggestion / reject, plus bulk variants) so the report scripts stay thin and every "reset" also clears the underlying vote rows. -
Web services: three AJAX endpoints —
get_string_ids(register and look up strings),submit_vote(approve1, reject-1, withdraw0),submit_suggestion— all require login and the appropriate capability, and re-check the access gate.
Development
Running CI checks
cd /path/to/plugin/repo MOODLE_DIR=/path/to/moodle ../moodle-plugin-ci/bin/moodle-plugin-ci phplint ./ ../moodle-plugin-ci/bin/moodle-plugin-ci validate -m "$MOODLE_DIR" ./ ../moodle-plugin-ci/bin/moodle-plugin-ci codechecker ./ ../moodle-plugin-ci/bin/moodle-plugin-ci phpmd ./ ../moodle-plugin-ci/bin/moodle-plugin-ci savepoints ./
Running unit tests
cd "$MOODLE_DIR" sudo -u www-data php admin/cli/phpunit.php --init sudo -u www-data vendor/bin/phpunit --testsuite local_langcrowd
Security
- All admin actions are CSRF-protected via Moodle's
confirm_sesskey(). - All SQL uses parameterised queries via the Moodle DML API.
- All user-supplied content displayed in reports is escaped with
s(). - External functions validate parameters with Moodle type constants and require appropriate capabilities.
- Suggestion text is cleaned as
PARAM_TEXT(strips HTML tags) before storage. - Stored string values (
sourcevalueandcurrentvalue) are resolved server-side from the language packs — clients cannot submit string values. As defence in depth, the string manager never serves a promoted value containing HTML markup.
Documentation
End-user guides are provided in the docs/ folder:
- English —
docs/en/user-guide.md - ไทย (Thai) —
docs/th/user-guide.md - 日本語 (Japanese) —
docs/ja/user-guide.md
Developer/maintenance notes for this repository (review reports, UI/UX
recommendations) live outside the repo, in the workspace dev-docs/ tree.
Testing
cd /path/to/moodle
php public/admin/tool/phpunit/cli/init.php
vendor/bin/phpunit --testsuite local_langcrowd_testsuite
The plugin ships a full PHPUnit suite covering the exporter, vote thresholding and status transitions, the scheduled task, the web services, the participation gate, and the privacy provider.
License
GNU General Public License v3 or later — see LICENSE.