yasser-elgammal/green-locale

Localized model attributes for Green Framework Web & API.

Maintainers

Package info

github.com/YasserElgammal/green-locale

pkg:composer/yasser-elgammal/green-locale

Statistics

Installs: 0

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v1.0.0 2026-05-31 13:30 UTC

This package is not auto-updated.

Last update: 2026-05-31 13:40:54 UTC


README

This package provides Localization for Green Framework.

Localized JSON attributes for Green Framework models.

green-locale lets a model store translated attributes such as name, description, or slug in a single JSON column, then read the right value for the current locale with explicit model methods.

Contents

Requirements

  • PHP ^8.2
  • Green Framework skeleton
  • Doctrine DBAL, already used by Green
  • Respect Validation, used for the package validation rules

Installation

Install it normally:

composer require yasser-elgammal/green-locale

Configuration

Create config/locale.php in your Green skeleton:

<?php

return [
    'default_locale' => $_ENV['APP_LOCALE'] ?? 'en',
    'fallback_locale' => $_ENV['APP_FALLBACK_LOCALE'] ?? 'en',

    'available_locales' => [
        'en',
        'ar',
    ],

    'resolver' => ['query', 'session', 'request', 'config'],

    'strict' => false,
];

Config Options

default_locale

The locale used when no resolver finds a locale.

fallback_locale

The locale used when a translated attribute does not have a value for the current locale.

available_locales

The list of locales your application supports.

resolver

The locale resolver driver or resolver chain. Resolvers are executed in the exact order they are defined in the array (from left to right). The first resolver that returns an available locale wins.

Supported drivers (and their typical priority):

  1. query: Highest priority. Reads the locale from the URL (e.g., ?locale=ar).
  2. session: Reads session()->get('locale') or $_SESSION['locale'] if the user changed their preference previously.
  3. request: Reads the Accept-Language HTTP header to detect the browser's preferred language.
  4. config: Lowest priority. Returns the default_locale if no other method succeeds.

strict

When false, missing locale values return the fallback value or null.

When true, missing locale values throw MissingLocaleValueException.

Bootstrapping

Boot the package once during application startup.

In public/index.php, after loading Composer and defining BASE_PATH, add:

use YasserElgammal\GreenLocale\LocaleServiceProvider;

LocaleServiceProvider::boot();

Example:

require_once __DIR__ . '/../vendor/autoload.php';

define('BASE_PATH', realpath(__DIR__ . '/../'));

$dotenv = Dotenv\Dotenv::createImmutable(BASE_PATH);
$dotenv->safeLoad();

use YasserElgammal\GreenLocale\LocaleServiceProvider;

LocaleServiceProvider::boot();

Database Columns

Localized attributes are stored as JSON strings in normal database columns.

Example stored value for name:

{
    "en": "Phone",
    "ar": "هاتف"
}

Model Setup

Use HasLocales on any model that has localized JSON attributes.

<?php

namespace App\Models;

use YasserElgammal\Green\Database\Model;
use YasserElgammal\GreenLocale\Concerns\HasLocales;

class Product extends Model
{
    use HasLocales;

    protected string $table = 'products';
    protected string $primaryKey = 'id';

    protected array $localeAttributes = [
        'name',
        'description',
        'slug',
    ];
}

Only attributes listed in $localeAttributes can be used with locale methods. Calling locale methods on undeclared attributes throws LocaleAttributeNotDeclared.

Reading Locale Values

Read the value for the current locale:

$product->localeValue('name');

Read a specific locale:

$product->localeValue('name', 'ar');

Read all locale values:

$product->localeValues('name');

Check if a locale value exists and is not empty:

$product->hasLocaleValue('name', 'en');

Fallback behavior:

// Current locale: ar
// Fallback locale: en
// Stored: {"en": "Phone"}

$product->localeValue('name'); // "Phone"

Writing Locale Values

Set one locale value:

$product->putLocaleValue('name', 'ar', 'هاتف');

Set multiple values and replace the whole locale map:

$product->syncLocaleValues('name', [
    'en' => 'Phone',
    'ar' => 'هاتف',
]);

Remove one locale value:

$product->forgetLocaleValue('name', 'ar');

All writing methods return $this, so chaining is supported:

