Appearance
Multi-Language (MUI) Translation Pattern
Flow ID: SY-23 | Type: Shared Pattern | Complexity: Medium Last Updated: 2026-05-12
Business Overview
The MUI (Multi-User Interface / Multi-Language) pattern is the platform's standard approach for storing and retrieving translated content. Every entity that needs localized text -- product names, category descriptions, badge labels, SEO metadata, blog articles -- follows the same companion-table pattern: a master table holds language-independent data, and a _mui companion table holds one row per language per master record.
This pattern is used by 38+ domain entities across the codebase, spanning both the legacy HMVC layer and the modern Domain/REST layer. Any admin flow that manages translatable content relies on MUI.
Why a shared pattern doc: Rather than repeating the MUI mechanics in every flow document, individual flow docs (AD-02, AD-05, AD-25, etc.) reference this document for the translation lifecycle details.
Architecture
Database Schema Convention
Every MUI-enabled entity follows the same two-table structure:
shop_product_badges (master table)
id PK
name language-independent field
badge language-independent field
shop_product_badges_mui (companion table)
id PK
badge_id FK -> shop_product_badges.id
badge_text translated field
lang VARCHAR(2), language code (e.g. 'el', 'en')Naming rules:
- Companion table name =
{master_table}_mui - Foreign key column name =
{singular_entity}_id(e.g.badge_id,category_id,product_id) - The
langcolumn stores ISO 639-1 two-letter language codes - Composite index on
({fk_column}, lang)for efficient lookups
Field distribution: Language-independent data (IDs, prices, flags, sort orders, dates, foreign keys, image filenames) stays in the master table. Translated text (names, descriptions, slugs, SEO metadata, URLs) goes in the _mui table.
Entities With Complex MUI Tables
Simple entities like Badges have only one translated field (badge_text). Content-rich entities have many:
| Entity | MUI Fields |
|---|---|
| Product | name, slug, description, short_description, ingredients, usage, extra_description, meta_title, meta_keywords, meta_description, url, redirect_url, video |
| Category | name, slug, fulltext, extra_description, meta_title, meta_keywords, meta_description, url, redirect_url |
| Blog Article | slug, title, description, small_description, meta_title, meta_keywords, meta_description, url, redirect_url, builder_block_id |
| Badge | badge_text |
| Subcontent | name, fulltext |
Implementation Layers
The MUI pattern is implemented in two parallel stacks that share the same database tables:
+-----------------------+
| Database Tables |
| master + _mui tables |
+-----------+-----------+
|
+-----------------+------------------+
| |
Legacy HMVC Layer Modern Domain + REST Layer
(ecommercen/) (src/)
| |
Adv_base_model BaseRepository
updateOrInsertMui() + MuiRepository
masterWithDetails() + MuiWriteRepository
| + MuiWriteData
Legacy Models + MuiEntity
(per-entity) + WriteService
| |
Legacy Controllers REST Controllers
(admin CRUD views) (JSON API)Legacy Layer (HMVC)
Base Model Methods
Adv_base_model (ecommercen/core/models/Adv_base_model.php) provides two methods used by all legacy MUI models:
updateOrInsertMui($table, $masterField, $masterValue, $lang, $data) -- Upsert pattern for a single language row. Checks if a row exists for the given master ID + language; updates if found, inserts if not. Used during updates to handle languages being added after initial creation.
masterWithDetails($master, $details) -- Merges a master record with its MUI rows into a single object with a captions property keyed by language abbreviation. Used when loading records for admin editing forms.
Legacy CRUD Lifecycle
Create:
php
// In model (e.g. Adv_categories_model)
function add_record($data, $data_mui) {
$this->db->insert($this->table, $data);
$id = $this->db->insert_id();
foreach ($data_mui as $lang => $q) {
$q['category_id'] = $id;
$this->db->insert($this->tableMui, $q);
}
return $id;
}Update:
php
function update_record($data, $data_mui, $id) {
$this->db->where('id', $id)->update($this->table, $data);
foreach ($data_mui as $lang => $q) {
$q['lang'] = $lang;
$this->updateOrInsertMui($this->tableMui, 'category_id', $id, $lang, $q);
}
}Read (admin edit form):
php
function getAdminRecord($id) {
$master = $this->db->from($this->table)->where('id', $id)->get()->row();
$muiRecs = $this->db->from($this->tableMui)->where('badge_id', $id)->get()->result();
foreach ($muiRecs as $row) {
$master->{$row->lang} = $row; // Keyed by language on master object
}
return $master;
}Read (storefront list):
php
// JOIN to _mui with current language filter
$this->db->select("{$this->table}.*, {$this->tableMui}.badge_text")
->join($this->tableMui, "{$this->table}.id = {$this->tableMui}.badge_id")
->where("{$this->tableMui}.lang", $this->languageAbbr);Delete:
php
function deleteRecord($id) {
$this->db->delete($this->table, ['id' => $id]);
$this->db->delete($this->tableMui, ['badge_id' => $id]);
}Language Configuration
Languages are configured in application/config/languages.php:
| Config Key | Purpose | Example |
|---|---|---|
language_abbr | Current request language | 'el' |
lang_uri_abbr | All available storefront languages | ['el' => 'greek', 'en' => 'english'] |
lang_desc | Display labels for storefront | ['el' => 'EL', 'en' => 'EN'] |
admin_languages | Languages available in admin panel | ['el' => 'EL', 'en' => 'EN'] |
Helper functions:
get_languages()-- returnslang_desc(storefront languages)getAdminLanguages()-- returnsadmin_languages(admin panel languages)config_item('lang_uri_abbr')-- used by the modern layer'sHasLocalestrait
Modern Domain Layer
File Structure Per Entity
Each MUI-enabled domain entity produces these files (using Badge as example):
src/Domains/Product/Badge/
Repository/
Entity.php # Master entity
Repository.php # Master read repository
RepositoryConfigurator.php # Declares 'translations' relation
MuiEntity.php # Translation entity
MuiRepository.php # Translation read repository
WriteRepository.php # Master write repository
MuiWriteRepository.php # Translation write repository
Service.php # Read service
WriteService.php # Write service (CRUD with MUI)
WriteData.php # Master field DTO
MuiWriteData.php # Translation field DTO
Validator.php # Validation for master + MUI
ListRequest.php # Filter/sort definitionsEntity
The master entity implements FilterTranslation and uses the FiltersTranslation trait to allow extracting a specific language's translation at runtime:
php
class Entity extends BaseEntity implements FilterTranslation
{
use FiltersTranslation;
public ?string $repository = Repository::class;
}The translation() method searches the loaded translations relation by language code:
php
// Get the Greek translation for a product
$greekTranslation = $product->translation('el');
// Returns the MuiEntity for lang='el', or nullMuiEntity
A simple BaseEntity subclass mapping the _mui table columns:
php
/**
* @property int $id
* @property int $badge_id
* @property string $badge_text
* @property string $lang
*/
class MuiEntity extends BaseEntity
{
public ?string $repository = MuiRepository::class;
}MuiRepository (Read)
Extends BaseRepository pointing at the _mui table. Has its own RelationConfigurator -- usually NullRelationConfigurator (no sub-relations), but some entities like Cms\Page have MUI-level relations (e.g. builder):
php
class MuiRepository extends BaseRepository
{
protected string $table = 'shop_product_badges_mui';
protected string $entity = MuiEntity::class;
public function __construct(
RelationConfigurator $configurator,
?CI_DB_query_builder $db = null,
) {
parent::__construct($configurator, $db);
}
}The $db = null default triggers the di()->get(CI_DB_query_builder::class) fallback inside BaseRepository::__construct() (src/Domains/Support/Repository/BaseRepository.php:24-28), so subclasses that only declare RelationConfigurator continue to work unchanged -- the change is fully backward-compatible.
RepositoryConfigurator (Relation Wiring)
The master entity's configurator declares the translations relation as ONE_TO_MANY pointing to the MuiRepository:
php
class RepositoryConfigurator implements RelationConfigurator
{
public function getRelations(): array
{
return [
'translations' => new Relation(
Relation::ONE_TO_MANY,
MuiRepository::class,
'badge_id', // FK column in the _mui table
scopableColumn: 'lang'
),
];
}
}This enables ?with=translations (all locales) and ?with=translations[el] (scoped to one locale) on REST API calls, and $repository->get($id, ['translations']) in code. See Per-Relation Language Scoping for the full URL grammar and data-flow details. The scopableColumn: 'lang' named argument opts this relation into locale scoping; relations without it are never filtered regardless of request params.
MuiWriteData (DTO)
Immutable value object for translation input. Accepts both snake_case (DB columns) and camelCase (API) field names. Annotated with OpenAPI for API documentation:
php
class MuiWriteData
{
public function __construct(
public readonly string $lang,
public readonly ?string $badgeText = null
) {}
public static function fromArray(array $data): self { /* dual-format parsing */ }
public function toArray(bool $excludeNull = false): array { /* snake_case output */ }
}Category B field handling (null → ''): Some _mui columns are NOT NULL with no DEFAULT value and no required validation — for example, url, image, and descr on slide_mui, or name and fulltext on categories_mui (Cms\Page). When the API consumer omits these fields, the constructor sets them to null. Because BaseWriteRepository::insert() now strips null values (see below), omitting a Category B column would cause a MySQL error. To prevent this, toArray() in the affected MuiWriteData files converts null → '' for these columns:
php
// src/Domains/Slider/Slide/MuiWriteData.php (representative example)
'url' => $this->url ?? '',
'image' => $this->image ?? '',
'descr' => $this->descr ?? '',This matches the legacy admin panel's behavior, where $this->input->post() always returned '' for empty form fields. The nine affected DTOs and their Category B fields are:
| File | Fields |
|---|---|
src/Domains/Slider/Slide/MuiWriteData.php | url, image, descr |
src/Domains/Cms/Page/MuiWriteData.php | name, fulltext |
src/Domains/Cms/Document/MuiWriteData.php | file |
src/Domains/Cms/Subcontent/MuiWriteData.php | name, fulltext |
src/Domains/Map/Location/MuiWriteData.php | title |
src/Domains/Map/Map/MuiWriteData.php | title |
src/Domains/Product/ProductList/MuiWriteData.php | description |
src/Domains/Product/Line/MuiWriteData.php | description |
src/Domains/Product/Promo/MuiWriteData.php | description |
MuiWriteRepository (Write)
Extends BaseWriteRepository with three standard methods. The FK column name is the only thing that varies between entities:
| Method | Purpose | Strategy |
|---|---|---|
insertForEntity($id, $translations) | Create translations for a new master record | Loop + $this->insert() with FK (null-filtered) |
replaceForEntity($id, $translations) | Update translations for an existing master record | DELETE all + re-INSERT |
deleteForEntity($id) | Remove all translations when master is deleted | DELETE WHERE fk = id |
insertForEntity() delegates to $this->insert() (inherited from BaseWriteRepository) rather than calling $this->db->insert() directly. This means every MUI row insert goes through the null-filtering step in BaseWriteRepository::insert() (see step 6 of the Create flow below). The replaceForEntity strategy (delete-all + re-insert) ensures clean handling of language additions, removals, and field changes without partial-update complexity.
WriteService (CRUD Orchestration)
The WriteService orchestrates master and MUI operations within a database transaction:
Create flow:
1. Parse master fields -> WriteData::fromArray()
2. Validate master fields -> Validator::validateForCreate()
3. Parse translations -> MuiWriteData::fromArray() per language
4. Validate each translation -> Validator::validateTranslations()
5. BEGIN TRANSACTION
6. INSERT master row -> WriteRepository::insert()
BaseWriteRepository::insert() strips null values via array_filter()
before the DB call; MySQL applies column DEFAULTs for omitted columns.
-> returns $id
7. INSERT MUI rows -> MuiWriteRepository::insertForEntity($id, $translations)
Each translation row also goes through BaseWriteRepository::insert(),
inheriting the same null filtering. Category B fields that are NOT NULL
with no DEFAULT are coerced to '' in MuiWriteData::toArray() before
this point (see MuiWriteData section above).
8. Reload entity with translations -> Repository::get($id, ['translations'])
9. COMMIT
10. Return complete entityUpdate flow:
1. Parse master fields -> WriteData::fromArray()
2. Validate master fields -> Validator::validateForUpdate()
3. Parse translations -> MuiWriteData::fromArray() per language
4. Validate each translation -> Validator::validateTranslations()
5. BEGIN TRANSACTION
6. UPDATE master row (non-null fields only) -> WriteRepository::update()
7. REPLACE MUI rows -> MuiWriteRepository::replaceForEntity($id, $translations)
8. Reload entity with translations -> Repository::get($id, ['translations'])
9. COMMIT
10. Return complete entityDelete flow:
1. BEGIN TRANSACTION
2. DELETE MUI rows first -> MuiWriteRepository::deleteForEntity($id)
3. DELETE master row -> WriteRepository::delete($id)
4. COMMITKey detail: on update, translations are only replaced if data['translations'] is present in the input. If the key is absent, MUI rows are left untouched. This allows updating only master fields without affecting translations.
Validator
Each entity's Validator has a validateTranslations() method that checks the MUI-specific rules. At minimum, lang is required on every translation:
php
public function validateTranslations(array $translations): void
{
foreach ($translations as $i => $translation) {
if (empty($translation->lang)) {
$errors["translations.$i.lang"] = 'Language is required for each translation.';
}
}
}Entities with slugs or names may add additional required-field checks.
REST API Layer
Resources
Each MUI entity has a MuiResource and MuiCollection pair that transforms DB rows to camelCase JSON:
php
// MuiResource -- transforms one translation row
class MuiResource extends BaseResource {
protected function resource(): array {
return [
'id' => $this->resource->id,
'badgeId' => $this->resource->badge_id,
'badgeText' => $this->resource->badge_text ?? '',
'lang' => $this->resource->lang ?? 'en',
];
}
}
// MuiCollection -- wraps an array of MuiResource
class MuiCollection extends BaseCollection {
public string $collects = MuiResource::class;
}Master Resource Integration
The master Resource includes translations via addCollectionToData():
php
class Resource extends BaseResource {
protected function resource(): array {
return $this->addRelationToData([
'id' => $this->resource->id,
'name' => $this->resource->name,
]);
}
protected function addRelationToData(array $data): array {
return $this->addCollectionToData($data, 'translations', MuiCollection::class);
}
}Translations are included in the response only when the relation is loaded (via ?with=translations).
API Request/Response Format
Read (GET with translations):
GET /rest/product/badge/1?with=translations
Response:
{
"success": true,
"data": {
"id": 1,
"name": "New Product",
"badge": "/files/badges/new.png",
"translations": [
{"id": 10, "badgeId": 1, "badgeText": "Νέο Προϊόν", "lang": "el"},
{"id": 11, "badgeId": 1, "badgeText": "New Product", "lang": "en"}
]
}
}Create (POST with translations):
POST /rest/product/badge
Content-Type: application/json
{
"name": "Sale",
"badge": "sale.png",
"translations": [
{"lang": "el", "badgeText": "Εκπτωση"},
{"lang": "en", "badgeText": "Sale"}
]
}Update (POST with translations):
POST /rest/product/badge/1
Content-Type: application/json
{
"name": "Updated Name",
"translations": [
{"lang": "el", "badgeText": "Updated Greek"},
{"lang": "en", "badgeText": "Updated English"}
]
}Note: Omitting "translations" from the update payload leaves existing translations untouched. Including it replaces all translations (delete + re-insert).
Per-Relation Language Scoping
A REST caller can request only a subset of locale rows by appending bracket-enclosed locale codes to a relation name. The feature is opt-in at the RepositoryConfigurator level via scopableColumn: 'lang'; relations that do not declare this field are never filtered.
URL grammar (src/Domains/Support/Request/WithParser.php):
| URL parameter | Effect |
|---|---|
?with=translations | Load all locales (unchanged behavior) |
?with=translations[el] | Load Greek only |
?with=translations[el,en] | Load Greek and English (CSV inside brackets) |
?with=translations[el],parent | Scoped translations + unscoped parent (, outside brackets is the relation separator) |
?with=children.translations[el] | Nested relation; leaf is scoped; relationParams key is the full dot-path children.translations |
Opt-in declaration (src/Domains/Support/Repository/Relation.php):
php
public ?string $scopableColumn;Each RepositoryConfigurator that participates declares scopableColumn: 'lang' on the Relation constructor call (see RepositoryConfigurator (Relation Wiring) above). All 38 translation-bearing configurators follow this pattern.
Parser: WithParser::parse(string $with): array is a static helper. It returns:
php
[
'relations' => ['translations' => [], 'parent' => [], ...],
'relationParams' => ['translations' => ['el'], 'children.translations' => ['el']],
]The bracket-aware top-level splitter treats , inside [...] as a value separator and , outside as a relation separator.
Data flow through the stack:
- URL parsed by
WithParser::parse(), called fromGenerateListRequest::generateRelationParams()(src/Domains/Support/Request/QueryListBuilder/GenerateListRequest.php) ListRequest::$relationParamscarries the resulting map (src/Domains/Support/Request/QueryListBuilder/ListRequest.php)HandlesRestfulActions::show()passes$listRequest->relationParamsas the 4th argument toService::get()(src/Rest/Support/Controllers/HandlesRestfulActions.php)Service::get()forwards it toBaseRepository::get(), which callsloadRelations($resultAsArray, $relations, $relationSorts, '', $relationParams)(src/Domains/Support/Repository/BaseRepository.php)BaseRepository::loadRelations()extracts$params = $relationParams[$relationPath] ?? []and forwards toloadRelation()and then to the loaderAbstractRelationLoader::applyScopableParams()(src/Domains/Support/Repository/RelationLoader/AbstractRelationLoader.php): when$paramsis non-empty and$relation->scopableColumnis set, emitsWHERE {table}.{scopableColumn} IN (...)on the related repository's DB builder- The collection in the response contains only the scoped rows; the array shape is unchanged (still an array even for a single locale)
For index() and item() endpoints, HandlesRestfulActions::index() uses $listRequest (which already carries relationParams) → Service::all() → buildSpecifications() → new WithRelations($relations, $listRequest->getRelationSorts(), $listRequest->relationParams) — the same scoping applies.
For internal callers (jobs, services, legacy bridges) pass $relationParams directly:
php
// Repository::get()
$repo->get($id, ['translations'], [], ['translations' => ['el']]);
// Repository::loadRelations()
$repo->loadRelations($items, ['translations'], [], '', ['translations' => ['el']]);
// WithRelations specification
new WithRelations(['translations' => []], [], ['translations' => ['el']]);Backward compatibility: an empty $relationParams = [] (the default for all existing callers) produces no IN (...) filter, so all locales are returned — identical behavior to before the feature was introduced.
OpenAPI: all 38 translation-bearing controllers' x-relations entries carry 'scopableColumn' => 'lang'. The canonical documentation is in the central OA\Info ## Relations block in src/Rest/OpenApi.php.
Filtering and Sorting by Translation Fields
The ListRequest class registers per-locale filter and sort entries for MUI fields using the HasLocales trait:
php
// In ListRequest::setAllowedFilters()
foreach ($this->getLocales() as $locale) {
$this->allowedFilters["name.$locale"] = [
'relation' => 'translations',
'field' => 'shop_product_mui.name',
'type' => FilterRequestType::Partial,
'language' => $locale,
];
}API usage: GET /rest/product?filter[name.el]=Προϊόν&sort=name.el
Sort by translation: Uses a SortByTranslation specification that LEFT JOINs the _mui table filtered by locale, then orders by the translated field:
php
class SortByTranslation implements Specification {
public function apply(CI_DB_query_builder $builder) {
$builder->join("{$muiTable} AS {$alias}", "{$alias}.{$fk} = {$table}.id AND {$alias}.lang = '{$locale}'", 'left');
$builder->order_by("{$alias}.{$field}", $this->direction);
}
}Filter by translation: Uses a FilterByTranslation specification with a subquery:
php
// Generates: WHERE shop_product.id IN (SELECT product_id FROM shop_product_mui WHERE lang = 'el' AND name LIKE '%term%')DI Container Registration
MUI components are registered in each domain's container.php. The pattern differs based on whether the MuiRepository has its own relations:
Without MUI-level relations (most entities):
php
$services->set(Badge\Repository\MuiRepository::class)
->arg('$configurator', new Reference(NullRelationConfigurator::class));
$services->set(Badge\Repository\MuiWriteRepository::class);With MUI-level relations (e.g. Product, Page):
php
$services->set(Product\Repository\MuiRepositoryConfigurator::class);
$services->set(Product\Repository\MuiRepository::class)
->arg('$configurator', new Reference(Product\Repository\MuiRepositoryConfigurator::class));
$services->set(Product\Repository\MuiWriteRepository::class);Slug and URL Handling
Entities with public-facing URLs (products, categories, blog articles, pages, events, offers, videos) store slug and optionally url and redirect_url in the _mui table. This means each language can have its own URL-friendly slug.
Slug generation is handled by the url_title() helper (application/helpers/MY_url_helper.php), which extends CI's default with full Greek character support -- converting accented Greek characters to their base forms, lowercasing via mb_strtolower, and replacing spaces with hyphens.
Slug generation typically happens in the admin frontend (Vue components or form handlers) rather than in the backend model layer. The backend stores whatever slug value is submitted.
Complete Entity Inventory
All 38 entities currently using the MUI pattern:
| Context | Entity | MUI Table | Notable MUI Fields |
|---|---|---|---|
| Ai | Position | ai_positions_mui | text fields |
| Cms | Blog Article | blog_mui | slug, title, description, SEO |
| Cms | Blog Author | blog_author_mui | name, slug, SEO |
| Cms | Blog Category | blog_category_mui | name, slug, SEO |
| Cms | Blog Tag | blog_tags_mui | name, slug, SEO |
| Cms | Document | documents_mui | name, slug |
| Cms | Offer | offers_mui | name, slug, description, SEO |
| Cms | Offer Category | offers_category_mui | name, slug, SEO |
| Cms | Page | categories_mui | name, slug, fulltext, SEO |
| Cms | Subcontent | cms_subcontent_mui | name, fulltext |
| Cms | Video | video_mui | name, slug, description, SEO |
| Country | Country | country_mui | name |
| Country | County | county_mui | name |
| Event | Category | event_categories_mui | name, slug, description, SEO |
| Event | Event | events_mui | name, slug, description, SEO |
| Map | Location | maps_locations_mui | name, description, address |
| Map | Map | maps_mui | name, description |
| Order | Store | stores_mui | name, address |
| Product | Attribute | shop_product_attribute_values_mui | name |
| Product | Attribute Group | shop_product_attribute_groups_mui | name |
| Product | Badge | shop_product_badges_mui | badge_text |
| Product | Category | shop_product_category_mui | name, slug, fulltext, SEO |
| Product | Line | shop_product_lines_mui | name, slug, SEO |
| Product | Product | shop_product_mui | name, slug, description, SEO, usage, ingredients |
| Product | Product List | shop_product_lists_mui | name, slug, description, SEO |
| Product | Promo | shop_product_promo_mui | name, slug, description, SEO |
| Product | Related Group | shop_product_related_groups_mui | name |
| Product | Supplier | shop_product_supplier_mui | name, slug |
| Product | Tag | shop_product_tags_mui | name, slug |
| Product | Tag Category | shop_product_tag_categories_mui | name, slug |
| Product | Vendor | shop_vendor_mui | name, vendor_slug, SEO |
| Product | Variation | shop_product_variations_mui | name |
| Product | Variation Group | shop_product_variation_groups_mui | name |
| Product | Variation Value | shop_product_variation_values_mui | name |
| Promotion | Gift | shop_gift_rules_mui | title, description, SEO |
| Slider | Slide | slide_mui | title, description, link |
| Slider | Slider | sliders_group_mui | name |
| Transporter | Transporter | shop_transporters_mui | name |
Comparison: Legacy vs. Modern
| Aspect | Legacy (HMVC) | Modern (Domain/REST) |
|---|---|---|
| MUI save strategy (update) | updateOrInsertMui() per language (upsert) | replaceForEntity() (delete-all + re-insert) |
| MUI save strategy (create) | Loop INSERT per language | insertForEntity() loop INSERT |
| Transaction handling | No explicit transactions | transactional() wrapper |
| MUI data structure on read | $master->{$lang} = $row (keyed by lang) | $entity->translations[] array of MuiEntity |
| Filtering by language | JOIN + WHERE lang | Subquery or JOIN via Specification |
| Input format | $_POST form data keyed by language | JSON translations[] array |
| Validation | CI form_validation | Dedicated Validator class |
| Slug generation | Frontend / url_title() helper | Frontend / API consumer |
Known Issues & Security Gaps
fromArray()duplicate-key lookups in 7 MuiWriteData files — The snake_case fallback to camelCase infromArray()is silently broken in 7 files: both sides of the??operator use the same array key (e.g.$data['url'] ?? $data['url'] ?? null), meaning the camelCase variant never fires. Affected:src/Domains/Slider/Slide/MuiWriteData.php:46-55,src/Domains/Map/Location/MuiWriteData.php:36-38,src/Domains/Map/Map/MuiWriteData.php:30-31,src/Domains/Product/ProductList/MuiWriteData.php:30-31,src/Domains/Product/Line/MuiWriteData.php:41-43,src/Domains/Product/Promo/MuiWriteData.php:42-46,src/Domains/Cms/Document/MuiWriteData.php:30-31. These fields only accept snake_case input from the API despite the dual-format design intent.
Related Flows
- AD-26 Language Management -- admin UI for managing languages and locale configuration
- CF-02 Product Detail -- multi-language product names, descriptions, and slugs
- SY-30 Client Features Reference -- per-client MUI customization patterns
References
- Base Model:
ecommercen/core/models/Adv_base_model.php--updateOrInsertMui(),masterWithDetails() - Support Classes:
src/Domains/Support/Repository/BaseRepository.php,BaseWriteRepository.php - Relation System:
src/Domains/Support/Repository/Relation.php,RelationConfigurator.php - With/Scoping Parser:
src/Domains/Support/Request/WithParser.php--parse()static helper, bracket-aware grammar - REST Relation Helpers:
src/Domains/Support/Request/GetsRequestRelations.php-- trait that wiresWithParserinto REST controllers - Translation Filtering:
src/Domains/Support/Model/FilterTranslation.php,FiltersTranslation.php - Specification Factories:
src/Domains/Support/Repository/Specification/NotEmptySpecificationFactory.php - REST Support:
src/Rest/Support/Resources/BaseResource.php--addCollectionToData() - Language Config:
application/config/languages.php - Language Helpers:
application/helpers/MY_language_helper.php - Slug Helper:
application/helpers/MY_url_helper.php--url_title()with Greek support - Container Pattern:
src/Domains/Product/container.php(reference for MUI DI wiring)