AI blog · DotApp PHP Framework 2.0
How translations and i18n work in DotApp PHP Framework
Visible Shop strings go through Translator. The key is the source text (looked up in lowercase).
Templates use {{_ "Send" }}. PHP uses Translator::trans('Send').
Locale files live in the module: app/modules/Shop/translations/sk_sk.json, loaded with
Translator::loadLocaleFile('Shop:sk_sk.json', 'sk_sk') in initialize().
A missing file is skipped with no exception. A missing key returns the original string — there is no pluralization and no locale fallback chain.
This article is a complete Shop locale: JSON, boot, template, and PHP with placeholders.
Must: load the file yourself
Putting a file in translations/ does nothing until initialize() calls loadLocaleFile (or loadFile / loadLocaleArray).
A wrong path or missing file is silent — the UI keeps showing English (the source key).
Keys are lowercased on lookup: "Send" and "send" are the same entry.
setDefaultLocale does not fill missing keys from another language — a miss returns the source text.
JSON values are product copy a vendor would ship, not prompt-echo.
Common mistakes
| Wrong | Right |
|---|---|
Drop sk_sk.json in the folder and expect it to load |
Translator::loadLocaleFile('Shop:sk_sk.json', 'sk_sk') in initialize() |
Invent __('Send') or {{ trans: Send }} |
Template {{_ "Send" }}. PHP Translator::trans('Send') |
| Assume a missing file throws | It is skipped. Check the path; use Translator::has('send') if you must detect a miss |
| Expect German to fall back to English keys in the JSON | There is no fallback chain. A miss returns the source string you passed in |
| Prompt-echo in JSON values (“so the user can hide…”) | Shipped UI copy: short, imperative, same language as the rest of the product |
Single quotes in {{_ 'Send' }} |
Double quotes only on the template helper |
When to translate
Buttons, labels, empty states, toasts, confirm copy, page titles, and permission names that a person can see.
Do not translate log lines, SQL, or technical exception text you only write to Logger.
Views: How to render views and layouts.
Scaffold: How to create a module.
Where files live
Module syntax Shop:sk_sk.json resolves to app/modules/Shop/translations/sk_sk.json.
A subfolder is allowed: Shop:checkout/sk_sk.json.
A path without a module prefix is relative to the project root.
Single-locale JSON is a flat object: source text → translated text.
Multi-locale JSON (for loadFile) nests locales: { "sk_sk": { "send": "Odoslať" } }.
{
"send": "Odoslať",
"catalog": "Katalóg",
"welcome, {{ arg0 }}": "Vitajte, {{ arg0 }}",
"no items yet.": "Zatiaľ žiadne položky."
}
Lookup lowercases the key. You may write "Send" in the JSON; trans('Send') still matches.
Placeholders in the value are {{ arg0 }}, {{ arg1 }}, … — replaced in order from extra PHP arguments.
Translator API
| Call | Returns | Notes |
|---|---|---|
trans($text, ...$args) / t() |
string |
Original text if the key is missing |
setLocale($locale) / getLocale() |
self / string |
Default current locale is en_us |
setDefaultLocale / getDefaultLocale |
self / string |
Does not fill missing keys from that locale |
loadLocaleFile($file, $locale) |
self |
Missing file skipped |
loadFile($file) |
self |
Multi-locale JSON |
loadArray / loadLocaleArray |
self |
PHP arrays instead of JSON |
has($key, $locale = null) |
bool |
Detect a missing key |
all($locale = null) |
array |
All keys for that locale |
Legacy: translator('Send') and translator([])->set_locale('sk_sk') still work (the helper in app/config.php).
New Shop code uses the facade. Do not invent a second i18n stack.
Template helper
<button type="submit">{{_ "Send" }}</button>
<h1>{{_ var: $headline }}</h1>
Double quotes on the literal form. {{_ var: $headline }} translates the value of the variable.
Pair with {{ var: $title }} when the string is already translated in PHP (or must stay raw).
Secure forms still need <fo-rm> and {{ formName(...) }} between the tags —
secure forms.
Complete Shop files
app/modules/Shop/
module.init.php
translations/sk_sk.json
translations/de_de.json
views/home.view.php
Controllers/Home.php
Load locales in initialize() next to config fallbacks. Set the active locale from config (or from a DSM flag you own — not from $_SESSION).
<?php
namespace Dotsystems\App\Modules\Shop;
use Dotsystems\App\Parts\Config;
use Dotsystems\App\Parts\Router;
use Dotsystems\App\Parts\Translator;
class Module extends \Dotsystems\App\Parts\Module
{
public function initialize($dotApp)
{
Config::module('Shop', 'prefix') ?? Config::module('Shop', 'prefix', '/shop');
Config::module('Shop', 'locale') ?? Config::module('Shop', 'locale', 'en_us');
Translator::setDefaultLocale('en_us');
Translator::loadLocaleFile('Shop:sk_sk.json', 'sk_sk');
Translator::loadLocaleFile('Shop:de_de.json', 'de_de');
Translator::setLocale((string) Config::module('Shop', 'locale'));
$p = Config::module('Shop', 'prefix');
Router::get($p . '/', 'Shop:Home@index!', Router::STATIC_ROUTE);
}
public function initializeRoutes()
{
return ['/shop', '/shop/*'];
}
public function initializeCondition($routeMatch)
{
return $routeMatch;
}
}
new Module($dotApp);
<?php
namespace Dotsystems\App\Modules\Shop\Controllers;
use Dotsystems\App\Parts\Config;
use Dotsystems\App\Parts\Logger;
use Dotsystems\App\Parts\Renderer;
use Dotsystems\App\Parts\Response;
use Dotsystems\App\Parts\Translator;
class Home extends \Dotsystems\App\Parts\Controller
{
public static function index($request)
{
$name = 'Ada';
$welcome = Translator::trans('Welcome, {{ arg0 }}', $name);
$html = Renderer::new()->module('Shop')
->setView('home')
->setViewVar('title', Translator::trans('Catalog'))
->setViewVar('welcome', $welcome)
->setViewVar('prefix', Config::module('Shop', 'prefix'))
->renderView();
if ($html === '') {
Logger::use()->error('Shop home view produced empty output');
return new Response(500, 'Template error');
}
return $html;
}
}
<!DOCTYPE html>
<html lang="sk">
<head>
<meta charset="utf-8" />
<title>{{ var: $title }}</title>
</head>
<body>
<h1>{{ var: $title }}</h1>
<p>{{ var: $welcome }}</p>
<p>{{_ "No items yet." }}</p>
<a href="{{ var: $prefix }}/contact">{{_ "Send" }}</a>
</body>
</html>
Override the locale for this app in app/config.php: Config::module('Shop', 'locale', 'sk_sk'); —
How app/config.php works.
Boot order: How module initialization works.
Product copy
JSON values and template literals a person can see must read as shipped UI.
Wrong: “This was added so the user can hide the icon.” Right: “Hide assistant.”
Match the surrounding screens. Do not add chatty extras in sk_sk.json that were not in the source.
FAQ
The UI is still English after I added sk_sk.json
The file is not loaded, the locale is still en_us, the path is wrong, or the JSON is invalid (invalid JSON is swallowed).
Call loadLocaleFile in initialize(), then setLocale('sk_sk'). Confirm the file is
app/modules/Shop/translations/sk_sk.json.
Does default locale fill missing Slovak keys?
No. A miss returns the source string you passed to trans() or {{_ }}.
Use Translator::has('send', 'sk_sk') if you need to detect a hole.
How do I pluralize?
You do not — not in this class. Pick a phrasing that works without a count, or choose the sentence in PHP before you translate.
Why is arg0 still visible?
The placeholder in the JSON value must be exactly {{ arg0 }} (spaces as shown). Pass the replacement as extra arguments to trans().
The template helper does not take PHP varargs — translate with placeholders in the controller, then {{ var: $welcome }}.
Should I use translator() from config.php?
The helper exists for old code. New Shop code uses Translator::. Do not mix a third library.
Is this the same as Request data(true)?
No. Translator is i18n. Request auto-protects incoming POST — passwords still need data(true).
That switch: Request lifecycle.