diff --git a/docs/.vuepress/config/locales/en/sidebar.js b/docs/.vuepress/config/locales/en/sidebar.js
index 69064ca42..517fb256a 100644
--- a/docs/.vuepress/config/locales/en/sidebar.js
+++ b/docs/.vuepress/config/locales/en/sidebar.js
@@ -23,6 +23,7 @@ module.exports = {
'frontend-pages',
'interactive-components',
'i18n',
+ 'language-packs',
'forms',
'permissions',
'settings',
@@ -35,9 +36,11 @@ module.exports = {
collapsable: false,
children: [
'api-throttling',
+ 'assets',
'console',
'extending-extensions',
'extensibility',
+ 'filesystem',
'formatting',
'mail',
'middleware',
diff --git a/docs/console.md b/docs/console.md
index cda361b45..adb3b4521 100644
--- a/docs/console.md
+++ b/docs/console.md
@@ -38,6 +38,12 @@ Gather information about Flarum's core and installed extensions. This is very us
Clears the backend flarum cache, including generated js/css, text formatter cache, and cached translations. This should be run after installing or removing extensions, and running this should be the first step when issues occur.
+### assets:publish
+
+`php flarum assets:publish`
+
+Publish assets from core and extensions (e.g. compiled JS/CSS, bootstrap icons, logos, etc). This is useful if your assets have become corrupted, or if you have switched [filesystem drivers](extend/filesystem.md) for the `flarum-assets` disk.
+
### migrate
`php flarum migrate`
@@ -49,3 +55,23 @@ Runs all outstanding migrations. This should be used when an extension that modi
`php flarum migrate:reset --extension [extension_id]`
Reset all migrations for an extension. This is mostly used by extension developers, but on occasion, you might need to run this if you are removing an extension, and want to clear all of its data from the database. Please note that the extension in question must currently be installed (but not necessarily enabled) for this to work.
+
+### schedule:run
+
+`php flarum schedule:run`
+
+Many extensions use scheduled jobs to run tasks on a regular interval. This could include database cleanups, posting scheduled drafts, generating sitemaps, etc. If any of your extensions use scheduled jobs, you should add a [cron job](https://ostechnix.com/a-beginners-guide-to-cron-jobs/) to run this command on a regular interval:
+
+```
+* * * * * cd /path-to-your-flarum-install && php flarum schedule:run >> /dev/null 2>&1
+```
+
+This command should generally not be run manually.
+
+Note that some hosts do not allow you to edit cron configuration directly. In this case, you should consult your host for more information on how to schedule cron jobs.
+
+### schedule:list
+
+`php flarum schedule:list`
+
+This command returns a list of scheduled commands (see `schedule:run` for more information). This is useful for confirming that commands provided by your extensions are registered properly. This **can not** check that cron jobs have been scheduled successfully, or are being run.
\ No newline at end of file
diff --git a/docs/extend/admin.md b/docs/extend/admin.md
index 39585b09a..dd0bbfad1 100644
--- a/docs/extend/admin.md
+++ b/docs/extend/admin.md
@@ -53,6 +53,7 @@ app.initializers.add('interstellar', function(app) {
{
setting: 'acme-interstellar.coordinates', // This is the key the settings will be saved under in the settings table in the database.
label: app.translator.trans('acme-interstellar.admin.coordinates_label'), // The label to be shown letting the admin know what the setting does.
+ help: app.translator.trans('acme-interstellar.admin.coordinates_help'), // Optional help text where a longer explanation of the setting can go.
type: 'boolean', // What type of setting this is, valid options are: boolean, text (or any other tag type), and select.
},
30 // Optional: Priority
@@ -75,6 +76,17 @@ If you use `type: 'select'` the setting object looks a little bit different:
}
```
+Also, note that additional items in the setting object will be used as component attrs. This can be used for placeholders, min/max restrictions, etc:
+
+```js
+{
+ setting: 'acme-interstellar.crew_count',
+ label: app.translator.trans('acme-interstellar.admin.crew_count_label'),
+ type: 'number',
+ min: 1,
+ max: 10
+}
+```
If you want to add something to the settings like some extra text or a more complicated input, you can also pass a callback as the first argument that returns JSX. This callback will be executed in the context of [`ExtensionPage`](https://api.docs.flarum.org/js/master/class/src/admin/components/extensionpage.js~extensionpage) and setting values will not be automatically serialized.
@@ -124,6 +136,7 @@ app.initializers.add('interstellar', function(app) {
icon: 'fas fa-rocket', // Font-Awesome Icon
label: app.translator.trans('acme-interstellar.admin.permissions.fly_rockets_label'), // Permission Label
permission: 'discussion.rocket_fly', // Actual permission name stored in database (and used when checking permission).
+ tagScoped: true, // Whether it be possible to apply this permission on tags, not just globally. Explained in the next paragraph.
},
'start', // Category permission will be added to on the grid
95 // Optional: Priority
@@ -131,6 +144,10 @@ app.initializers.add('interstellar', function(app) {
});
```
+If your extension interacts with the [tags extension](https://github.com/flarum/tags) (which is fairly common), you might want a permission to be tag scopable (i.e. applied on the tag level, not just globally). You can do this by including a `tagScoped` attribute, as seen above. Permissions starting with `discussion.` will automatically be tag scoped unless `tagScoped: false` is indicated.
+
+To learn more about Flarum permissions, see [the relevant docs](permissions.md).
+
### Chaining Reminder
Remember these functions can all be chained like:
diff --git a/docs/extend/assets.md b/docs/extend/assets.md
new file mode 100644
index 000000000..2fd7dedd2
--- /dev/null
+++ b/docs/extend/assets.md
@@ -0,0 +1,8 @@
+# Extension Assets
+
+Some extensions might want to include assets like images or JSON files in their source code (note that this is not the same as uploads, which would probably require a [filesystem disk](filesystem.md)).
+
+This is actually very easy to do. Just create an `assets` folder at the root of your extension, and place any asset files there.
+Flarum will then automatically copy those files to its own `assets` directory (or other storage location if [one is offered by extensions](filesystem.md)) every time the extension is enabled or [`php flarum assets:publish`](../console.md) is executed.
+
+If using the default storage driver, assets will be available at `https://FORUM_URL/assets/extensions/EXTENSION_ID/file.path`. However, since other extensions might use remote filesystems, we recommend serializing the url to assets you need in the backend. See [Flarum's serialization of the logo and favicon URLs](https://github.com/flarum/core/blob/bba6485effc088e38e9ae0bc8f25528ecbee3a7b/src/Api/Serializer/ForumSerializer.php#L85-L86) for an example.
\ No newline at end of file
diff --git a/docs/extend/console.md b/docs/extend/console.md
index e2f363ea3..158f05fd0 100644
--- a/docs/extend/console.md
+++ b/docs/extend/console.md
@@ -39,8 +39,23 @@ return [
];
```
-::: tip Scheduled Commands
+## Scheduled Commands
-The [fof/console library](https://github.com/FriendsOfFlarum/console) allows you to schedule commands to run on a regular interval! However, please note that this is a community solution.
+The `Flarum\Extend\Console`'s `schedule` method allows extension developers to create scheduled commands that run on an interval:
-:::
+
+```php
+use Flarum\Extend;
+use YourNamespace\Console\CustomCommand;
+use Illuminate\Console\Scheduling\Event;
+
+return [
+ // Other extenders
+ (new Extend\Console())->schedule('cache:clear', function (Event $event) {
+ $event->everyMinute();
+ }, ['Arg1', '--option1', '--option2']),
+ // Other extenders
+];
+```
+
+In the callback provided as the second argument, you can call methods on the [$event object](https://laravel.com/api/8.x/Illuminate/Console/Scheduling/Event.html) to schedule on a variety of frequencies (or apply other options, such as only running on one server). See the [Laravel documentation](https://laravel.com/docs/8.x/scheduling#scheduling-artisan-commands) for more information.
\ No newline at end of file
diff --git a/docs/extend/filesystem.md b/docs/extend/filesystem.md
new file mode 100644
index 000000000..14159aa8c
--- /dev/null
+++ b/docs/extend/filesystem.md
@@ -0,0 +1,132 @@
+# Filesystem
+
+Flarum core integrates with the filesystem to store and serve assets (like compiled JS/CSS or upload logos/favicons) and avatars.
+
+Extensions can use Flarum's provided utils for their own filesystem interaction and file storage needs. This system is based around [Laravel's filesystem tools](https://laravel.com/docs/8.x/filesystem), which are in turn based on the [Flysystem library](https://github.com/thephpleague/flysystem).
+
+## Disks
+
+Filesystem **disks** represent storage locations, and are backed by storage drivers, which we'll cover later.
+Flarum core has 2 disks: `flarum-assets` and `flarum-avatars`.
+
+### Using existing disks
+
+To access a disk, you'll need to retrieve it from the [Filesystem Factory](https://laravel.com/api/8.x/Illuminate/Contracts/Filesystem/Factory.html).
+To do so, you should inject the factory contract in your class, and access the disks you need.
+
+Let's take a look at core's [`DeleteLogoController`](https://github.com/flarum/core/blob/bba6485effc088e38e9ae0bc8f25528ecbee3a7b/src/Api/Controller/DeleteLogoController.php#L19-L59) for an example:
+
+```php
+settings = $settings;
+ $this->uploadDir = $filesystemFactory->disk('flarum-assets');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function delete(ServerRequestInterface $request)
+ {
+ RequestUtil::getActor($request)->assertAdmin();
+
+ $path = $this->settings->get('logo_path');
+
+ $this->settings->set('logo_path', null);
+
+ if ($this->uploadDir->exists($path)) {
+ $this->uploadDir->delete($path);
+ }
+
+ return new EmptyResponse(204);
+ }
+}
+```
+
+The object returned by `$filesystemFactory->disk(DISK_NAME)` implements the [Illuminate\Contracts\Filesystem\Cloud](https://laravel.com/api/8.x/Illuminate/Contracts/Filesystem/Cloud.html) interface, and can be used to create/get/move/delete files, and to get the URL to a resource.
+
+### Declaring new disks
+
+Some extensions will want to group their resources / uploads onto a custom disk as opposed to using `flarum-assets` or `flarum-avatars`.
+
+This can be done via the `Filesystem` extender:
+
+```php
+use Flarum\Extend;
+
+return [
+ (new Extend\Filesystem)
+ ->disk('flarum-uploads', function (Paths $paths, UrlGenerator $url) {
+ return [
+ 'root' => "$paths->public/assets/uploads",
+ 'url' => $url->to('forum')->path('assets/uploads')
+ ];
+ });
+```
+
+Since all disks use the local filesystem by default, you'll need to provide a base path and base URL for the local filesystem.
+
+The config array can contain other entries supported by [Laravel disk config arrays](https://laravel.com/docs/8.x/filesystem#configuration). The `driver` key should not be provided, and will be ignored.
+
+## Storage drivers
+
+Flarum selects the active driver for each disk by checking the `disk_driver.DISK_NAME` key in the [settings repository](settings.md) and [config.php file](../config.md). If no driver is configured, or the configured driver is unavailable, Flarum will default to the `local` driver.
+
+You can define new storage drivers by implementing the [`Flarum\Filesystem\DriverInterface` interface](https://github.com/flarum/core/blob/bba6485effc088e38e9ae0bc8f25528ecbee3a7b/src/Filesystem/DriverInterface.php#L16-L16), and registering it via the `Filesystem` extender:
+
+```php
+use Flarum\Extend;
+
+return [
+ (new Extend\Filesystem)
+ ->driver('aws-with-cdn', AwsWithCdnDriver::class);
+```
+
+Filesystem storage drivers are a very powerful tool that allows you to completely customize file storage locations, attach arbitrary CDNs, and otherwise extend the filesystem / cloud storage integration layer.
+
+::: danger
+
+Some drivers might try to index their filesystem every time the driver is instantiated, even if only the `url` method is needed. This can have serious performance implications. In most cases, you'll want to ensure that your driver's `url` method does not ping the remote filesystem. Similarly, the remote filesystem should usually not be accessed until operations are actually executed.
+
+:::
+
+## GUI and Admin Configuration
+
+Flarum does not currently provide a GUI for selecting drivers for disks, or for entering settings for drivers. This might be added in the future.
+For now, extensions are responsible for providing a GUI for their disks and drivers.
+
+As noted [above](#storage-drivers), if your extension provides a GUI for selecting drivers for a disk, it should modify the `disk_driver.DISK_NAME` key in settings.
diff --git a/docs/extend/frontend.md b/docs/extend/frontend.md
index 7bf3ec7e5..f7c472d82 100644
--- a/docs/extend/frontend.md
+++ b/docs/extend/frontend.md
@@ -248,7 +248,7 @@ You should familiarize yourself with [Mithril's component API](https://mithril.j
* `CustomComponentClass.component(attrs, children)`
* `{children}`
-However, component classes extending `Component` must call `super` when using the `oninit`, `oncreate`, and `onbeforeupdate` methods.
+However, component classes extending `Component` must call `super` when using the lifecycle methods (`oninit`, `oncreate`, `onbeforeupdate`, `onupdate`, `onbeforeremove`, and `onremove`).
To use Flarum components, simply extend `flarum/common/Component` in your custom component class.
@@ -385,4 +385,15 @@ Some potential "advanced" uses include:
### Flarum Utils
-Flarum defines (and provides) quite a few util and helper functions, which you may want to use in your extensions. The best way to learn about them is through [the source code](https://github.com/flarum/core/tree/master/js) or [our javascript API documentation](https://api.docs.flarum.org/js/).
+Flarum defines (and provides) quite a few util and helper functions, which you may want to use in your extensions. A few particularly useful ones:
+
+- `flarum/common/utils/Stream` provides [Mithril Streams](https://mithril.js.org/stream.html), and is useful in [forms](forms.md).
+- `flarum/common/utils/classList` provides the [clsx library](https://www.npmjs.com/package/clsx), which is great for dynamically assembling a list of CSS classes for your components
+- `flarum/common/utils/extractText` extracts text as a string from Mithril component vnode instances (or translation vnodes).
+- `flarum/common/utils/throttleDebounce` provides the [throttle-debounce](https://www.npmjs.com/package/throttle-debounce) library
+- `flarum/common/helpers/avatar` displays a user's avatar
+- `flarum/common/helpers/highlight` highlights text in strings: great for search results!
+- `flarum/common/helpers/icon` displays an icon, usually used for FontAwesome.
+- `flarum/common/helpers/username` shows a user's display name, or "deleted" text if the user has been deleted.
+
+And there's a bunch more! Some are covered elsewhere in the docs, but the best way to learn about them is through [the source code](https://github.com/flarum/core/tree/master/js) or [our javascript API documentation](https://api.docs.flarum.org/js/).
diff --git a/docs/extend/i18n.md b/docs/extend/i18n.md
index c926d705e..5606f90ca 100644
--- a/docs/extend/i18n.md
+++ b/docs/extend/i18n.md
@@ -1,6 +1,6 @@
# Internationalization
-Flarum features a powerful translation system (based on [Symfony's translator](https://symfony.com/doc/3.3/translation.html)) that allows the interface to display information in virtually any language. You should consider taking advantage of Flarum's translator as you develop your extension, even if you have no intention of using it in more than a single language.
+Flarum features a powerful translation system (based on [Symfony's translator](https://symfony.com/doc/5.2/translation.html) and [ICU MessageFormat](https://symfony.com/doc/5.2/translation/message_format.html)) that allows the interface to display information in virtually any language. You should consider taking advantage of Flarum's translator as you develop your extension, even if you have no intention of using it in more than a single language.
For one thing, this system will allow you to change the information displayed by your extension without editing the actual code. It will also give you the tools needed to efficiently deal with phenomena such as pluralization and agreement for gender. And you never know: it may come in handy later if you decide you want to add more languages and make your extension available to users around the world!
@@ -10,7 +10,13 @@ We will also describe the [standard format](#appendix-a:-standard-key-format) to
## How Flarum Translates
-Unlike Flarum's bundled extensions — which are translated using resources added by [language packs](/languages.md) — a third-party extension has to supply all of its own translations. As a Flarum developer, it will be up to you to obtain and maintain the resources for every language you want your extension to support.
+In your code, instead of hardcoding strings in a language, you will call the `trans` method on a translator object, providing a unique translation key and optionally some variables.
+Then, when users visit the page, Flarum will look in its registered translation files for a matching translation.
+
+Flarum pulls translations from several sources:
+
+- Locale files included in extension source code.
+- [Language packs](language-packs.md), which include translations for multiple extensions.
As a rule, a Flarum site can only display translations corresponding to the language packs that have been installed on that site. But Flarum will do its level best — within this limitation — to render your extension's output in some sort of user-readable language:
@@ -19,7 +25,7 @@ As a rule, a Flarum site can only display translations corresponding to the lang
3. As a last-ditch effort, it will look for a "generic" English translation of the output.
4. If none of the above are available, it will give up and display a translation key.
-Since English translations could be the only thing standing between forum users and unsightly translation keys, we strongly recommend including a complete set of English resources with every extension. (You will also need to include English resources if you wish to list your extension on the Flarum Marketplace.)
+Since English translations could be the only thing standing between forum users and unsightly translation keys, we strongly recommend including a complete set of English resources with every extension. (You will also need to include English resources if you wish to list your extension on the Flarum Marketplace, when that launches). Generally, extension developers will include English translations in their extension, and other language translations in a [language pack](language-packs.md).
## Locale File
@@ -41,7 +47,7 @@ Each translation in the locale file must be preceded by a key, which you will us
sample_key: This is a sample translation.
```
-You can also use keys to **namespace** your translations. For starters, the first line of the locale file should consist of a single key that collects all the translations for your extension in a single namespace. This key should exactly match the name of the folder where your extension lives — `kebab-case` and all.
+You can also use keys to **namespace** your translations. For starters, the first line of the locale file should consist of a single key that collects all the translations for your extension in a single namespace. This key should exactly match your [extension's ID](admin.md#telling-the-api-about-your-extension) — `kebab-case` and all.
Additional keys can be used to divide the extension namespace into groups. This comes in handy when you want to organize your translations according to where they appear in the user interface, for example. These intermediate namespacing keys should always be in `snake_case`.
@@ -172,28 +178,29 @@ Of course, you can give a parameter any name you like — you could use `trans` like you'd use `app.translator.trans` in JavaScript.
-You can learn more about the Translator's methods in [Symfony's `Translator` documentation](https://github.com/symfony/symfony/blob/3.3/src/Symfony/Component/Translation/Translator.php), which Flarum's `Translator` extends.
+You can learn more about the Translator's methods in [Symfony's `Translator` documentation](https://symfony.com/doc/5.2/translation/message_format.html), which Flarum's `Translator` extends.
## Registering Locales
@@ -270,7 +272,7 @@ These two keys don't correspond to interfaces; they're for translations that req
The keys in this level are not so rigidly defined. Their main purpose is to chop the UI up into manageable chunks, so localizers can find the translations and see for themselves how they are used by the software. (Third-level keys aren't used in the `ref` and `group` namespaces, which don't need chopping.)
-If you're modifying an existing location — to add a new setting to the Settings page, for example — you should copy our namespacing so experienced Flarum localizers will know at a glance exactly where the new translations are displayed. See the [English locale files](https://github.com/flarum/lang-english/tree/master/locale) for the details of our namespacing scheme.
+If you're modifying an existing location — to add a new setting to the Settings page, for example — you should copy our namespacing so experienced Flarum localizers will know at a glance exactly where the new translations are displayed. See English translations in `flarum/core` and bundled extensions for examples.
If your extension adds a new location — such as a new dialog box — you should feel free to create a new third-level key to namespace the translations that go there. Take a couple minutes to familiarize yourself with the namespacing in the locale files linked above, then create a new key that fits in with that scheme.
@@ -453,7 +455,7 @@ Now look at this similar set of sentences, output by similar code in the [Mentio
In English, the simple past tense is not affected by pluralization. Since the verb phrase is always the same, it would be fairly easy to ignore the plurals and use the `app.translator.trans()` method to produce the single necessary translation. Of course, that simply wouldn't work for the French localizer, who needs to inflect the verb differently in each of the above three sentences.
-This is the sort of situation where the humdrum chore of language abstraction requires a bit of extra care and attention. Remember to ask yourself whether each noun (or pronoun) can be pluralized. If it can, then be sure to use the `app.translator.transChoice()` method and pass an appropriate variable to the translator. Of course, you don't need to provide any variant translations in the English resources …
+This is the sort of situation where the humdrum chore of language abstraction requires a bit of extra care and attention. Remember to ask yourself whether each noun (or pronoun) can be pluralized. If it can, make sure to pass an appropriate variable to the translator. Of course, you don't need to provide any variant translations in the English resources …
```yaml
mentioned_by_text: "{users} replied to this." # Can be pluralized ...
diff --git a/docs/extend/language-packs.md b/docs/extend/language-packs.md
new file mode 100644
index 000000000..e6007fb6f
--- /dev/null
+++ b/docs/extend/language-packs.md
@@ -0,0 +1,59 @@
+# Translating Flarum
+
+::: tip
+
+To make this process easier, we **strongly** recommend using the community-led [weblate](https://discuss.flarum.org/d/20807-simplify-translation-process-with-weblate) initiative to contribute to existing language packs, or create new ones.
+This automates checking for changes in the extension's used translations.
+
+See [this GitHub page](https://github.com/rob006-software/flarum-lang-template/wiki) for a guide to getting started.
+
+:::
+
+## Declaring a Language Pack
+
+Language packs should be their own extensions, with no other code / logic included.
+
+The [`LanguagePack` extender](https://github.com/flarum/core/blob/master/src/Extend/LanguagePack.php) allows you to define that your extension is a language pack.
+
+This extender has no setters. All you have to do is instantiate it, make sure you language pack is in the `locale` folder, and you're done!
+
+Here's a quick example from [Flarum English](https://github.com/flarum/lang-english/blob/master/extend.php):
+
+```php
+can` that don't use the full name of a permission; for example, `$actor->can('reply', $discussion)`, where the backing permission is actually called `discussion.reply`.
+
+This is done in core to make authorization calls shorter and simpler. Essentially, if the second argument is a discussion, Core's [DiscussionPolicy](https://github.com/flarum/core/blob/bba6485effc088e38e9ae0bc8f25528ecbee3a7b/src/Discussion/Access/DiscussionPolicy.php#L39-L44) will check the `discussion.PROVIDED_ABILITY` permission automatically.
+
+This can be used by extensions when a model namespace isn't present: for example, `$actor->can('someAbility, $discussion)` will check the `discussion.someAbility` permission if the `$discussion` argument is an instance of the `Discussion` model. However, this means you can't prefix your permissions with extension namespaces (or you have to put the extension namespace at the end).
+
+These magic model-based conversions are applied to discussion, group, and user authorization checks. For posts, the logic is slightly different: `$actor->can('ability', $post)` will check `$actor->('abilityPosts, $post->discusssion)` on the post's discussion.
+
+If you want to use authorization checks with an ability name that differs from the backing permission name, and these cases do not apply to your permission's naming, you'll have to use a custom policy.
+
+See our [authorization documentation](authorization.md) for more information on the `can` method, policies, and how authorization checks are processed.
+
+## Adding Custom Permissions
To learn more about adding permissions through the admin dashboard, see the [relevant documentation](admin.md).
diff --git a/docs/extend/service-provider.md b/docs/extend/service-provider.md
index ff09a6a61..6399c29c1 100644
--- a/docs/extend/service-provider.md
+++ b/docs/extend/service-provider.md
@@ -29,22 +29,28 @@ A custom service provider should extend `Flarum\Foundation\AbstractServiceProvid
container->resolving(SomeClass::class, function ($container) {
+ return new SomeClass($container->make('some.binding'));
+ })
}
- public function boot()
+ public function boot(Container $container)
{
// custom logic here
}
}
```
-The `register` method will run during step (3) above, and the `boot` method will run during step (5) above. In both methods, the container is available via `$this->app`.
+The `register` method will run during step (3) above, and the `boot` method will run during step (5) above. In the `register` method, the container is available via `$this->container`. In the `boot` method, the container (or any other arguments), should be injected via typehinted method arguments.
+
+Flarum does not currently support Laravel Octane, but some [best practices](https://laravel.com/docs/8.x/octane#dependency-injection-and-octane), like using the `$container` argument inside `bind`, `singleton`, and `resolving` callbacks instead of `$this->container` should be used. See the [Octane documentation](https://laravel.com/docs/8.x/octane#dependency-injection-and-octane) for more information.
To actually register your custom service provider, you can use the `ServiceProvider` extender in `extend.php`:
diff --git a/docs/extend/testing.md b/docs/extend/testing.md
index b870672b3..6892f4a1c 100644
--- a/docs/extend/testing.md
+++ b/docs/extend/testing.md
@@ -146,9 +146,10 @@ Your testcase classes should extend this class.
There are several important utilities available for your test cases:
-- The `setting()` method allows you to override settings before the app has booted. This is useful if your boot process has logic depending on settings (e.g. which driver to use for some system).
-- The `extension()` method will take Flarum IDs of extensions to enable as arguments. Your extension should always call this with your extension's ID at the start of test cases, unless the goal of the test case in question is to confirm some behavior present without your extension, and compare that to behavior when your extension is enabled. If your extension is dependent on other extensions, make sure they are included in the composer.json `require` field (or `require-dev` for [optional dependencies](dependencies.md)), and also list their composer package names when calling `extension()`. Note that you must list them in a valid order.
-- The `extend()` method takes instances of extenders as arguments, and is useful for testing extenders introduced by your extension for other extensions to use.
+- The `setting($key, $value)` method allows you to override settings before the app has booted. This is useful if your boot process has logic depending on settings (e.g. which driver to use for some system).
+- Similarly, the `config($key, $value)` method allows you to override config.php values before the app has booted. You can use dot-delimited keys to set deep-nested values in the config array.
+- The `extension($extensionId)` method will take Flarum IDs of extensions to enable as arguments. Your extension should always call this with your extension's ID at the start of test cases, unless the goal of the test case in question is to confirm some behavior present without your extension, and compare that to behavior when your extension is enabled. If your extension is dependent on other extensions, make sure they are included in the composer.json `require` field (or `require-dev` for [optional dependencies](dependencies.md)), and also list their composer package names when calling `extension()`. Note that you must list them in a valid order.
+- The `extend($extender)` method takes instances of extenders as arguments, and is useful for testing extenders introduced by your extension for other extensions to use.
- The `prepareDatabase()` method allow you to pre-populate your database. This could include adding users, discussions, posts, configuring permissions, etc. Its argument is an associative array that maps table names to arrays of [record arrays](https://laravel.com/docs/8.x/queries#insert-statements).
If your test case needs users beyond the default admin user, you can use the `$this->normalUser()` method of the `Flarum\Testing\integration\RetrievesAuthorizedUsers` trait.
diff --git a/docs/extend/translate.md b/docs/extend/translate.md
deleted file mode 100644
index 9f551027c..000000000
--- a/docs/extend/translate.md
+++ /dev/null
@@ -1,39 +0,0 @@
-# Translating Flarum
-
-### LanguagePack
-
-As simplest extender, the [`LanguagePack` extender](https://github.com/flarum/core/blob/master/src/Extend/LanguagePack.php) allows you to define that your extension is a language pack.
-
-This extender has no setters. All you have to do is instantiate it, make sure you language pack is in the `locale` folder, and you're done!
-
-Here's a quick example from [Flarum English](https://github.com/flarum/lang-english/blob/master/extend.php):
-
-```php
-translator->trans('some.translation', ['{username}' => 'Some Username!'])`).
+- There's no support for complex applications (nested pluralization, non-number-based selection)
+- As a result of the previous point, genderization is impossible. And that's kinda important for a lot of languages.
+
+### New System
+
+In v5, Symfony dropped their proprietary `transChoice` system in favor of the more-or-less standard [ICU MessageFormat](https://symfony.com/doc/5.2/translation/message_format.html). This solves pretty much every single one of the aforementioned issues. In this release, Flarum will be fully switching to ICU MessageFormat as well. What does this mean for extensions?
+
+- `transChoice` should not be used at all; instead, the variable passed for pluralization should be included in the data.
+- Keys for backend translations no longer need to be surrounded by curly braces.
+- Translations can now use the [`select` and `plural`](https://symfony.com/doc/5.2/translation/message_format.html) formatter syntaxes. For the `plural` formatter, the `offset` parameter and `#` magic variables are supported.
+- These `select` and `plural` syntaxes can be nested to arbitrary depth. This is often a bad idea though (beyond, say, 2 levels), as things can get unnecessarily complex.
+
+No change to translation file naming is necessary (Symfony docs say that an `+intl-icu` suffix is necessary, but Flarum will now interpret all translation files as internationalized).
+
+#### Future Changes
+
+In the future, this will serve as a basis for additional features:
+
+- Translator preprocessors will allow extensions to modify arguments passed to translations. This will enable genderization (extensions could automatically extract a gender field from any objects of type "user" passed in).
+- We could support internationalized "magic variables" for numbers: currently, `one` is supported, but others (`few`, `many`, etc) currently aren't.
+- We could support ordinal formatting in additional to just plural formatting.
+
+#### Changes Needed in Extensions
+
+The `transChoice` methods in the frontend and backend have been removed.
+The `trans` method should always be used for translating, regardless of pluralization.
+If a translation requires pluralization, make sure you pass in the controlling variable as one of the arguments.
+
+In the frontend, code that looked like this:
+
+```js
+app.translator.transChoice('some-translation', guestCount, {host: hostName});
+```
+
+should be changed to:
+
+```js
+// This uses ES6 key-property shorthand notation. {guestCount: guestCount} is equivalent to {guestCount}
+app.translator.trans('some-translation', {host: hostName, guestCount });
+```
+
+Similarly, in the backend,
+
+```php
+$translator->transChoice('some-translation', $guestCount, ['{host}' => $hostName]);
+```
+
+should be changed to:
+
+```php
+$translator->trans('some-translation', ['host' => $hostName, 'guestCount' => $guestCount]);
+```
+
+Note that in the backend, translation keys were previously wrapped in curly braces.
+This is no longer needed.
+
+#### Changes Needed in Translations
+
+Translations that aren't using pluralization don't need any changes.
+
+Pluralized translations should be changed as follows:
+
+`For {count} minute|For {count} minutes`
+
+to
+
+`{count, plural, one {For # minute} other {For # minutes}}`
+
+Note that in this example, `count` is the variable that controls pluralization. If a different variable were used (such as guestCount in the example above), this would look like:
+
+`{guestCount, plural, one {For # minute} other {For # minutes}}`
+
+See [our i18n docs](i18n.md) for more information.
+
+### Permissions Changes
+
+For a long time, the `viewDiscussions` and `viewUserList` permissions have been confusing. Despite their names:
+
+- `viewDiscussions` controls viewing both discussions and users.
+- `viewUserList` controls searching users, not viewing user profiles.
+
+To clear this up, in v1.0, these permissions have been renamed to `viewForum` and `searchUsers` respectively.
+A migration in core will automatically adjust all permissions in the database to use the new naming. However, any extension code using the old name must switch to the new ones immediately to avoid security issues. To help the transfer, a warning will be thrown if the old permissions are referenced.
+
+We have also slightly improved tag scoping for permissions. Currently, permissions can be applied to tags if:
+
+- The permission is `viewForum`
+- The permission is `startDiscussion`
+- The permission starts with `discussion.`
+
+However, this doesn't work for namespaced permissions (`flarum-acme.discussion.bookmark`), or permissions that don't really have anything to do with discussions, but should still be scoped (e.g. `viewTag`). To counter this, a `tagScoped` attribute can be used on the object passed to [`registerPermission`](admin.md) to explicitly indicate whether the permission should be tag scopable. If this attribute is not provided, the current rules will be used to determine whether the permission should be tag scopable.
+
+## Frontend
+
+### Tooltip Changes
+
+The `flarum/common/components/Tooltip` component has been introduced as a simpler and less framework-dependent way to add tooltips. If your code is creating tooltips directly (e.g. via `$.tooltip()` in `oncreate` methods), you should wrap your components in the `Tooltip` component instead. For example:
+
+```tsx
+
+
+
+```
+
+See [the source code](https://github.com/flarum/core/blob/master/js/src/common/components/Tooltip.tsx) for more examples and instructions.
+
+See [the PR](https://github.com/flarum/core/pull/2843/files) for examples of how to change existing code to use tooltips.
+
+### PaginatedListState
+
+The `flarum/common/states/PaginatedListState` state class has been introduced to abstract away most of the logic of `DiscussionListState` and `NotificationListState`. It provides support for loading and displaying paginated lists of JSON:API resources (usually models). In future releases, we will also provide an `PaginatedList` (or `InfiniteScroll`) component that can be used as a base class for these paginated lists.
+
+Please see [the source code](https://github.com/flarum/core/blob/master/js/src/common/states/PaginatedListState.ts) for a list of methods.
+
+Note that `flarum/forum/states/DiscussionListState`'s `empty` and `hasDiscussions` methods have been removed, and replaced with `isEmpty` and `hasItems` respectively. This is a breaking change.
+
+### New Loading Spinner
+
+The old `spin.js` based loading indicator has been replaced with a CSS-based solution. For the most part, no changes should be needed in extensions, but in some cases, you might need to update your spinner. This change also makes it easier to customize the spinner.
+
+See [this discussion](https://discuss.flarum.org/d/26994-beta16-using-the-new-loading-spinner) for more information.
+
+### classList util
+
+Ever wrote messy code trying to put together a list of classes for some component? Well, no longer! The [clsx library](https://www.npmjs.com/package/clsx) is now available as the `flarum/common/utils/classList` util.
+
+### User List
+
+An extensible user list has been added to the admin dashboard. In future releases, we hope to extract a generic model table component that can be used to list any model in the admin dashboard.
+
+See [the source code](https://github.com/flarum/core/blob/master/js/src/admin/components/UserListPage.tsx#L41) for a list of methods to extend, and examples of how columns should look like (can be added by extending the `columns` method and adding items to the [ItemList](frontend.md)).
+
+### Miscellaneous
+
+- Components should now call `super` for ALL Mithril lifecycle methods they define. Before, this was only needed for `oninit`, `onbeforeupdate`, and `oncreate`. Now, it is also needed in `onupdate`, `onbeforeremove`, and `onremove`. See [this GitHub issue](https://github.com/flarum/core/issues/2446) for information on why this change was made.
+- The `flarum/common/utils/insertText` and `flarum/common/utils/styleSelectedText` utils have been moved to core from `flarum/markdown`. See `flarum/markdown` for an example of usage.
+- The `extend` and `override` utils can now modify several methods at once by passing in an array of method names instead of a single method name string as the second argument. This is useful for extending the `oncreate` and `onupdate` methods at once.
+- The `EditUserModal` component is no longer available through the `flarum/forum` namespace, it has been moved to `flarum/admin`. Imports should be adjusted.
+- The `Model` and `Route` JS extenders have been removed for now. There aren't currently used in any extensions that we know of. We will be reintroducing JS extenders during v1.x releases.
+- The `Search` component can now be used with the `SearchState` state class. Previously, `SearchState` was missing the `getInitialSearch` method expected by the `Search` component.
+
+## Backend
+
+### Filesystem Extenders
+
+In this release, we refactored our use of the filesystem to more consistently use [Laravel's filesystem API](https://laravel.com/docs/8.x/filesystem). Extensions can now declare new disks, more easily use core's `flarum-assets` and `flarum-avatars` disks, and create their own storage drivers, enabling CDN and cloud storage support.
+
+### Compat and Closure Extenders
+
+In early Flarum versions, the `extend.php` file allowed arbitrary functions that allowed exection of arbitrary code one extension boot. For example:
+
+```php
+return [
+ // other extenders
+ function (Dispatcher $events) {
+ $events->subscribe(Listener\FilterDiscussionListByTags::class);
+ $events->subscribe(Listener\FilterPostsQueryByTag::class);
+ $events->subscribe(Listener\UpdateTagMetadata::class);
+ }
+];
+```
+
+This approach was difficult to maintain and provide a well-tested public API for, frequently resolved classes early (breaking all sorts of things), and was not very descriptive. With the extender API completed in beta 16, this approach is no longer necessary. Support for these closures has been removed in this stable version.
+
+One type of functionality for which the extender replacement isn't obvious is container bindings ([e.g. flarum/pusher](https://github.com/flarum/pusher/blob/v0.1.0-beta.14/extend.php#L33-L49)). This can be done with via the service provider extender (e.g. [a newer version of flarum/pusher](https://github.com/flarum/pusher/blob/master/extend.php#L40-L41)).
+
+If you are unsure about which extenders should be used to replace your use of callbacks in `extend.php`, or are not sure that such an extender exists, please comment so below or reach out! We're in the final stages of finishing up the extender API, so now is the time to comment.
+
+### Scheduled Commands
+
+The [fof/console](https://github.com/FriendsOfFlarum/console) library has been a popular way to schedule commands (e.g. for publishing scheduled posts, running DB-heavy operations, etc) for several release. In Flarum 1.0, this functionality has been brought into core's `Console` extender. See our [console extension documentation](console.md) for more information on how to create schedule commands, and our [console user documentation](../console.md) for more information on how to run scheduled commands.
+
+### Eager Loading Extender
+
+As part of solving [N+1 Query issues](https://secure.phabricator.com/book/phabcontrib/article/n_plus_one/) in some [Flarum API endpoints](https://github.com/flarum/core/issues/2637), we have introduced a `load` method on the `ApiController` extender that allows you to indicate relations that should be eager loaded.
+
+This should be done if you know a relation will always be included, or will always be referenced by controller / permission logic. For example, we will always need the tags of a discussion to check what permissions a user has on that discussion, so we should eager load the discussion's `tags` relationship. For example:
+
+```php
+return [
+ // other extenders
+ (new Extend\ApiController(FlarumController\ListDiscussionsController::class))
+ ->addInclude(['tags', 'tags.state', 'tags.parent'])
+ ->load('tags'),
+];
+```
+
+### RequestUtil
+
+The `Flarum\Http\RequestUtil`'s `getActor` and `withActor` should be used for getting/setting the actor (user) on requests. `$request->getAttribute('actor')` and `$request->withAttribute('actor')` are deprecated, and will be removed in v2.0.
+
+### Miscellaneous
+
+- The `Formatter` extender now has an `unparse` method that allows modifying XML before unparsing content.
+- All route names must now be unique. In beta 16, uniqueness was enforced per-method; now, it is mandatory for all routes.
+- API requests sent through `Flarum\Api\Client` now run through middleware, including `ThrottleApi`. This means that it is now possible to throttle login/registration calls.
+- In beta 16, registering custom [searchers](search.md) was broken. It has been fixed in stable.
+- The `post_likes` table in the [flarum/likes](https://github.com/flarum/likes) extension now logs the timestamp when likes were created. This isn't used in the extension, but could be used in other extensions for analytics.
+- The `help` attribute of [admin settings](admin.md) no longer disappears on click.
+- The `generate:migration` console command has been removed. The [Flarum CLI](https://discuss.flarum.org/d/26525-rfc-flarum-cli-alpha) should be used instead.
+- The `GambitManager` util class is now considered internal API, and should not be used directly by extensions.
+- The `session` attribute is no longer available on the `User` class. This caused issues with queue drivers, and was not conceptually correct (a user can have multiple sessions). The current session is still available via the `$request` instance.
+- The `app`, `base_path`, `public_path`, `storage_path`, and `event` global helpers have been restored, but deprecated perpetually. These exist in case Laravel packages need them; they **should not** be used directly by Flarum extension code. The `flarum/laravel-helpers` package has been abandoned.
+- The following deprecated features from beta 16 have been removed:
+ - The `TextEditor`, `TextEditorButton`, and `SuperTextarea` components are no longer exported through the `flarum/forum` namespace, but through `flarum/common` (with the exception of `SuperTextarea`, which has been replaced with `flarum/common/utils/BasicEditorDriver`).
+ - Support for `Symfony\Component\Translation\TranslatorInterface` has been removed, `Symfony\Contracts\Translation\TranslatorInterface` should be used instead.
+ - All backwards compatibility layers for the [beta 16 access token refactors](update-b16.md#access-token-and-authentication-changes) have been removed
+ - The `GetModelIsPrivate` event has been removed. The `ModelPrivate` extender should be used instead.
+ - The `Searching` (for both `User` and `Discussion` models), `ConfigureAbstractGambits`, `ConfigureDiscussionGambits`, and `ConfigureUserGambits` events have been removed. The `SimpleFlarumSearch` extender should be used instead.
+ - The `ConfigurePostsQuery` event has been removed. The `Filter` extender should be used instead.
+ - The `ApiSerializer` extender's `mutate` method has been removed. The same class's `attributes` method should be used instead.
+ - The `SearchCriteria` and `SearchResults` util classes have been removed. `Flarum\Query\QueryCriteria` and `Flarum\Query\QueryResults` should be used instead.
+ - The `pattern` property of `AbstractRegexGambit` has been removed; the `getGambitPattern` method is now a required abstract method.
+ - The `AbstractSearch` util class has been removed. `Flarum\Search\SearchState` and `Flarum\Filter\FilterState` should be used instead.
+ - The `CheckingPassword` event has been removed, the `Auth` extender should be used instead.
+
+## Tags Extension Changes
+
+As mentioned above, [tag scopable permissions](#permissions-changes) can now be explicitly declared.
+
+We've also made several big refactors to significantly improve tags performance.
+
+Firstly, the [Tag](https://github.com/flarum/tags/blob/d093ca777ba81f826157522c96680717d3a90e24/src/Tag.php#L38-L38) model's static `getIdsWhereCan` and `getIdsWhereCannot` methods have been removed. These methods scaled horribly on forums with many tags, sometimes adding several seconds to response time.
+
+These methods have been replaced with a `whereHasPermission` [Eloquent dynamic scope](https://laravel.com/docs/8.x/eloquent#dynamic-scopes) that returns a query. For example:
+
+`Tag::whereHasPermission($actor, 'viewDiscussions)`.
+
+That query can then be used in other DB queries, or further constricted to retrieve individual tags.
+
+Secondly, we no longer load in all tags as part of the initial payload. The initial payload contains all top-level primary tags, and the top 3 secondary tags. Other tags are loaded in as needed. Future releases will paginate secondary tags. This should enable forums to have thousands of secondary tags without significant performance impacts. These changes mean that extensions should not assume that all tags are available in the model store.
+
+## Testing Library Changes
+
+- Bundled extensions will not be automatically enabled; all enabled extensions must be specified in that test case.
+- `setting` and `config` methods have been added that allow configuring settings and config.php values before the tested application boots. See [the testing docs](testing.md) for more information.
+- The `php flarum test:setup` command will now drop the existing test DB tables before creating the database. This means that you can run `php flarum test:setup` to ensure a clean database without needing to go into the database and drop tables manually.