$product
    ->putLocaleValue('name', 'en', 'Phone')
    ->putLocaleValue('name', 'ar', 'هاتف');

Persisting Model Changes

Green models are DTO-style objects. Calling putLocaleValue() updates the model attribute in memory. You still need to persist the changed attribute through your table gateway.

$table = new ProductTable();
$product = $table->fetchById($id);

$product->putLocaleValue('name', 'ar', 'هاتف');

$table->update($id, [
    'name' => $product->getAttribute('name'),
]);

Full controller example:

use YasserElgammal\Green\Http\Request;
use YasserElgammal\Green\Http\JsonResponse;

#[Route('POST', '/api/products/{id}/locale')]
public function updateLocale(Request $request, int $id): JsonResponse
{
    $table = new ProductTable();
    $product = $table->fetchById($id);

    $product->putLocaleValue('name', $request->input('locale'), $request->input('name'));

    $table->update($id, [
        'name' => $product->getAttribute('name'),
    ]);

    return api()->item($product, new ProductTransformer());
}

Locale Resolution

The current locale is managed by LocaleManager.

use YasserElgammal\GreenLocale\LocaleManager;

$manager = LocaleManager::getInstance();

$manager->current();
$manager->fallback();
$manager->available();
$manager->isAvailable('ar');

Set the current locale manually:

LocaleManager::getInstance()->setLocale('ar');

Set the fallback locale manually:

LocaleManager::getInstance()->setFallbackLocale('en');

If you already have an app middleware that stores the locale in the session, sync it into LocaleManager:

use YasserElgammal\GreenLocale\LocaleManager;

$locale = session()->get('locale');
$manager = LocaleManager::getInstance();

if (is_string($locale) && $manager->isAvailable($locale)) {
    $manager->setLocale($locale);
}

Querying Localized Values

Use LocaleQueryBuilder with Green table builders.

use YasserElgammal\GreenLocale\LocaleQueryBuilder;

$table = new ProductTable();
$qb = $table->builder();

LocaleQueryBuilder::whereLocale($qb, 'name', 'en', 'Phone');

$products = $table->fetchAllFromBuilder($qb);

LIKE query:

LocaleQueryBuilder::whereLocaleLike($qb, 'name', 'en', '%Pho%');

Order by a locale value:

LocaleQueryBuilder::orderByLocale($qb, 'name', 'ar', 'ASC');

Generated SQL uses JSON extraction:

JSON_UNQUOTE(JSON_EXTRACT(`name`, '$."en"')) = :locale_value

Validation Rules

The package includes Respect Validation rules.

LocaleArrayRule

Validates that the input is an associative array with valid locale keys.

use YasserElgammal\GreenLocale\Validation\LocaleArrayRule;

$rule = new LocaleArrayRule();

$rule->validate([
    'en' => 'Phone',
    'ar' => 'هاتف',
]); // true

Valid locale key examples:

  • en
  • ar
  • en_US

LocaleRequiredRule

Validates that required locales exist and are not empty.

use YasserElgammal\GreenLocale\Validation\LocaleRequiredRule;

$rule = new LocaleRequiredRule(['en', 'ar']);

$rule->validate([
    'en' => 'Phone',
    'ar' => 'هاتف',
]); // true

LocaleExistsRule

Validates that all locale keys are available locales.

use YasserElgammal\GreenLocale\Validation\LocaleExistsRule;

$rule = new LocaleExistsRule(['en', 'ar']);

$rule->validate([
    'en' => 'Phone',
    'fr' => 'Telephone',
]); // false

Example inside a payload:

use Respect\Validation\Validator as v;
use YasserElgammal\Green\Payload\Payload;
use YasserElgammal\GreenLocale\Validation\LocaleArrayRule;
use YasserElgammal\GreenLocale\Validation\LocaleExistsRule;
use YasserElgammal\GreenLocale\Validation\LocaleRequiredRule;

class StoreProductPayload extends Payload
{
    public function rules(): array
    {
        return [
            'name' => v::allOf(
                new LocaleArrayRule(),
                new LocaleRequiredRule(['en']),
                new LocaleExistsRule(['en', 'ar'])
            ),
        ];
    }
}

Events

The package does not use a hidden event bus. Locale events are collected on the model and can be dispatched explicitly.

Events:

  • LocaleValueStored
  • LocaleValueForgotten
  • LocaleValuesSynced

