Lang Architecture

The Lang package uses a small factory-plus-translator model.

Shared instance lifecycle

LangFactory::get() is the main entry point.

On first use it:

  1. registers LangFactory in the DI container if needed
  2. resolves the factory from DI
  3. memoizes one Lang instance inside the factory

Repeated calls return the same Lang object until the container or factory state is reset.

Config loading

The factory lazily imports config/lang.php when the lang config key is not already present.

It expects these settings:

  • enabled
  • supported
  • default
  • url_segment

If no usable language can be detected and default is empty, the factory throws LangException::misconfiguredDefaultConfig().

The default value is used as-is. It does not need to appear in supported, because the supported list is only applied to request-derived values.

Detection details

Query parameter

?lang=<value> has highest priority.

URL segment

The URL strategy reads request()->getSegment((int) config()->get('lang.url_segment')).

There is one routing-specific adjustment: when the current route has a prefix and lang.url_segment is 1, the factory increments the segment index to 2. That keeps the language lookup aligned with prefixed routes.

Header fallback

Header-based detection accepts only the primary language from the first Accept-Language entry, normalized to a two-letter code.

Example: es-ES, en;q=0.8 resolves to es.

Translation loading model

Translation files are loaded once per language instance and reused until you explicitly call flush().

The enabled flag stored on Lang is informational at this layer. The factory still creates the service either way; the web bootstrap decides whether load() is called automatically.

Loading order is:

  1. shared translation files
  2. current module translation files

Both locations are scanned with *.php globbing. Each file is required and imported under its basename.

That means this structure:

  • shared/resources/lang/en/messages.php
  • modules/Blog/resources/lang/en/messages.php

loads both files into the messages namespace. Because module files are imported after shared files, later imports can replace or extend earlier values for the same file/key paths.

Translation lookup contract

t($key, $params) behaves like this:

  • if the key exists, return the translated string
  • if params are passed, apply placeholder substitution
  • if the key does not exist, return the original key

Missing keys fall back to the key string (not null) in normal lookup flow.

Stateful caveat: reported lang vs translator lang

Lang stores the current language string and the Translator as separate state.

  • setLang() updates only Lang::$currentLang
  • the existing Translator keeps the language string it was constructed with

So this is not a full language switch:

$lang = LangFactory::get();
$lang->setLang('es');

After that, current_lang() reports es, but already-wired translation loading still points at the translator's original language unless you rebuild the Lang instance.

The same caveat applies to flush() followed by load(): that reload cycle clears cached translations, but it still reloads the translator's original language.