Multilingual Without a Plugin: How i18n Works in Synaptik CMS

Most flat-file and lightweight CMS platforms treat multilingual support as an afterthought — something you bolt on with a plugin that duplicates your content, adds its own settings page, and slows every request down with an extra layer of lookups. Synaptik CMS takes a different approach: internationalization is built into the core, split cleanly between the admin panel and the public site, and designed to add zero measurable overhead once cached.

Here's exactly how it works.

Two separate language tracks

Synaptik CMS distinguishes between the language your visitors see and the language you work in as an administrator. These are independent settings:

  • active_language in settings.json controls the front-end — what your visitors see.
  • admin_language controls the admin panel interface, and falls back to active_language if not explicitly set.

This matters in practice: you can run a French-language public site while working in an English-language admin panel, or the reverse. Nothing forces the two to match.

Where translations live

Locale files are plain JSON, split by scope:

ScopePathPurpose
Front-end/lang/front/{locale}.jsonStrings shown to site visitors
Admin/lang/admin/{locale}.jsonStrings shown in the admin panel

Out of the box, Synaptik CMS ships English, French, and Spanish for both scopes. Each file is a flat key-value map:

{
  "_meta": {
    "language": "English",
    "locale": "en",
    "author": "",
    "version": "1.0"
  },
  "home": "Home",
  "read_more": "Read more",
  "search_placeholder": "Enter your search term..."
}

The _meta block isn't shown to visitors — it's what populates the language name in the admin's language dropdown, and it's stripped out before the strings are sent to the browser.

Adding a new language

There's no interface for machine-translating a new language automatically — and honestly, that's on purpose. Auto-translated UI strings are a common source of the awkward, slightly-wrong copy that makes a multilingual site feel unpolished. Adding a language is a deliberate three-step process:

  1. Copy lang/front/en.json to lang/front/{locale}.json and translate every value.
  2. Set active_language (and optionally admin_language) to the new locale code in Admin → Settings.

Every user-facing string in Synaptik CMS's core — front-end and admin — goes through this system. There are no hardcoded English strings hiding in a template waiting to break your translation.

How it stays fast: the OPcache-friendly cache layer

This is the part that matters if you care about page load times, and you should.

Reading and decoding a JSON file on every single request is wasteful — it means a file_get_contents() and a json_decode() call before a single string can be displayed. Synaptik CMS avoids this by compiling each locale's JSON into a plain PHP file the first time it's requested:

<?php
/**
 * Auto-generated cache — DO NOT EDIT MANUALLY.
 */
return ['home' => 'Home', 'read_more' => 'Read more', /* ... */];

Because this is a native PHP array literal, PHP's OPcache compiles it to bytecode and keeps it in shared memory. Every subsequent request that calls __t('read_more') reads the string straight from RAM — no disk I/O, no JSON parsing, ever, until the source file changes.

Cache invalidation is automatic: the CMS compares the cache file's modification time against both the source .json file and settings.json. Edit a translation, and the very next request regenerates the cache — you never touch a "clear cache" button.

Using translations in templates

Two functions cover essentially every use case:

$label = __t('read_more');   // Returns the translated string
_e('read_more');             // Echoes it directly

Both fall back gracefully — if a key is missing from the active locale, __t() returns the fallback text you pass as a second argument, or the raw key itself if you don't pass one. A missing translation degrades to visible-but-ugly, never to a fatal error or a blank string.

For strings that need JavaScript (search UI, gallery lightbox labels), the same translation table is exposed via lang_js_bridge(), injected once in header.php as window.CMS_LANG. No second translation system, no duplicated strings — the front-end JavaScript reads from the same JSON source the PHP does.

URL slugs are localized too

Internationalization in Synaptik CMS isn't just UI copy — routing itself is locale-aware. Content type URL prefixes are defined per locale:

"url_slug_article":  "article",
"url_slug_category": "category"

A French locale file can define these as article and categorie, and every URL Synaptik CMS generates — via cleanUrl() in templates, in the admin panel, and in the router that parses incoming requests — respects that mapping automatically. You don't maintain a separate routing table for each language; the same routing logic reads whichever locale is active.

Why no database-backed translation system

A common pattern in database-driven CMS platforms is to store every translatable string as a row, with a language column, queried at render time. It works, but it means every page load costs at least one extra query per translatable region, and usually several.

Synaptik CMS's flat-file, cache-compiled approach trades that flexibility (no live translation editor with a WYSIWYG "translate this page" button) for something we think matters more for a lightweight CMS: translation lookups that cost nothing beyond what PHP itself already costs to run. If you're building a two- or three-language site — a personal blog, a small business site, a portfolio — this is the right tradeoff.

If you need a fully dynamic, per-content multilingual system where the content itself (not just the UI) is translated across multiple languages with its own editorial workflow, that's a different feature entirely — and one we're evaluating separately from the UI-level i18n system described here.