Skip to content

Commit

Permalink
docs(book): 📝 wrote docs for i18n, error pages, and static content
Browse files Browse the repository at this point in the history
  • Loading branch information
arctic-hen7 committed Sep 20, 2021
1 parent f7f7892 commit 0375f01
Show file tree
Hide file tree
Showing 8 changed files with 143 additions and 7 deletions.
14 changes: 7 additions & 7 deletions docs/next/src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,13 @@
- [Writing Views](./views.md)
- [Templates and Routing](./templates/intro.md)
- [Modifying the `<head>`](./templates/metadata-modification.md)
- [Error Pages]()
- [Static Content]()
- [Internationalization]()
- [Defining Translations]()
- [Using Translations]()
- [Translations Managers]()
- [Other Translation Engines]()
- [Error Pages](./error-pages.md)
- [Static Content](./static-content.md)
- [Internationalization](./i18n/intro.md)
- [Defining Translations](./i18n/defining.md)
- [Using Translations](./i18n/using.md)
- [Translations Managers](./i18n/translations-managers.md)
- [Other Translation Engines](./i18n/other-engines.md)
- [Rendering Strategies]()
- [Build Paths]()
- [Build State]()
Expand Down
30 changes: 30 additions & 0 deletions docs/next/src/error-pages.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Error Pages

When developing for the web, you'll need to be familiar with the idea of an *HTTP status code*, which is a numerical indication in HTTP (HyperText Transfer Protocol) of how the server reacted to a client's request. The most well-known of these is the infamous *404 Not Found* error, but there are dozens of these in total. Don't worry, you certainly don't need to know all of them by heart!

## Handling HTTP Status Codes in Perseus

Perseus has an *app shell* that manages fetching pages for the user (it's a little more complicated than the traditional design of that kind of a system, but that's all you need to know for now), and this is where HTTP errors will occur as it communicates with the Perseus server. If the status code is an error, this shell will fail and render an error page instead of the page the user visited. This way, an error page can be displayed at any route, without having to navigate to a special route.

