diff --git a/src/Web/Documentation/content/main/2-features/11-localization.md b/src/Web/Documentation/content/main/2-features/11-localization.md new file mode 100644 index 00000000..95801829 --- /dev/null +++ b/src/Web/Documentation/content/main/2-features/11-localization.md @@ -0,0 +1,168 @@ +--- +title: Localization +description: "Tempest provides convenient utilities for localizing applications, including a translator built on the MessageFormat 2.0 specification." +--- + +## Overview + +Tempest provides a simple {b`Tempest\Intl\Translator`} interface for localizing applications. It allows you to translate messages into different languages and formats them according to the current or specified locale. + +The translator implements the [MessageFormat 2.0](https://messageformat.unicode.org/) specification, which provides a flexible syntax for defining translation messages. This specification is [maintained by the Unicode project](https://github.com/unicode-org/message-format-wg) and is widely used in internationalization libraries. + +## Translating messages + +To translate messages, you may [inject](../1-essentials/05-container.md) the {`Tempest\Intl\Translator`} interface and use its `translate()` method. If the translation message accepts variables, you may pass them as named parameters. + +```php +$translator->translate('cart.expire_at', expire_at: $expiration); +// Your cart is valid until 1:30 PM +``` + +To translate a message in a specific locale, you may use the `translateForLocale()` instead and provide the {b`Tempest\Intl\Locale`} as the first parameter. + +```php +$translator->translateForLocale(Locale::FRENCH, 'cart.expire_at', expire_at: $expiration); +// Votre panier expire à 12h30 +``` + +Alternatively, you may use the `translate` or the `translate_for_locale` function in the `Tempest\Intl` namespace. + +### Configuring the locale + +The current locale is stored in the `currentLocale` property of the {`Tempest\Intl\IntlConfig`} [configuration object](../1-essentials/06-configuration.md). You may configure another default locale by creating a dedicated configuration file: + +```php intl.config.php +return new IntlConfig( + currentLocale: Locale::FRENCH, + fallbackLocale: Locale::ENGLISH, +); +``` + + By default, Tempest uses the [`intl.default_locale`](https://www.php.net/manual/en/locale.getdefault.php) ini value for the current locale. + +### Changing the locale + +You may update the current locale at any time by mutating the {b`Tempest\Intl\IntlConfig`} configuration object. For instance, this could be done in a [middleware](../1-essentials/01-routing.md#route-middleware): + +```php +final readonly class SetLocaleMiddleware implements HttpMiddleware +{ + public function __construct( + private Authenticator $authenticator, + private IntlConfig $intlConfig, + ) {} + + public function __invoke(Request $request, HttpMiddlewareCallable $next): Response + { + $this->intlConfig->currentLocale = $this->authenticator + ->currentUser() + ->preferredLocale; + + return $next($request); + } +} +``` + +## Defining translation messages + +Translation messages are usually stored in translation files. Tempest automatically [discovers](../4-internals/02-discovery.md) YAML and JSON translation files that use the `..{yaml,json}` naming format, where `` may be any string, and `` must be an [ISO 639-1](https://en.wikipedia.org/wiki/List_of_ISO_639_language_codes) language code. + +For instance, you may store translation files in a `lang` directory: + +``` +src/ +└── lang/ + ├── messages.fr.yaml + └── messages.en.yaml +``` + +Alternatively, you may call the `add()` method on a {`Tempest\Intl\Catalog\Catalog`} instance to add a translation message at runtime. + +```php +$catalog->add(Locale::FRENCH, 'order.continue_shopping', 'Continuer vos achats'); +``` + +### Message syntax + +Tempest implements the [MessageFormat 2.0](https://messageformat.unicode.org/) specification, which provides a flexible syntax for defining translation messages. The syntax allows for variables, [pluralization](#pluralization), and [custom formatting functions](#custom-formatting-functions). + +Since most translation messages are multiline, YAML is the recommended format for defining them. Here is an example of a translation message that uses a [variable](https://messageformat.unicode.org/docs/reference/variables/), a [function](https://messageformat.unicode.org/docs/reference/functions/) and a function [parameter](https://messageformat.unicode.org/docs/reference/functions/#options): + +```yaml messages.en.yaml +today: + Today is {$today :datetime pattern=|yyyy/MM/dd|} +``` + +:::info +You may learn more about this syntax in the [MessageFormat documentation](https://messageformat.unicode.org/docs/translators/). +::: + +### Pluralization + +Pluralizing messages may be done using [matchers](https://messageformat.unicode.org/docs/reference/matchers/) and the `number` function. This syntax supports languages that have more than two plural categories. For instance, you may translate this sentence in Polish: + +```php messages.pl.yaml +cart: + items_count: + .input {$count :number} + .match $count + one {{Masz {$count} przedmiot.}} + few {{Masz {$count} przedmioty.}} + many {{Masz {$count} przedmiotów.}} + other {{Masz {$count} przedmiotów.}} +``` + +For more complex translation messages, you may also use multiple variables in a matcher. In this example, we use a `type` and a `count` variable in the same matcher. + +```php messages.pl.yaml +cart: + items_by_type_count: + .input {$type :string} + .input {$count :number} + .match $type $count + product one {{Masz {$count} produkt w koszyku.}} + product few {{Masz {$count} produkty w koszyku.}} + product many {{Masz {$count} produktów w koszyku.}} + product * {{Masz {$count} produktów w koszyku.}} + service one {{Masz {$count} usługę w koszyku.}} + service few {{Masz {$count} usługi w koszyku.}} + service many {{Masz {$count} usług w koszyku.}} + service * {{Masz {$count} usług w koszyku.}} + * one {{Masz {$count} element w koszyku.}} + * few {{Masz {$count} elementy w koszyku.}} + * many {{Masz {$count} elementów w koszyku.}} + * * {{Masz {$count} elementów w koszyku.}} +``` + +### Using markup + +Markup may be added to translation messages using a [dedicated syntax](https://messageformat.unicode.org/docs/reference/markup/) defined in the MessageFormat specification. Tempest provides a markup implementation that renders HTML tags and Iconify icons. + +```yaml +bold_text: 'This is {#strong}bold{/strong}.' +ui: + open_menu: '{#icon-tabler-menu/} Open menu' +``` + +It is possible to implement your own markup by implementing the {b`Tempest\Intl\MessageFormat\MarkupFormatter`} or {b`Tempest\Intl\MessageFormat\StandaloneMarkupFormatter`} interfaces. Classes implementing these interfaces are automatically discovered by Tempest. + +### Custom formatting functions + +The [MessageFormat 2.0](https://messageformat.unicode.org/) specification allows for defining custom formatting functions that can be used in translation messages. By default, Tempest provides formatting functions for strings, numbers and dates. + +You may define a custom formatting function by implementing the {b`Tempest\Intl\MessageFormat\FormattingFunction`} interface. For instance, the function for formatting dates is implemented as follows: + +```php +final class DateTimeFunction implements FormattingFunction +{ + public string $name = 'datetime'; + + public function evaluate(mixed $value, array $parameters): FormattedValue + { + $datetime = DateTime::parse($value); + $formatted = $datetime->format(Arr\get_by_key($parameters, 'pattern')); + + return new FormattedValue($value, $formatted); + } +} +``` diff --git a/src/Web/Documentation/content/main/2-features/15-datetime.md b/src/Web/Documentation/content/main/2-features/15-datetime.md index ae88d88f..6b4d3df1 100644 --- a/src/Web/Documentation/content/main/2-features/15-datetime.md +++ b/src/Web/Documentation/content/main/2-features/15-datetime.md @@ -107,7 +107,7 @@ You may format a {b`Tempest\DateTime\DateTime`} instance in a specific format us ```php use Tempest\DateTime\FormatPattern; -use Tempest\DateTime\Locale; +use Tempest\Intl\Locale; $date->format(); // 19 Sept 2025, 02:00:00 $date->format(pattern: FormatPattern::COOKIE); // Monday, 19-Sept-2025 02:00:00 BST