feat(11.1-04): add the Database section and the core Services pages
- docs/database: models, migrations, queries and pagination, relations, casts and validation, attachments and transactions (lagoon.Transaction, lagoon.AfterCommit, nested savepoints, lagoon.OnDatabase) - docs/services: configuration, events, routing with auth groups, rate limiting, authentication, the OAuth server, mail and localization - runnable Examples for lagoon, attach, compass, surf, wire, bouncer, wristband, postcard, phrasebook and festival; lagoon TestDocs* regions run on the package's Postgres harness through DocsDB - 15 new required pages
This commit is contained in:
84
docs/services/localization.md
Normal file
84
docs/services/localization.md
Normal file
@@ -0,0 +1,84 @@
|
||||
---
|
||||
title: Localization
|
||||
description: Ship plugin translations in lang YAML catalogs, translate with :name placeholders and CLDR plurals through phrasebook, and read the request locale.
|
||||
section: services
|
||||
order: 80
|
||||
---
|
||||
# Localization
|
||||
|
||||
WinterCMS plugins keep their strings in `lang/<locale>/*.php` and read them with `Lang::get('acme.blog::lang.posts.title')` and `trans_choice`. SummerCMS keeps the key form and Laravel's message syntax; [phrasebook](../../modules/phrasebook/README.md) loads the catalogs and translates.
|
||||
|
||||
## Catalogs
|
||||
|
||||
A plugin ships `lang/<locale>/<group>.yaml` files and implements `pact.HasLang` to return them. Nested maps flatten into dotted keys under the plugin ID, so `title` in `lang/en/posts.yaml` of `acme.blog` is the key `acme.blog::posts.title`. At boot, `phrasebook.Activate` loads the framework strings and every plugin's catalogs and publishes one `phrasebook.Translator` on the application. Duplicate keys, malformed paths and values that are not strings fail the start-up.
|
||||
|
||||
A plugin that implements `pact.HasLangOverrides` can replace keys of any loaded namespace, the framework's admin strings included, with files laid out as `lang/<locale>/<namespace>/<group>.yaml`. Overrides may also add a locale.
|
||||
|
||||
## Translating
|
||||
|
||||
`phrasebook.Translator.Get` translates a key in the request locale, and `phrasebook.Translator.GetIn` in a locale you name. A lookup tries the locale, then its parent (`pt-BR`, then `pt`), then `app.fallback_locale`. A key that no locale has comes back unchanged, and outside production it is logged once. Placeholders follow Laravel: `:name` inserts the value, `:Name` capitalizes its first letter and `:NAME` upper-cases it:
|
||||
|
||||
```go src=modules/phrasebook/example_test.go#ExampleTranslator_Get
|
||||
cat := phrasebook.NewCatalog()
|
||||
if err := cat.Load("acme.blog", langFS); err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
tr := phrasebook.NewTranslator(cat, phrasebook.Options{Locale: "en", Fallback: "en"})
|
||||
|
||||
// surf stores the request locale on the context; here the example does.
|
||||
ctx := towel.WithLocale(context.Background(), "pl")
|
||||
fmt.Println(tr.Get(ctx, "acme.blog::posts.title", nil))
|
||||
// pl has no greeting, so the fallback locale answers.
|
||||
fmt.Println(tr.Get(ctx, "acme.blog::posts.greeting", map[string]string{"name": "Ada"}))
|
||||
fmt.Println(tr.GetIn("en", "acme.blog::posts.shout", map[string]string{"name": "Ada"}))
|
||||
// A missing key comes back as the key.
|
||||
fmt.Println(tr.Get(ctx, "acme.blog::posts.missing", nil))
|
||||
// Output:
|
||||
// Posty
|
||||
// Hello, Ada
|
||||
// Welcome, ADA
|
||||
// acme.blog::posts.missing
|
||||
```
|
||||
|
||||
Application code gets the published translator with `app.Lookup[*phrasebook.Translator]()`.
|
||||
|
||||
## Plurals
|
||||
|
||||
`phrasebook.Translator.Choice` and `phrasebook.Translator.ChoiceIn` pick a plural form and fill in `:count`. A key can hold a map of CLDR plural categories (`one`, `few`, `many`, `other`, ...), checked against the categories the locale actually has, so a Polish string gets the forms Polish needs. Laravel's pipe syntax works too, with exact (`{0}`) and range (`[2,*]`) conditions:
|
||||
|
||||
```go src=modules/phrasebook/example_test.go#ExampleTranslator_Choice
|
||||
cat := phrasebook.NewCatalog()
|
||||
if err := cat.Load("acme.blog", langFS); err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
tr := phrasebook.NewTranslator(cat, phrasebook.Options{Locale: "en", Fallback: "en"})
|
||||
|
||||
for _, n := range []int{1, 3, 5, 22} {
|
||||
fmt.Println(tr.ChoiceIn("pl", "acme.blog::posts.count", n, nil))
|
||||
}
|
||||
fmt.Println(tr.ChoiceIn("en", "acme.blog::posts.count", 5, nil))
|
||||
for _, n := range []int{0, 1, 7} {
|
||||
fmt.Println(tr.ChoiceIn("en", "acme.blog::posts.drafts", n, nil))
|
||||
}
|
||||
// Output:
|
||||
// 1 post
|
||||
// 3 posty
|
||||
// 5 postów
|
||||
// 22 posty
|
||||
// 5 posts
|
||||
// No drafts
|
||||
// One draft
|
||||
// 7 drafts
|
||||
```
|
||||
|
||||
## The request locale
|
||||
|
||||
The locale lives on the request context, not in a global. For every route, [surf](../../modules/surf/README.md) sets it from the `Accept-Language` header; the `locale.from-principal` middleware switches it to the signed-in user's preferred locale. Code reads it with `towel.Locale`, and code outside a request sets it with `towel.WithLocale`, as the example above does. A context without a locale uses `app.locale`.
|
||||
|
||||
## Strings for the admin
|
||||
|
||||
The admin SPA receives its strings from the server. `phrasebook.Translator.Bundle` returns every key under a prefix as CLDR plural forms, merged over the fallback chain, and `phrasebook.Translator.Forms` returns one key. Start-up fails if an admin (`backend::`) string cannot be expressed as CLDR forms, so a pipe string with a condition the SPA cannot evaluate is caught before any admin sees it.
|
||||
|
||||
The framework ships its validation messages (`lagoon::validate`) and admin strings (`backend::lang`) in English and Polish.
|
||||
Reference in New Issue
Block a user