7: Translations
Phlo treats translation as a rendering concern: you write view text in your source language, mark it, and the lang resource serves the right language at request time, translating missing entries in the background. In this chapter the poll learns Dutch under a /nl prefix.
7.1: Source-language markers
Static view text gets a marker keyed by the language the text is written in. This app's source language is English, so the key is en:
view:
<main#app.poll>
<h1>{en: Which stack wins?}</h1>
{{ $this->results }}
{{ $this->choices }}
</main>
{en: ...} compiles to a call to the en() helper, which asks the translation layer for the text in the active language. If the active language is English, the text passes through untouched. If you were writing a Dutch-source app you would use {nl: ...} with the same mechanics; the marker always names the source language of that specific string. Outside views, the same helpers work in code: method title => en('Phlo Poll').
Update the <h1> in poll.phlo now; the page still renders identically, because the active language is still English.
7.2: Configure the language layer
Three small pieces of configuration. First, the resources in data/app.json (the lang resource brings cookie detection, INI file caching, and AI-backed translation):
{
"resources": [
"DB/DB",
"DB/model",
"DB/JSONDB",
"DB/JSON.result",
"payload",
"phlo.async",
"DOM/form",
"lang",
"cookies",
"files/INI",
"AI/AI",
"AI/OpenAI",
"security/creds"
]
}
Second, tell the runtime where translation files live, in www/app.php:
phlo_app(
id: 'Poll',
host: 'localhost',
build: true,
debug: true,
app: dirname(__DIR__).'/',
langs: dirname(__DIR__).'/langs/',
);
Every named argument to phlo_app() becomes an app-wide constant, so langs is now a path you (and the lang resource) can use anywhere. Create the folder: mkdir app/langs.
Third, declare the app's languages in app.phlo:
prop lang = 'en'
prop langs = ['en' => 'English', 'nl' => 'Nederlands']
Rebuild and lint; both end at [].
7.3: A /nl route prefix
The active language lives on %app->lang. Set it from the URL with a second route in poll.phlo:
route GET poll => $this->home
route GET $lang:nl poll {
%app->lang = $lang
$this->home
}
$lang:nl is a value-list segment: it only matches when the first path segment is literally nl. With more languages you extend the list: $lang:nl,de,fr. The English route keeps the clean URL; the prefixed route sets the language and renders the same page.
Open http://localhost/nl/poll. The <html lang="nl"> attribute follows %app->lang, but the heading still reads English: there is no Dutch translation yet.
7.4: The translation cache
Translations live in langs/<lang>.ini, one line per string, keyed by a hash of the source text. When a marked string has no entry for the active language, the page shows the source text and schedules a background translation through OpenAI; the next request reads the cached result. For that backfill to work, put an API key in data/creds.ini:
OpenAI = sk-...
No key? Everything still works, missing strings just stay in the source language. You can also write the file yourself; after the first backfill, langs/nl.ini looks like this:
enWhichStad3 = "Welke stack wint?"
The key encodes the source language plus a hash of the exact text, so changed source text gets a new entry. Reload http://localhost/nl/poll once the entry exists:
<h1>Welke stack wint?</h1>
Same view, same route logic, second language. The poll is one chapter away from done: it still cannot push results to other viewers.