Integration Guide
How to drop the i18n Manager next to an existing project and use it to manage that project's translations — without changing your app's stack.
Integration models
The i18n Manager is a self-contained admin tool with its own database. The recommended approach is to run it as a standalone service and have your project read its translations. Your project never imports the PHP code — it only consumes data.
| Model | How | Best for |
|---|---|---|
| A. Standalone service (recommended) | Deploy the manager at its own host/vhost (e.g. https://i18n.yourcompany.com) against a dedicated database. Your project reads from its API. |
Any stack. Keeps concerns separate; no path conflicts. |
| B. Build-time data source | Run the manager locally/internally, export per-language JSON, and commit those files into your project's existing i18n pipeline. | Static sites, bundlers, offline runtime, no extra network dependency in production. |
/api/, /includes/, $_SERVER['DOCUMENT_ROOT']). Running it as its own app avoids reworking those paths — and is the path these recipes assume.The consumption endpoint
Everything your project needs comes from one endpoint:
GET {MANAGER}/api/translations_i18n.php
Query params (all optional):
namespace=nav # only keys in this namespace
search=home # ILIKE match on key/description
format=json # json (default) or js
Response (format=json) — a flat map of every active language:
{
"nav.home": { "en": "Home", "de": "Startseite" },
"home.welcome": { "en": "Welcome", "de": "Willkommen" }
}
With format=js the same data is returned as var i18n = {…}; (content-type application/javascript) — handy to load via a plain <script> tag. For a single-language file (e.g. to commit per locale) use /api/translations_export.php?language=de&format=json.
map[key][lang], falling back to a default language (usually en) when a key/language is missing.Recipe — PHP project
Fetch the map once, cache it to a local file, and expose a tiny t() helper. This keeps production fast and resilient if the manager is briefly unreachable.
<?php
// i18n.php — drop into your project
const I18N_MANAGER = 'https://i18n.yourcompany.com';
const I18N_CACHE = __DIR__ . '/cache/i18n.json';
const I18N_TTL = 300; // seconds
const I18N_FALLBACK = 'en';
function i18n_map(): array {
// Serve from cache if fresh
if (is_file(I18N_CACHE) && time() - filemtime(I18N_CACHE) < I18N_TTL) {
return json_decode(file_get_contents(I18N_CACHE), true) ?: [];
}
// Refresh from the manager
$url = I18N_MANAGER . '/api/translations_i18n.php?format=json';
$json = @file_get_contents($url);
if ($json !== false) {
@mkdir(dirname(I18N_CACHE), 0775, true);
file_put_contents(I18N_CACHE, $json, LOCK_EX);
return json_decode($json, true) ?: [];
}
// Fall back to the last good cache even if stale
return is_file(I18N_CACHE)
? (json_decode(file_get_contents(I18N_CACHE), true) ?: [])
: [];
}
function t(string $key, ?string $lang = null): string {
static $map = null;
$map ??= i18n_map();
$lang ??= $_SESSION['lang'] ?? I18N_FALLBACK;
return $map[$key][$lang]
?? $map[$key][I18N_FALLBACK]
?? $key; // show the key if nothing found
}
?>
<!-- usage in a template -->
<h1><?= htmlspecialchars(t('home.welcome')) ?></h1>
echo t(...) (no escaping); escape everything else with htmlspecialchars() to avoid XSS.Recipe — JavaScript / SPA
The dashboard's own localization engine is fully reusable. Save this as i18n.js, serve it from your project, and tag elements with data-i18n just like the manager does.
// i18n.js — framework-free runtime localizer
(function (global) {
const MANAGER = 'https://i18n.yourcompany.com';
const FALLBACK = 'en';
let map = {};
async function load() {
const res = await fetch(`${MANAGER}/api/translations_i18n.php?format=json`);
map = await res.json();
return map;
}
function t(key, lang) {
const e = map[key];
return (e && (e[lang] ?? e[FALLBACK])) ?? key;
}
function apply(lang) {
localStorage.setItem('lang', lang);
document.documentElement.setAttribute('lang', lang);
document.querySelectorAll('[data-i18n]').forEach(el => {
const v = t(el.getAttribute('data-i18n'), lang);
if (v != null) el.textContent = v;
});
document.querySelectorAll('[data-i18n-html]').forEach(el => {
const v = t(el.getAttribute('data-i18n-html'), lang);
if (v != null) el.innerHTML = v; // only for trusted HTML keys
});
document.querySelectorAll('[data-i18n-placeholder]').forEach(el => {
const v = t(el.getAttribute('data-i18n-placeholder'), lang);
if (v != null) el.setAttribute('placeholder', v);
});
}
global.I18n = { load, t, apply };
})(window);
// bootstrap
I18n.load().then(() => I18n.apply(localStorage.getItem('lang') || 'en'));
<!-- usage -->
<h1 data-i18n="home.welcome">Welcome</h1>
<input data-i18n-placeholder="filter.searchplaceholder" placeholder="Search…">
<script src="/i18n.js"></script>
Access-Control-Allow-Origin. If you can't enable that, use the build-time export instead and ship a same-origin JSON file.Recipe — build-time export
For static sites or bundlers, snapshot the translations at build time and ship them with your app — no runtime dependency on the manager.
# Whole map (all languages) → commit as one file
curl -s "https://i18n.yourcompany.com/api/translations_i18n.php?format=json" \
-o src/locales/i18n.json
# Or one file per language
for L in en de fr; do
curl -s "https://i18n.yourcompany.com/api/translations_export.php?language=$L&format=json" \
-o "src/locales/$L.json"
done
Wire that curl step into your prebuild/CI so every release picks up the latest translations.
Adding keys from your project
The bundled scanner finds translation keys in your project's markup and scaffolds the import for the manager. Point it at your project root:
# Discover keys in your project not yet in the manager's DB,
# and machine-translate them into every language.
node tools/scan_i18n_keys.js \
--root /path/to/your/project \
--endpoint https://i18n.yourcompany.com/api/translations_i18n.php \
--translate \
--out seeds
This writes seeds/new_keys.json (paste into the manager's Bulk Import tab) and seeds/new_keys.sql (run against the DB). Translators then refine and review the values in the UI.
Caching & cache-busting
- Cache the map in your project (the PHP recipe shows a TTL file cache). The manager sends
Cache-Control: no-store, so caching is your project's responsibility. - Invalidate on publish — bump a version query param (e.g.
?v=2025-06-18) when translations change, or simply rely on the TTL. - Fail soft — always keep the last good copy and fall back to it (and to the fallback language, and finally the key itself) so a missing translation never breaks a page.
Pre-production checklist
- Authentication — the manager has none out of the box. Put it behind auth (reverse-proxy basic auth, SSO, VPN, or IP allow-list) so only translators can write. The read endpoints can stay public if your content is not sensitive.
- Externalize DB credentials in
classes/class_pgsql_data.php(env vars) and rotate any value that was committed. - Dedicated database for the manager; your project reads via the API, not by sharing tables.
- HTTPS on the manager so fetched translations aren't tampered with in transit.
- CORS configured if you use the runtime (JS) model across origins.
Limitations to design around
- Locale codes are 2 letters (
en,de). No regional variants likeen-USorpt-BRwithout a schema change. - No pluralization or variable interpolation — values are plain strings/HTML. If you need
{count} itemsor plural rules, handle that in your own rendering layer on top of the returned strings. - Namespace is the text before the first dot only.
- PostgreSQL backs the manager regardless of your project's database — your project just reads JSON.
Quick reference
| Need | Use |
|---|---|
| All translations, all languages (runtime) | GET /api/translations_i18n.php?format=json |
| Same, as a JS variable | GET /api/translations_i18n.php?format=js |
| One language, downloadable file | GET /api/translations_export.php?language=de&format=json |
| One namespace only | …translations_i18n.php?namespace=nav |
| Discover new keys in your project | node tools/scan_i18n_keys.js --root … --endpoint … |
| Full API reference | Swagger · ReDoc |