You can define one error page for each HTTP status code in Perseus, and you can see a list of those [here](https://httpstatuses.com). Here's an example of doing so for *404 Not Found* and *400* (a generic error caused by the client) (taken from [here](https://github.com/arctic-hen7/perseus/tree/main/examples/showcase/src/error_pages.rs)):

```rust,no_run,no_playground
{{#include ../../../examples/showcase/src/error_pages.rs}}
```

It's conventional in Perseus to define a file called `src/error_pages.rs` and put your error pages in here for small apps, but for larger apps where your error pages are customized with beautiful logos and animations, you'll almost certainly want this to be a folder, and to have a separate file for each error page.

When defining an instance of `ErrorPages`, you'll need to provide a fallback page, which will be used for all the status codes that you haven't specified unique pages for. In the above example, this fallback would be used for, say, a *500* error, which indicates an internal server error.

The most important thing to note about these error pages is the arguments they each take, which have all been ignored in the above example with `_`s. There are four of these:

- URL that caused the error
- HTTP status code (`u16`)
- Error message
- Translator (inside an `Option<T>`)

## Translations in Error Pages

Error pages are also available for you to use yourself (see the [API docs](https://docs.rs/perseus) on the functions to call for that) if an error occurs in one of your own pages, and in that case, if you're using i18n, you'll have a `Translator` available. However, there are *many* cases in Perseus in which translators are not available to error pages (e.g. the error page might have been rendered because the translator couldn't be initialized for some reason), and in these cases, while it may be tempting to fall back to the default locale, you should optimally make your page as easy to decipher for speakers of other languages as possible. This means emoji, pictures, icons, etc. Bottom line: if the fourth parameter to an error page is `None`, then communicate as universally as possible.

An alternative is just to display an error message in every language that your app supports, which may in some cases be easier and more practical.
27 changes: 27 additions & 0 deletions docs/next/src/i18n/defining.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Defining Translations

The first part of setting up i18n in Perseus is to state that your app uses it, which is done in the `define-app!` macro like so (taken from [the i18n example](https://github.com/arctic-hen7/perseus/tree/main/examples/i18n)):

```rust,no_run,no_playground
{{#include ../../../../examples/i18n/src/lib.rs}}
```

There are two subfields under the `locales` key: `default` and `other`. Each of these locales should be specified in the form `xx-XX`, where `xx` is the language code (e.g. `en` for English, `fr` for French, `la` for Latin) and `XX` is the region code (e.g. `US` for United States, `GB` for Great Britain, `CN` for China).

## Routing

After you've enabled i18n like so, every page on your app will be rendered behind a locale. For example, `/about` will become `/en-US/about`, `/fr-FR/about`, and`/es-ES/about` in the above example. These are automatically rendered by Perseus at build-time, and they behave exactly the same as every other feature of Perseus.

Of course, it's hardly optimal to direct users to a pre-translated page if they may prefer it in another language, which is why Perseus supports *locale detection* automatically. In other words, you can direct users to `/about`, and they'll automatically be redirected to `/<locale>/about`, where `<locale>` is their preferred locale according to `navigator.languages`. This matching is done based on [RFC 4647](https://www.rfc-editor.org/rfc/rfc4647.txt), which defines how locale detection should be done.

## Adding Translations

After you've added those definitions to `define_app!`, if you try to run your app, you'll find that ever page throws an error because it can't find any of the translations files. These must be defined under `translations/` (which should be NEXT to `/src`, not under it!), though this can be customized (explained later). They must also adhere to the naming format `xx-XX.ftl` (e.g. `en-US.ftl`). `.ftl` is the file extension that [Fluent](https://projectfluent.org) files use, which is the default translations system of Perseus. If you'd like to use a different system, this will be explained later.

Here's an example of a translations file (taken from [here](https://github.com/arctic-hen7/perseus/blob/main/examples/i18n/translations/en-US.ftl)):

```fluent
{{#include ../../../../examples/i18n/translations/en-US.ftl}}
```

You can read more about Fluent's syntax [here](https://projectfluent.org) (it's *very* powerful).
7 changes: 7 additions & 0 deletions docs/next/src/i18n/intro.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Internationalization

Internationalization (abbreviated *i18n*) is making an app available in many languages. Perseus supports this out-of-the-box with [Fluent](https://projectfluent.org).

The approach usually taken to i18n is to use translation IDs in your code instead of natural language. For example, instead of writing `format!("Hello, {}!", name.get())`, you'd write something like `t!("greeting", {"name" => name.get()})`. This ensures that your app works well for people across the world, and is crucial for any large apps.

This section will explain how i18n works in Perseus and how to use it to make lightning-fast apps that work for people across the planet.
13 changes: 13 additions & 0 deletions docs/next/src/i18n/other-engines.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Other Translation Engines

Perseus uses [Fluent](https://projectfluent.org) for i18n by default, but this isn't set in stone. Rather than providing only one instance of `Translator`, Perseus can support many through Cargo's features system. By default, Perseus will enable the `translator-fluent` feature to build a `Translator` `struct` that uses Fluent. The `translator-dflt-fluent` feature will also be enabled, which sets `perseus::Translator` to be an alias for `FluentTranslator`.

If you want to create a translator for a different system, this will need to be integrated into Perseus as a pull request, but we're more than happy to help with these efforts. Optimally, Perseus will in future support multiple translations systems, and developers will be able to pick the one they like the most

## Why Not a Trait?

It may seem like this problem could simply be solved with a `Translator` trait, as is done with translations managers, but unfortunately this isn't so simple because of the way translators are transported through the app. The feature-gating solution was chosen as the best compromise between convenience and performance.

## How Do I Make One?

If you want to make your own alternative translation engine, please [open an issue](https://github.com/arctic-hen7/perseus/issues/new/choose) about it, explaining the system you want to support. Provided the system is compatible with Perseus' i18n design (which it certainly should be if we've done our job correctly!), we'll be happy to help you get it into Perseus!
17 changes: 17 additions & 0 deletions docs/next/src/i18n/translations-managers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Translations Managers

As mentioned earlier, Perseus expects your translations to be in the very specific location of `translations/<locale>.ftl`, which may not be feasible or preferable in all cases. In fact, there may indeed be cases where translations might be stored in an external database (not recommended for performance as translations are regularly requested, filesystem storage with caching is far faster).

If you'd like to change this default behavior, this section is for you! Perseus manages the locations of translations with a `TranslationsManager`, which defines a number of methods for accessing translations, and should implement caching internally. Perseus has two inbuilt managers: `FsTranslationsManager` and `DummyTranslationsManager`. The former is used by default, and the latter if i18n is disabled.

## Using a Custom Translations Manager

The `define_app!` macro accepts a property called `translations_manager` if you define `locales`, which can be used to specify a non-default translations manager.

## Using a Custom Directory

If you just want to change the directory in which translations are stored, you can still use `FsTranslationsmanager`, just initialize it with a different directory, and make sure to set up caching properly. See [here](https://github.com/arctic-hen7/perseus/blob/f7f7892fbf124a7d887b1f22a1641c79773d6246/packages/perseus/src/macros.rs#L35-L50) for how this is done internally.

## Building a Custom Translations Manager

This is more complex, and you'll need to consult [this file](https://github.com/arctic-hen7/perseus/blob/main/packages/perseus/src/translations_manager.rs) (note: the client translations manager is very different) in the Perseus source code for guidance. If you're stuck, don't hesitate to ask a question under [discussions](https://github.com/arctic-hen7/perseus/discussions/new) on GitHub!
21 changes: 21 additions & 0 deletions docs/next/src/i18n/using.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Using Translations

Perseus tries to make it as easy as possible to use translations in your app by exposing the low-level Fluent primitives necessary to work with very complex translations, as well as a `t!` macro that does the basics.

All translations in Perseus are done with an instance of `Translator`, which is provided through Sycamore's [context system](https://sycamore-rs.netlify.app/docs/v0.6/advanced/contexts). Here's an example taken from [here](https://github.com/arctic-hen7/perseus/blob/main/examples/i18n/src/templates/index.rs):

```rust,no_run,no_playground
{{#include ../../../../examples/i18n/src/templates/index.rs}}
```

In that example, we've imported `perseus::t`, and we use it to translate the `hello` ID, which takes an argument for the username. Notice that we don't provide a locale, Perseus handles all this in the background for us.

## Getting the `Translator`

That said, there are some cases in which you'll want access to the underlying `Translator` so you can do more complex things. You can get it like so:

```rust,no_run,no_playground
sycamore::context::use_context::<Rc<Translator>>();
```

To see all the methods available on `Translator`, see [the API docs](https://docs.rs/perseus).
21 changes: 21 additions & 0 deletions docs/next/src/static-content.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Static Content

It's very routine in a web app to need to access *static content*, like images, and Perseus supports this out-of-the-box. Any and all static content for your website that should be served over the network should be put in a directory called `static/`, which should be at the root of your project (NOT under `src/`!). Any files/folders you put in there will be accessible on your website at `/.perseus/static/[filename-here]` **to anyone**. If you need content to be protected in some way, this is not the mechanism to use (consider a separate API endpoint)!

## Aliasing Static Content

One problem with making all static content available under `/.perseus/static/` is that there are sometimes occasions where you need it available at other locations. The most common example of this is `/favicon.ico` (the little logo that appears next to your app's title in a browser tab), which must be at that path.

*Static aliases* allow you to handle these conditions with ease, as they let you define static content to be available at any given path, and to map to any given file in your project's directory.

You can define static aliases in the `define_app!` macro's `static_aliases` parameter. Here's an example from [here](https://github.com/arctic-hen7/perseus/blob/main/examples/basic/src/lib.rs):

```rust,no_run,no_playground
{{#include ../../../examples/basic/src/lib.rs}}
```

### Security

Of course, being able to serve any file on your system in a public-facing app is a major security vulnerability, so Perseus will only allow you to create aliases for paths in the current directory. Any absolute paths or paths that go outside the current directory will be disallowed. Note that these paths are defined relative to the root of your project.

**WARNING:** if you accidentally violate this requirement, your app **will not load** at all!

0 comments on commit 0375f01

Please sign in to comment.