diff --git a/docs/extend/language-packs.md b/docs/extend/language-packs.md index 56be70d80..54e51478e 100644 --- a/docs/extend/language-packs.md +++ b/docs/extend/language-packs.md @@ -39,6 +39,16 @@ Translation files should go in the `locale` directory. Each file should be named The contents of the file should correspond to the extension's english translations, with the values translated in your language. See our [internationalization](i18n.md) docs for more information. +## Gambit Keywords + +Some extensions expose gambit keywords — the search tokens users type into the search bar, such as `is:following`, `author:admin`, or `country:US` — as translatable strings. These are typically found in the extension's locale file under keys ending in `_key`. + +Translating a gambit keyword lets users search in their own language. For example, translating `country.key` from `country` to `pays` means French users can type `pays:france` into the search bar. + +The English keyword always works as a fallback — users can type either the translated keyword or the English original and get the same result. You do not need to document this for your users; it happens automatically. + +See [Localizing gambit keywords](./search#localizing-gambit-keywords) in the search documentation for the full technical details, including how to identify which translation keys control gambit keywords and what to do if the English fallback stops working. + ## DayJS Translations Flarum use [the DayJS library](https://day.js.org/) to format and internationalize dates. diff --git a/docs/extend/search.md b/docs/extend/search.md index b78a1d40a..aaf1da707 100644 --- a/docs/extend/search.md +++ b/docs/extend/search.md @@ -365,6 +365,140 @@ So you want to make sure your gambit is added within the common frontend. ::: +### Localizing gambit keywords + +Gambit keywords are fully localizable. English always works as a fallback, regardless of the active locale — so `is:hidden` or `author:behz` will always be accepted even when the site language is French, Chinese, or anything else. + +#### How keyword localization works + +The `key()` method drives the keyword that appears in the search bar and autocomplete. You should always return a translated string from `app.translator.trans()`: + +```ts +key(): string { + return app.translator.trans('acme.lib.gambits.users.country.key', {}, true); +} +``` + +When the locale is English, `key()` returns `'country'` — and everything works as before. When the locale is French and a language pack translates `country.key` to `'pays'`, the gambit responds to `pays:france` in the search bar. + +#### The English fallback — `canonicalKey()` + +`GambitManager` builds a second alias pattern from `canonicalKey()`. If the translated pattern doesn't match a token in the search query, the canonical English pattern is tried instead. This means `country:france` continues to work even when the locale is French. + +By default `canonicalKey()` returns the same value as `key()`. For untranslated gambits (where `key()` already returns English) no override is needed — the alias path simply never fires. + +**If your gambit's `key()` returns a translated string, you must override `canonicalKey()` to return the hardcoded English keyword.** This is the only change required for localization support: + +```ts +import app from 'flarum/common/app'; +import { KeyValueGambit } from 'flarum/common/query/IGambit'; + +export default class CountryGambit extends KeyValueGambit { + key(): string { + // Translated — e.g. 'pays' in French, 'country' in English + return app.translator.trans('acme.lib.gambits.users.country.key', {}, true); + } + + // highlight-start + canonicalKey(): string { + // Hardcoded English — never translates, always accepted as an alias + return 'country'; + } + // highlight-end + + hint(): string { + return app.translator.trans('acme.lib.gambits.users.country.hint', {}, true); + } + + filterKey(): string { + return 'country'; + } +} +``` + +For `BooleanGambit`, the pattern is identical. If `key()` can return an array (for gambits like `SubscriptionGambit` that match multiple keywords), `canonicalKey()` must return the same shape — an array of hardcoded English keywords: + +```ts +import app from 'flarum/common/app'; +import { BooleanGambit } from 'flarum/common/query/IGambit'; + +export default class SubscriptionGambit extends BooleanGambit { + key(): string[] { + return [ + app.translator.trans('flarum-subscriptions.lib.gambits.discussions.subscription.following_key', {}, true), + app.translator.trans('flarum-subscriptions.lib.gambits.discussions.subscription.ignoring_key', {}, true), + ]; + } + + // highlight-start + canonicalKey(): string[] { + return ['following', 'ignoring']; + } + // highlight-end + + filterKey(): string { + return 'subscription'; + } +} +``` + +#### `toFilter()` must always produce canonical filter values + +When a gambit matches via the canonical alias rather than the translated keyword, the `matches` array still contains what was typed in the search bar (e.g. `'following'`). Your `toFilter()` implementation should map matched keywords to a stable, locale-independent filter value — never pass the raw matched string directly to the API if it might vary by locale: + +```ts +toFilter(matches: string[], negate: boolean): Record { + const filterKey = (negate ? '-' : '') + this.filterKey(); + + // Map both translated and canonical English keywords to the stable DB value. + const allFollowKeywords = [ + 'following', + 'followed', + app.translator.trans('flarum-subscriptions.lib.gambits.discussions.subscription.following_key', {}, true), + ]; + + const value = allFollowKeywords.includes(matches[1]) ? 'follow' : 'ignore'; + + return { [filterKey]: value }; +} +``` + +The backend filter (`SubscriptionFilter` in this case) should only expect the stable canonical values (`'follow'`, `'ignore'`), never locale-specific surface keywords. + +#### For language pack maintainers + +To translate gambit keywords, find the translation keys ending in `_key` in the extension's locale file. For example, a `locale/en.yml` may contain: + +```yaml +acme: + lib: + gambits: + users: + country: + key: country + hint: "country code (e.g. US, FR)" +``` + +Translate the `key` value to your language. The hint is shown in the autocomplete dropdown and should also be translated: + +```yaml +acme: + lib: + gambits: + users: + country: + key: pays + hint: "code pays (p. ex. US, FR)" +``` + +Once translated, users can search with `pays:france` in French. The English `country:france` will continue to work as a fallback for any user — translated and English keywords are always accepted simultaneously. + +:::info Extension authors must implement `canonicalKey()` + +The English fallback only works if the extension author has overridden `canonicalKey()`. If you translate a gambit keyword and the English form stops working, the extension has not yet been updated. File an issue with the extension to request `canonicalKey()` support. + +::: + ### Advanced gambits If neither of the above gambit classes are suitable for your needs, you may directly implement the `IGambit` interface. Your class must implement the following: diff --git a/docs/extend/update-2_0.md b/docs/extend/update-2_0.md index e08bd248b..9fc6cc1fd 100644 --- a/docs/extend/update-2_0.md +++ b/docs/extend/update-2_0.md @@ -190,6 +190,7 @@ There have been many changes to the core frontend codebase, including renamed or * A `Footer` component has been added that allows you to easily add content to the footer. * A `Form` component has been added to ensure consistent styling across forms. You should use this component in your extension if you are creating a form. * An API for frontend gambits has been introduced, [checkout the full documentation](./search#gambits). +* Gambit keywords are now **fully localizable**. The English keyword always works as a fallback regardless of the active locale. If your extension registers a gambit whose `key()` returns a translated string, you must now also override `canonicalKey()` to return the hardcoded English keyword — `GambitManager` uses this to build an alias pattern so both the translated and English forms are accepted. See [Localizing gambit keywords](./search#localizing-gambit-keywords) for details and examples. * A `FormGroup` component has been added that allows you to add any supported type of input similar to the admin panel's settings registration. [checkout the documentation for more details](./forms). * `WelcomeHero.prototype.viewItems` has been moved to `WelcomeHero.prototype.bodyItems`. * The frontend `Routes` extender has been modified to allow passing a custom route resolver class as the fourth argument when [adding routes](./routes#frontend-routes).