Read pending events:

$events = $product->localeEvents();

Dispatch and clear pending events:

$events = $product->dispatchLocaleEvents(function (object $event): void {
    // Send to your logger, queue, or application event system.
});

If no callback is passed, dispatchLocaleEvents() simply returns the events and clears the pending list.

$events = $product->dispatchLocaleEvents();

Helpers

The package autoloads helper functions.

locale_manager();
current_locale();
fallback_locale();

Examples:

$locale = current_locale();
$fallback = fallback_locale();

locale_manager()->setLocale('ar');

Transformers

Localized values are usually resolved in transformers:

use YasserElgammal\Green\Database\Model;

class ProductTransformer
{
    public function transform(Model $model): array
    {
        return [
            'id' => (int) $model->id,
            'name' => $model->localeValue('name'),
            'description' => $model->localeValue('description'),
            'all_names' => $model->localeValues('name'),
        ];
    }
}

Twig Usage

If your model is passed to Twig, call the explicit model method:

{{ product.localeValue('name') }}

To expose the current locale:

return view('products/index', [
    'products' => $products,
    'locale' => current_locale(),
]);

Testing

Run package tests from the Green skeleton:

vendor/bin/phpunit -c ../green-locale/phpunit.xml

Run the Green skeleton tests:

vendor/bin/phpunit tests

Validate Composer files:

composer validate --no-check-publish
cd ../green-locale
composer validate --no-check-publish

Full Example

1. Create Table

Generate a new migration:

php green make:migration CreateProductsTable
namespace Database\Migrations;

use YasserElgammal\Green\Database\Migrations\Migration;
use YasserElgammal\Green\Database\Schema\Blueprint;
use YasserElgammal\Green\Database\Schema\Schema;

class CreateProductsTable extends Migration
{
    public function up(): void
    {
        Schema::create('products', function (Blueprint $table) {
            $table->id();
            $table->text('name');
            $table->text('description')->nullable();
            $table->text('slug')->nullable();
            $table->timestamps();
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('products');
    }
}

2. Create Model

namespace App\Models;

use YasserElgammal\Green\Database\Model;
use YasserElgammal\GreenLocale\Concerns\HasLocales;

class Product extends Model
{
    use HasLocales;

    protected string $table = 'products';
    protected string $primaryKey = 'id';

    protected array $localeAttributes = [
        'name',
        'description',
    ];
}

3. Create Table Gateway

namespace App\Tables;

use App\Models\Product;
use YasserElgammal\Green\Database\Table;

class ProductTable extends Table
{
    public function __construct()
    {
        parent::__construct(new Product());
    }
}

4. Insert Product

$table = new ProductTable();

$product = $table->insert([
    'name' => json_encode([
        'en' => 'Phone',
        'ar' => 'هاتف',
    ], JSON_UNESCAPED_UNICODE),
    'description' => json_encode([
        'en' => 'Smart device',
        'ar' => 'جهاز ذكي',
    ], JSON_UNESCAPED_UNICODE),
]);

5. Read Product

LocaleManager::getInstance()->setLocale('ar');

echo $product->localeValue('name'); // هاتف

6. Update Product Locale

$product->putLocaleValue('name', 'ar', 'هاتف جديد');

$table->update($product->id, [
    'name' => $product->getAttribute('name'),
]);

7. Query Product

$qb = $table->builder();

LocaleQueryBuilder::whereLocale($qb, 'name', 'en', 'Phone');

$products = $table->fetchAllFromBuilder($qb);

Troubleshooting

Locale Always Falls Back

Check that:

  • LocaleServiceProvider::boot() is called
  • The requested locale exists in available_locales
  • The resolver chain includes the source you are using, such as query or session

LocaleAttributeNotDeclared

Add the attribute to $localeAttributes:

protected array $localeAttributes = [
    'name',
    'description',
];

Missing Values Return Null

If strict is false, missing values return fallback or null.

If you want missing values to throw exceptions:

'strict' => true,

JSON Query Does Not Work

LocaleQueryBuilder uses JSON functions:

JSON_UNQUOTE(JSON_EXTRACT(...))

Make sure your database supports these functions. MySQL supports them. SQLite support depends on the JSON extension being available.

Contributing

Contributions are welcome!