Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions docs/extend/language-packs.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
134 changes: 134 additions & 0 deletions docs/extend/search.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, any> {
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:
Expand Down
1 change: 1 addition & 0 deletions docs/extend/update-2_0.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down