Lang Architecture
The package has three layers: LangFactory builds a Lang per adapter, Lang holds the current language, and an adapter turns a key into text.
Shared instance lifecycle
LangFactory::get(?string $adapter = null) is the main entry point.
On each call it:
- registers
LangFactoryin the DI container if needed and resolves it - imports
config/lang.phpwhen thelangconfig key is not present yet - takes the adapter name from the argument or from
lang.default - detects the language from the request
- returns the cached
Langfor that adapter, creating it on the first call
The language is detected on every call but used only when the instance is created. After that, the instance keeps its language until setLang() changes it.
file adapters are created with the language and the lang.file block. deepl and google_translate adapters also get a new HttpClient.
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 reads the first Accept-Language entry and keeps its first two characters, lowercased.
Example: es-ES, en;q=0.8 resolves to es.
Default
lang.default_locale is used as-is. It does not need to appear in supported, because the supported list is only applied to request-derived values. If it is empty too, the factory throws LangException::misconfiguredDefaultConfig().
Lang
Lang stores the current language and the adapter.
setLang()updates both: the reported language and the adapter's languagegetTranslation()asks the adapter- any other method call is forwarded to the adapter, or throws
LangException::methodNotSupported()when the adapter does not have it
File adapter
Translations are loaded on the first get() and kept until flush() or a language change.
Loading order is:
- shared translation files:
shared/resources/lang/<lang>/*.php - current module translation files:
modules/<module>/resources/lang/<lang>/*.php
Each file is required and imported under its basename. That means this structure:
shared/resources/lang/en/messages.phpmodules/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.
When neither location has a file, loading throws LangException::translationsNotFound().
setLang() with a different language clears the loaded translations, so the next lookup loads the new language.
Remote adapters
DeepLAdapter and GoogleTranslateAdapter share RemoteAdapterTrait. A lookup goes through these steps:
- Source text. Without a source catalog, the key with its placeholders filled is the text. With
use_source_catalog, aFileAdapterinsource_localeprovides the text for the key. - Bypass. An empty text is returned as is. With a source catalog, so is a key that has no source text, and any lookup whose language equals
source_locale. - Cache. With
cache.enabled, the answer is looked up in the configured cache adapter underprefix+ a hash of adapter, language, source locale and text. - Request. The text is sent to the provider (
target_langin upper case for DeepL, withsource_lang/sourcewhensource_localeis set). The URL can be overridden with anapi_urlkey in the adapter block. - Store. The translation is cached for
ttlseconds and returned.
Translation lookup contract
With the file adapter, 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). With a remote adapter, the result is the provider's translation of the source text described above.
