') + ')', 'gi');
if (regex.test(text)) {
found = true;
var frag = document.createDocumentFragment();
var parts = text.split(regex);
parts.forEach(function(part, i) {
if (i % 2 === 0) {
frag.appendChild(document.createTextNode(part));
} else {
var span = document.createElement('span');
span.className = 'userscript-highlight';
span.textContent = part;
frag.appendChild(span);
}
});
node.parentNode.replaceChild(frag, node);
}
});
} else if (node.nodeType === 1 && node.childNodes) { // element
var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];
if (!skipTags.includes(node.tagName)) {
Array.from(node.childNodes).forEach(highlight);
}
}
}
highlight(document.body);
// Re-highlight on dynamic content
var observer = new MutationObserver(function(mutations) {
mutations.forEach(function(m) {
m.addedNodes.forEach(function(node) {
if (node.nodeType === 1 || node.nodeType === 3) highlight(node);
});
});
});
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); }
})();
(function(){
try {
var __m = "*";
var __re = new RegExp('^' + ".*" + ', 'i');
if (__m === '*' || __re.test(location.href)) {
// Strip utm_, fbclid, gclid, etc. from all links on page
(function() {
var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',
'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',
'ref', 'ref_src', 'source', 'medium', 'campaign'];
function cleanUrl(url) {
try {
var u = new URL(url, window.location.origin);
var changed = false;
trackingParams.forEach(function(p) {
if (u.searchParams.has(p)) {
u.searchParams.delete(p);
changed = true;
}
});
return changed ? u.toString() : url;
} catch (e) {
return url;
}
}
function cleanLinks() {
document.querySelectorAll('a[href]').forEach(function(a) {
var clean = cleanUrl(a.href);
if (clean !== a.href) a.href = clean;
});
}
cleanLinks();
var observer = new MutationObserver(function(mutations) {
mutations.forEach(function(m) {
m.addedNodes.forEach(function(node) {
if (node.nodeType === 1) {
if (node.tagName === 'A') cleanLinks();
node.querySelectorAll('a[href]').forEach(function(a) {
var clean = cleanUrl(a.href);
if (clean !== a.href) a.href = clean;
});
}
});
});
});
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); }
})();
(function(){
try {
var __m = "youtube.com";
var __re = new RegExp('^' + "youtube\\.com" + ', 'i');
if (__m === '*' || __re.test(location.href)) {
// Auto-enable theater mode on YouTube
(function() {
function tryTheater() {
var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]');
if (btn && !btn.classList.contains('activated')) {
btn.click();
}
}
// Try immediately
tryTheater();
// Try after navigation (SPA)
var lastUrl = location.href;
setInterval(function() {
if (location.href !== lastUrl) {
lastUrl = location.href;
setTimeout(tryTheater, 500);
}
}, 1000);
// Also try on player load
var observer = new MutationObserver(tryTheater);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); }
})();
(function(){
try {
var __m = "*";
var __re = new RegExp('^' + ".*" + ', 'i');
if (__m === '*' || __re.test(location.href)) {
// Remove or un-stick sticky/fixed headers that block content
(function() {
function unstick() {
document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) {
if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {
el.style.position = 'static';
el.style.top = 'auto';
el.style.zIndex = 'auto';
}
});
}
unstick();
var observer = new MutationObserver(unstick);
observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });
})();
}
} catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); }
})();
})();
[2.x] feat(gambits): localization alias support — English keywords always work regardless of locale by imorland · Pull Request #4570 · flarum/framework · GitHub
You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Adds canonicalKey() and canonicalPattern() to BooleanGambit and KeyValueGambit. Both default to key(), so extensions that don't translate their keywords require no changes.
GambitManager.match() now tries the canonical English pattern as a second-pass alias when the translated pattern doesn't match — English keywords (is:hidden, author:behz, etc.) are always accepted regardless of the active locale.
Fixes SubscriptionFilter to accept only stable canonical values (follow/ignore) instead of raw locale strings leaking from the frontend. SubscriptionGambit.toFilter() now maps matched surface keywords to canonical values before sending them to the API.
Unrecognised subscription filter values now return an empty result set (WHERE 0 = 1) rather than crashing with a TypeError or returning all discussions.
Adds a Flarum\Subscriptions\Extend\Subscription extender so third-party extensions can register custom subscription types with their accepted filter aliases:
If your gambit's key() returns a translated string via app.translator.trans(), add a canonicalKey() override returning the hardcoded English keyword. That's the only change needed. See the companion docs PR flarum/docs#524 for full guidance.
Test plan
PHP: SubscriptionFilterTest — covers canonical values, surface aliases, negation, unrecognised values, and the Subscription extender (custom type registration + unregistered alias still returns empty)
JS: IGambit.test.ts — regression tests for all built-in gambit classes + TDD tests for canonicalKey() / canonicalPattern()
JS: GambitManager.test.ts — regression tests + localization alias describe block (translated vs canonical produce identical output, negation, unmatched keywords not consumed)
…ork regardless of locale
Adds `canonicalKey()` and `canonicalPattern()` to `BooleanGambit` and `KeyValueGambit`.
`GambitManager.match()` now tries the canonical English pattern as an alias when the
translated pattern doesn't match, so English keywords like `is:hidden` or `author:behz`
are always accepted even when the site locale has translated them.
Fixes `SubscriptionFilter` accepting raw locale strings from the frontend; it now
only accepts the stable canonical values `follow`/`ignore`. `SubscriptionGambit.toFilter()`
maps matched keywords to canonical values before sending them to the API.
Closes#4566
imorland
changed the title
feat(gambits): localization alias support — English keywords always work regardless of locale[2.x] feat(gambits): localization alias support — English keywords always work regardless of localeApr 14, 2026
…on types
Adds a Flarum\Subscriptions\Extend\Subscription extender so third-party
extensions can register custom subscription types (e.g. 'lurk') with their
accepted filter aliases:
(new Extend\Subscription())
->addSubscriptionType('lurk', ['lurk', 'lurking', 'lurked'])
SubscriptionFilter now resolves values through a container-bound registry
('flarum-subscriptions.subscription_types') rather than a hardcoded match
expression. The built-in 'follow' and 'ignore' types are seeded by the
subscriptions extension's own extend.php via the same extender.
…tures
- Add canonicalPattern() to the IGambit object literal in GambitsAutocomplete.tsx
so it satisfies the updated interface (TS2345)
- Add __esModule: true to the nanoid Jest mock so Jest's CJS/ESM interop resolves
the named export correctly
- Move lurk discussion/post/discussion_user fixtures into setUp() so the
subscription_extender_registers_custom_type test can find them
Jest runs with --experimental-vm-modules which does real ESM linking, so CJS
mock files cannot satisfy named ESM imports. Replace emptyModule.js and
nanoid.js with proper .mjs equivalents using ESM export syntax.
Fix SubscriptionFilterTest: seed the 'lurk-user' fixture under user 3 (not
user 2) to avoid polluting the negation tests' result sets; switch negation
assertions from assertEqualsCanonicalizing to assertContains/assertNotContains
so they remain correct regardless of how many discussions exist.
Replace the lurk custom-type test (which required seeding an invalid enum value)
with a test that registers an additional alias for the existing 'follow' type,
which is a valid use of the extender and exercises the same code path.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Resolves#4566.
canonicalKey()andcanonicalPattern()toBooleanGambitandKeyValueGambit. Both default tokey(), so extensions that don't translate their keywords require no changes.GambitManager.match()now tries the canonical English pattern as a second-pass alias when the translated pattern doesn't match — English keywords (is:hidden,author:behz, etc.) are always accepted regardless of the active locale.SubscriptionFilterto accept only stable canonical values (follow/ignore) instead of raw locale strings leaking from the frontend.SubscriptionGambit.toFilter()now maps matched surface keywords to canonical values before sending them to the API.subscriptionfilter values now return an empty result set (WHERE 0 = 1) rather than crashing with aTypeErroror returning all discussions.Flarum\Subscriptions\Extend\Subscriptionextender so third-party extensions can register custom subscription types with their accepted filter aliases:Extension authors
If your gambit's
key()returns a translated string viaapp.translator.trans(), add acanonicalKey()override returning the hardcoded English keyword. That's the only change needed. See the companion docs PR flarum/docs#524 for full guidance.Test plan
SubscriptionFilterTest— covers canonical values, surface aliases, negation, unrecognised values, and theSubscriptionextender (custom type registration + unregistered alias still returns empty)IGambit.test.ts— regression tests for all built-in gambit classes + TDD tests forcanonicalKey()/canonicalPattern()GambitManager.test.ts— regression tests + localization alias describe block (translated vs canonical produce identical output, negation, unmatched keywords not consumed)