---
title: "Translation Project"
description: "Derafu Translation"
type: "docs"
category: "doc"
tags: [php]
authors: [Anonymous]
date: "2026-08-21"
last_update: "2026-08-21"
time_minutes: 1
draft: false
unlisted: false
url: "https://www.derafu.dev/docs/core/translation"
---

# Derafu Translation



---

## Introduction

Translation Library with Exception Support

# Translation Library with Exception Support

![GitHub last commit](https://img.shields.io/github/last-commit/derafu/translation/main)
![CI Workflow](https://github.com/derafu/translation/actions/workflows/ci.yml/badge.svg?branch=main&amp;event=push)
![GitHub code size in bytes](https://img.shields.io/github/languages/code-size/derafu/translation)
![GitHub Issues](https://img.shields.io/github/issues-raw/derafu/translation)
![Total Downloads](https://poser.pugx.org/derafu/translation/downloads)
![Monthly Downloads](https://poser.pugx.org/derafu/translation/d/monthly)

A PHP library that solves one specific problem: **making exceptions translatable without compromising your existing code**, while providing powerful ICU message formatting support.

## Features

- 🔄 **Translatable Exceptions**: Make any exception translatable with minimal changes.
- 🌍 **ICU Support**: Built-in ICU message formatting for complex messages.
- 📦 **Multiple Formats**: Load translations from PHP, JSON, YAML files or custom provider.
- ⛓️ **Locale Fallback**: Configurable locale fallback chain.
- 🎯 **Framework Agnostic**: Works with any system implementing Symfony&#039;s TranslatorInterface.
- 🪶 **Lightweight**: Minimal dependencies (only requires `symfony/translation-contracts`).
- 🧩 **Extensible**: Easy to add custom message providers.

## Installation

```bash
composer require derafu/translation
```

## Basic Usage

### Making Exceptions Translatable

```php
// Before: Your existing exception.
class ValidationException extends Exception
{
    public function __construct(string $message)
    {
        parent::__construct($message);
    }
}

// After: Just change the parent class.
class ValidationException extends TranslatableException
{
    // No changes needed!
}
```

### Using Translatable Exceptions

```php
// 1. Simple string (works like before).
throw new ValidationException(&#039;Email is invalid&#039;);

// 2. With translation key and parameters.
throw new ValidationException([
    &#039;validation.email.invalid&#039;,
    &#039;email&#039; =&gt; &#039;test@example.com&#039;
]);

// 3. With ICU formatting.
throw new ValidationException(
    &#039;The email {email} is not valid&#039;,
    [&#039;email&#039; =&gt; &#039;test@example.com&#039;]
);

// 4. With complex ICU patterns.
throw new ValidationException(
    &#039;{gender, select, female{She} male{He} other{They}} sent {count, plural, one{# message} other{# messages}}&#039;,
    [
        &#039;gender&#039; =&gt; &#039;female&#039;,
        &#039;count&#039; =&gt; 5
    ]
);
```

### Translation Files

Support for multiple formats:

```php
// PHP arrays (fastest).
// translations/messages/en.php
return [
    &#039;welcome&#039; =&gt; &#039;Welcome {name}!&#039;,
    &#039;messages&#039; =&gt; &#039;{count, plural, =0{No messages} one{# message} other{# messages}}&#039;
];

// JSON.
// translations/messages/en.json
{
    &quot;welcome&quot;: &quot;Welcome {name}!&quot;,
    &quot;messages&quot;: &quot;{count, plural, =0{No messages} one{# message} other{# messages}}&quot;
}

// YAML (requires symfony/yaml).
# translations/messages/en.yaml
welcome: &#039;Welcome {name}!&#039;
messages: &#039;{count, plural, =0{No messages} one{# message} other{# messages}}&#039;
```

### Setting Up the Translator

```php
use Derafu\Translation\Translator;
use Derafu\Translation\Provider\PhpMessageProvider;

// Create provider.
$provider = new PhpMessageProvider(__DIR__ . &#039;/translations&#039;);

// Create translator with fallback locales.
$translator = new Translator(
    $provider,
    &#039;en&#039;, // Optional. Default `null` for Translator y &#039;en&#039; for ICU.
    [&#039;es_CL&#039;, &#039;es&#039;, &#039;en&#039;] // Optional. Default are: &#039;en&#039;, &#039;en_US&#039;, &#039;es&#039;, &#039;es_CL&#039;.
);

// Use in your exception handling.
try {
    // Your code.
} catch (TranslatableException $e) {
    // Get translation for current locale.
    echo $e-&gt;trans($translator);

    // Or specific locale.
    echo $e-&gt;trans($translator, &#039;es&#039;);
}
```

## Message Providers

The library includes three message providers:

- `PhpMessageProvider`: Uses PHP files returning arrays (fastest).
- `JsonMessageProvider`: Uses JSON files.
- `YamlMessageProvider`: Uses YAML files (requires `symfony/yaml`).

## Key Benefits

1. **Zero Compromise**: Keep your existing exception handling code.
2. **ICU Power**: Full ICU message formatting support.
3. **Flexible Loading**: Choose your preferred translation file format.
4. **Clean Separation**: Error logic stays separate from presentation.
5. **Type Safe**: Full PHP 8.3+ type safety.

## When to Use This Library

This library is perfect when you:

- Need to internationalize exceptions without refactoring.
- Want ICU message formatting support.
- Need flexible translation file formats.
- Want to keep error handling and translation separate.
- Use exceptions for business logic validation.




---

## Quick Start

Quick Start Guide

# Quick Start Guide

Get started with Derafu Translation library in minutes.

## Installation

Install via Composer:

```bash
composer require derafu/translation
```

Optional dependencies:

```bash
# If you want to use YAML files.
composer require symfony/yaml

# If you want to use Symfony&#039;s translator.
composer require symfony/translation
```

## Basic Setup

### 1. Create Translation Files

Choose your preferred format:

```php
// translations/messages/en.php
return [
    &#039;welcome&#039; =&gt; &#039;Welcome {name}!&#039;,
    &#039;errors&#039; =&gt; [
        &#039;required&#039; =&gt; &#039;The field {field} is required.&#039;,
        &#039;email&#039; =&gt; &#039;Invalid email: {email}&#039;
    ]
];
```

```json
// translations/messages/en.json
{
    &quot;welcome&quot;: &quot;Welcome {name}!&quot;,
    &quot;errors.required&quot;: &quot;The field {field} is required.&quot;,
    &quot;errors.email&quot;: &quot;Invalid email: {email}&quot;
}
```

```yaml
# translations/messages/en.yaml
welcome: &#039;Welcome {name}!&#039;
errors:
  required: &#039;The field {field} is required.&#039;
  email: &#039;Invalid email: {email}&#039;
```

### 2. Create Translator

```php
use Derafu\Translation\Translator;
use Derafu\Translation\Provider\PhpMessageProvider;

// Create message provider.
$provider = new PhpMessageProvider(__DIR__ . &#039;/translations&#039;);

// Create translator.
$translator = new Translator(
    provider: $provider,
    locale: &#039;en&#039;,
    fallbackLocales: [&#039;en&#039;]
);
```

### 3. Make Exceptions Translatable

```php
use Derafu\Translation\Exception\Core\TranslatableException;

class ValidationException extends TranslatableException
{
    // No additional code needed!
}
```

## First Translation

### Basic Usage

```php
// 1. Simple string messages.
throw new ValidationException(&#039;Email is required.&#039;);

// 2. With translation key.
throw new ValidationException(&#039;errors.required&#039;, [&#039;field&#039; =&gt; &#039;email&#039;]);

// 3. With ICU formatting.
throw new ValidationException(
    &#039;The value {value} must be between {min} and {max}.&#039;,
    [
        &#039;value&#039; =&gt; 42,
        &#039;min&#039; =&gt; 1,
        &#039;max&#039; =&gt; 10,
    ]
);
```

### Handling Exceptions

```php
try {
    // Your code.
} catch (ValidationException $e) {
    // Get default message.
    echo $e-&gt;getMessage();

    // Get translated message.
    echo $e-&gt;trans($translator);

    // Get message in specific locale.
    echo $e-&gt;trans($translator, &#039;es&#039;);
}
```

## Common Use Cases

### Form Validation

```php
class UserController
{
    public function create(Request $request): Response
    {
        $data = $request-&gt;toArray();

        if (empty($data[&#039;email&#039;])) {
            throw new ValidationException([
                &#039;errors.required&#039;,
                &#039;field&#039; =&gt; &#039;email&#039;,
            ]);
        }

        if (!filter_var($data[&#039;email&#039;], FILTER_VALIDATE_EMAIL)) {
            throw new ValidationException([
                &#039;errors.email&#039;,
                &#039;email&#039; =&gt; $data[&#039;email&#039;],
            ]);
        }

        // Create user...
    }
}
```

### API Responses

```php
class ErrorHandler
{
    public function handle(Throwable $e): JsonResponse
    {
        if ($e instanceof TranslatableException) {
            return new JsonResponse([
                &#039;error&#039; =&gt; [
                    &#039;message&#039; =&gt; $e-&gt;trans($this-&gt;translator),
                    &#039;code&#039; =&gt; $e-&gt;getCode(),
                ]
            ]);
        }

        // Handle other errors...
    }
}
```

### Complex Messages

```php
// Pluralization.
throw new ValidationException(
    &#039;{count, plural, =0{No files uploaded} one{1 file uploaded} other{# files uploaded}}&#039;,
    [&#039;count&#039; =&gt; $fileCount]
);

// Gender.
throw new ValidationException(
    &#039;{gender, select, female{She} male{He} other{They}} uploaded {count} files&#039;,
    [
        &#039;gender&#039; =&gt; $user-&gt;getGender(),
        &#039;count&#039; =&gt; $fileCount
    ]
);
```

## Next Steps

1. Read about [ICU Message Format](icu-formatting) for powerful message formatting.
2. Learn about [Message Providers](custom-providers) for different storage options.
3. Check [Real World Examples](real-world) for common patterns.
4. Explore [Advanced Usage](advanced-usage) for complex scenarios.

## Common Pitfalls

1. Always include &#039;other&#039; case in plural/select patterns:
    ```php
    // Wrong.
    &#039;{gender, select, female{She} male{He}}&#039;

    // Correct.
    &#039;{gender, select, female{She} male{He} other{They}}&#039;
    ```

2. Remember to provide all parameters:
    ```php
    // Will fail.
    throw new ValidationException(
        &#039;The field {field} is required&#039;
        // Missing parameters!
    );

    // Correct.
    throw new ValidationException(
        &#039;The field {field} is required&#039;,
        [&#039;field&#039; =&gt; &#039;email&#039;]
    );
    ```

3. Use consistent translation keys:
    ```php
    // Recommended structure.
    validation.required
    validation.email.invalid
    validation.range.between
    ```

## Tips &amp; Tricks

1. Use constants for translation keys:
    ```php
    final class Messages
    {
        public const REQUIRED = &#039;validation.required&#039;;
        public const EMAIL_INVALID = &#039;validation.email.invalid&#039;;
    }
    ```

2. Create factory methods for common exceptions:
    ```php
    class ValidationException extends TranslatableException
    {
        public static function required(string $field): self
        {
            return new self([
                Messages::REQUIRED,
                &#039;field&#039; =&gt; $field,
            ]);
        }
    }
    ```

3. Use type hints for better IDE support:
    ```php
    /**
     * @throws ValidationException When email is invalid
     */
    public function validateEmail(string $email): void
    {
        // Validation code.
    }
    ```




---

## API Reference

API Reference

# API Reference

Complete reference documentation for the Derafu Translation library.

## Interfaces

### TranslatableInterface

Extends Symfony&#039;s `TranslatorInterface` and adds ICU support.

```php
interface TranslatableInterface extends TranslatorInterface, Stringable
{
    /**
     * Converts the message to a string using ICU formatting.
     */
    public function __toString(): string;
}
```

### MessageProviderInterface

Defines how translation messages are loaded.

```php
interface MessageProviderInterface
{
    /**
     * Returns all messages for a given locale and domain.
     *
     * @param string $locale The locale to load messages for.
     * @param string $domain The translation domain.
     * @return array&lt;string, string&gt; Array of messages where key is the message id.
     */
    public function getMessages(string $locale, string $domain = &#039;messages&#039;): array;

    /**
     * Returns all available locales for a given domain.
     *
     * @param string $domain The translation domain.
     * @return array&lt;string&gt; List of available locales.
     */
    public function getAvailableLocales(string $domain = &#039;messages&#039;): array;
}
```

## Classes

### TranslatableMessage

Core message class that supports ICU formatting.

```php
final class TranslatableMessage implements TranslatableInterface
{
    /**
     * @param string $message The message used for translation.
     * @param array&lt;string, mixed&gt; $parameters Parameters for translation.
     * @param string|null $domain The translation domain.
     * @param string $defaultLocale The locale for ICU formatting.
     */
    public function __construct(
        string $message,
        array $parameters = [],
        ?string $domain = null,
        string $defaultLocale = &#039;en&#039;
    );

    /**
     * @throws RuntimeException When ICU formatting fails.
     */
    public function __toString(): string;

    public function trans(
        TranslatorInterface $translator,
        ?string $locale = null
    ): string;
}
```

### Translator

Main translator implementation, using Symfony&#039;s `TranslatorTrait`, with ICU and fallback support.

```php
final class Translator implements TranslatorInterface
{
    /**
     * @param MessageProviderInterface $provider Provider of messages.
     * @param string|null $locale Default locale.
     * @param array|null $fallbackLocales Fallback locales chain.
     */
    public function __construct(
        MessageProviderInterface $provider,
        ?string $locale = null,
        ?array $fallbackLocales = null
    );

    /**
     * @throws IntlException When ICU formatting fails
     */
    public function trans(
        ?string $id,
        array $parameters = [],
        ?string $domain = null,
        ?string $locale = null
    ): string;
}
```

## Exceptions

### TranslatableException

Base exception class that supports translation.

```php
abstract class TranslatableException extends Exception implements TranslatableInterface
{
    /**
     * @param string|array|TranslatableInterface $message The exception message:
     *   - string: Used as both message and translation key.
     *   - array: First element is message, rest are parameters.
     *   - TranslatableInterface: Used directly.
     * @param int $code The exception code.
     * @param Throwable|null $previous Previous exception.
     * @throws InvalidArgumentException When message array is invalid.
     */
    public function __construct(
        string|array|TranslatableInterface $message,
        int $code = 0,
        ?Throwable $previous = null
    );

    public function trans(
        TranslatorInterface $translator,
        ?string $locale = null
    ): string;
}
```

## Message Providers

### AbstractMessageProvider

Base class for file-based message providers.

```php
abstract class AbstractMessageProvider implements MessageProviderInterface
{
    /**
     * @param string $directory Base directory for translation files.
     * @throws RuntimeException If directory doesn&#039;t exist.
     */
    public function __construct(
        protected readonly string $directory
    );

    /**
     * Returns the file extension without dot.
     */
    abstract protected function getFileExtension(): string;

    /**
     * Parses a file into messages array.
     *
     * @throws RuntimeException If file can&#039;t be parsed.
     */
    abstract protected function parseFile(string $file): array;
}
```

### PhpMessageProvider

Loads translations from PHP files.

```php
final class PhpMessageProvider extends AbstractMessageProvider
{
    /**
     * Expected file format:
     * &lt;?php
     * return [
     *     &#039;key&#039; =&gt; &#039;value&#039;,
     * ];
     *
     * @throws RuntimeException If file doesn&#039;t return array.
     */
    protected function parseFile(string $file): array;
}
```

### JsonMessageProvider

Loads translations from JSON files.

```php
final class JsonMessageProvider extends AbstractMessageProvider
{
    /**
     * Expected file format:
     * {
     *     &quot;key&quot;: &quot;value&quot;
     * }
     *
     * @throws RuntimeException If JSON is invalid.
     */
    protected function parseFile(string $file): array;
}
```

### YamlMessageProvider

Loads translations from YAML files (requires symfony/yaml).

```php
final class YamlMessageProvider extends AbstractMessageProvider
{
    /**
     * Expected file format:
     * key: value
     *
     * @throws RuntimeException If YAML is invalid.
     */
    protected function parseFile(string $file): array;
}
```

## Error Handling

All exceptions extend from base PHP exceptions.

Common exceptions:

- `TranslatableRuntimeException`: General runtime errors.
- `TranslatableInvalidArgumentException`: Invalid input parameters.

---

Can&#039;t extend Derafu Translation exceptions? Then use `TranslatableExceptionTrait` in your exceptions to get the translation capabilities.




---

## Exceptions

Working with Translatable Exceptions

# Working with Translatable Exceptions

This guide covers all aspects of working with exceptions in the Derafu Translation library.

## Available Exceptions

The library provides translatable versions of all standard PHP exceptions:

### Core Exceptions

- `TranslatableException`: Base exception.
- `TranslatableLogicException`: For logical errors.
- `TranslatableRuntimeException`: For runtime errors.

### Logic Exceptions

- `TranslatableDomainException`: Domain logic violations.
- `TranslatableInvalidArgumentException`: Invalid input.
- `TranslatableLengthException`: Invalid length.
- `TranslatableOutOfRangeException`: Value out of valid set.

### Runtime Exceptions

- `TranslatableOutOfBoundsException`: Invalid index or key.
- `TranslatableOverflowException`: Arithmetic overflow.
- `TranslatableRangeException`: Value not within range.
- `TranslatableUnderflowException`: Arithmetic underflow.
- `TranslatableUnexpectedValueException`: Unexpected value type.

## Using the Trait

### TranslatableExceptionTrait

If you want to add translation capabilities to your own exceptions, you can use the trait:

```php
use Derafu\Translation\Contract\TranslatableInterface;
use Derafu\Translation\Exception\TranslatableExceptionTrait;
use DomainException;

class MyCustomException extends DomainException implements TranslatableInterface
{
    use TranslatableExceptionTrait;
}
```

### When to Use the Trait vs Extending Base Exceptions

Use the trait when:

- You already extend another exception.
- You need to customize the exception behavior.
- You want to add additional functionality.

Use the base exceptions when:

- You don&#039;t need custom behavior.
- You want the simplest implementation.
- You&#039;re creating domain-specific exceptions.

Example with trait:

```php
class OrderException extends DomainException implements TranslatableInterface
{
    use TranslatableExceptionTrait;

    protected string $defaultDomain = &#039;orders&#039;;

    public static function insufficientStock(Product $product): self
    {
        return new self([
            &#039;order.insufficient_stock&#039;,
            &#039;product&#039; =&gt; $product-&gt;getName(),
        ]);
    }
}
```

Example with inheritance:
```php
class ValidationException extends TranslatableDomainException
{
    // Inherits all translation functionality.
}
```

## When to Use Each Exception

### TranslatableException

Base exception, use when:

- General error conditions.
- No specific exception type fits.
- Creating custom exception hierarchies.

Real-world examples:

1. Generic API errors.
    ```php
    throw new TranslatableException([
        &#039;api.error.generic&#039;,
        &#039;code&#039; =&gt; $errorCode,
    ]);
    ```

2. System configuration errors.
    ```php
    throw new TranslatableException([
        &#039;system.config.missing&#039;,
        &#039;key&#039; =&gt; &#039;database.host&#039;,
    ]);
    ```

3. Third-party service errors.
    ```php
    throw new TranslatableException([
        &#039;service.unavailable&#039;,
        &#039;service&#039; =&gt; &#039;payment&#039;,
        &#039;reason&#039; =&gt; $response-&gt;getError(),
    ]);
    ```

### TranslatableLogicException

For programming errors that can be detected during development:

1. Invalid business rule implementation.
    ```php
    throw new TranslatableLogicException([
        &#039;logic.invalid_workflow&#039;,
        &#039;state&#039; =&gt; $currentState,
        &#039;action&#039; =&gt; $attemptedAction,
    ]);
    ```

2. Configuration errors.
    ```php
    throw new TranslatableLogicException([
        &#039;config.invalid_combination&#039;,
        &#039;option1&#039; =&gt; $value1,
        &#039;option2&#039; =&gt; $value2,
    ]);
    ```

3. Invalid method usage.
    ```php
    throw new TranslatableLogicException([
        &#039;method.invalid_order&#039;,
        &#039;method&#039; =&gt; &#039;process&#039;,
        &#039;required_before&#039; =&gt; &#039;validate&#039;,
    ]);
    ```

### TranslatableDomainException

For violations of domain rules:

1. Business rule violations.
    ```php
    throw new TranslatableDomainException([
        &#039;order.invalid_status_transition&#039;,
        &#039;from&#039; =&gt; $currentStatus,
        &#039;to&#039; =&gt; $newStatus,
    ]);
    ```

2. Invalid entity state.
    ```php
    throw new TranslatableDomainException([
        &#039;product.cannot_publish&#039;,
        &#039;reason&#039; =&gt; &#039;missing_price&#039;,
    ]);
    ```

3. Business constraint violations.
    ```php
    throw new TranslatableDomainException([
        &#039;account.overdraft_limit_exceeded&#039;,
        &#039;amount&#039; =&gt; $amount,
        &#039;limit&#039; =&gt; $overdraftLimit,
    ]);
    ```

### TranslatableInvalidArgumentException

For invalid input values:

1. Invalid parameter types.
    ```php
    throw new TranslatableInvalidArgumentException([
        &#039;argument.invalid_type&#039;,
        &#039;argument&#039; =&gt; &#039;date&#039;,
        &#039;expected&#039; =&gt; &#039;DateTime&#039;,
        &#039;received&#039; =&gt; get_debug_type($date),
    ]);
    ```

2. Invalid format.
    ```php
    throw new TranslatableInvalidArgumentException([
        &#039;argument.invalid_format&#039;,
        &#039;field&#039; =&gt; &#039;phone&#039;,
        &#039;format&#039; =&gt; &#039;+XX-XXX-XXXXXX&#039;,
    ]);
    ```

3. Invalid options.
    ```php
    throw new TranslatableInvalidArgumentException([
        &#039;argument.invalid_option&#039;,
        &#039;option&#039; =&gt; &#039;sort&#039;,
        &#039;valid&#039; =&gt; implode(&#039;, &#039;, $validOptions),
    ]);
    ```

### TranslatableLengthException

For invalid lengths:

1. String length violations.
    ```php
    throw new TranslatableLengthException([
        &#039;length.string_too_long&#039;,
        &#039;field&#039; =&gt; &#039;title&#039;,
        &#039;max&#039; =&gt; 255,
        &#039;current&#039; =&gt; strlen($title),
    ]);
    ```

2. Collection size issues.
    ```php
    throw new TranslatableLengthException([
        &#039;length.too_many_items&#039;,
        &#039;max&#039; =&gt; 10,
        &#039;current&#039; =&gt; count($items),
    ]);
    ```

3. Buffer size problems.
    ```php
    throw new TranslatableLengthException([
        &#039;length.buffer_overflow&#039;,
        &#039;size&#039; =&gt; $bufferSize,
        &#039;required&#039; =&gt; $requiredSize,
    ]);
    ```

### TranslatableOutOfRangeException

For values outside valid set:

1. Invalid enum values.
    ```php
    throw new TranslatableOutOfRangeException([
        &#039;value.invalid_status&#039;,
        &#039;value&#039; =&gt; $status,
        &#039;valid&#039; =&gt; implode(&#039;, &#039;, Status::cases()),
    ]);
    ```

2. Invalid date ranges.
    ```php
    throw new TranslatableOutOfRangeException([
        &#039;date.out_of_range&#039;,
        &#039;date&#039; =&gt; $date-&gt;format(&#039;Y-m-d&#039;),
        &#039;min&#039; =&gt; $minDate-&gt;format(&#039;Y-m-d&#039;),
        &#039;max&#039; =&gt; $maxDate-&gt;format(&#039;Y-m-d&#039;),
    ]);
    ```

3. Invalid numerical ranges.
    ```php
    throw new TranslatableOutOfRangeException([
        &#039;value.out_of_range&#039;,
        &#039;value&#039; =&gt; $percentage,
        &#039;min&#039; =&gt; 0,
        &#039;max&#039; =&gt; 100,
    ]);
    ```

### TranslatableOutOfBoundsException

For invalid array/collection access:

1. Invalid array index.
    ```php
    throw new TranslatableOutOfBoundsException([
        &#039;index.invalid&#039;,
        &#039;index&#039; =&gt; $index,
        &#039;max&#039; =&gt; count($array) - 1,
    ]);
    ```

2. Invalid page number.
    ```php
    throw new TranslatableOutOfBoundsException([
        &#039;page.invalid&#039;,
        &#039;page&#039; =&gt; $page,
        &#039;total&#039; =&gt; $totalPages,
    ]);
    ```

3. Invalid collection access.
    ```php
    throw new TranslatableOutOfBoundsException([
        &#039;record.not_found&#039;,
        &#039;id&#039; =&gt; $id,
        &#039;type&#039; =&gt; &#039;user&#039;,
    ]);
    ```

### TranslatableRangeException

For values technically valid but not allowed:

1. Value scale errors.
    ```php
    throw new TranslatableRangeException([
        &#039;number.too_many_decimals&#039;,
        &#039;value&#039; =&gt; $number,
        &#039;max_decimals&#039; =&gt; 2,
    ]);
    ```

2. Business range violations.
    ```php
    throw new TranslatableRangeException([
        &#039;discount.exceeds_limit&#039;,
        &#039;discount&#039; =&gt; $discount,
        &#039;max_allowed&#039; =&gt; $maxDiscount,
    ]);
    ```

3. Technical limitations.
    ```php
    throw new TranslatableRangeException([
        &#039;file.too_large&#039;,
        &#039;size&#039; =&gt; $fileSize,
        &#039;max&#039; =&gt; $maxUploadSize,
    ]);
    ```

### TranslatableUnexpectedValueException

For values of unexpected type:

1. Invalid return values.
    ```php
    throw new TranslatableUnexpectedValueException([
        &#039;api.unexpected_response&#039;,
        &#039;expected&#039; =&gt; &#039;array&#039;,
        &#039;received&#039; =&gt; gettype($response),
    ]);
    ```

2. Invalid data format.
    ```php
    throw new TranslatableUnexpectedValueException([
        &#039;data.invalid_format&#039;,
        &#039;expected&#039; =&gt; &#039;JSON&#039;,
        &#039;received&#039; =&gt; $detectedFormat,
    ]);
    ```

3. Invalid state values.
    ```php
    throw new TranslatableUnexpectedValueException([
        &#039;state.unexpected&#039;,
        &#039;state&#039; =&gt; $currentState,
        &#039;expected&#039; =&gt; implode(&#039;, &#039;, $validStates),
    ]);
    ```

## Best Practices

1. **Choose Specific Exceptions**
   - Use the most specific exception type available.
   - Consider the error&#039;s nature (logic vs runtime).
   - Think about error recovery possibilities.

2. **Consistent Domain Language**
   - Use consistent translation keys.
   - Group related messages.
   - Use clear, descriptive keys.

3. **Provide Context**
   - Include relevant parameters.
   - Add debugging information.
   - Consider logging needs.

4. **Exception Hierarchy**
   - Create domain-specific exceptions.
   - Extend appropriate base classes.
   - Use meaningful inheritance chains.

5. **Translation Keys**
   - Use consistent naming patterns.
   - Group by domain/module.
   - Include error type in key.

### Example hierarchy

```php
// Base exception for your domain.
abstract class OrderException extends TranslatableDomainException
{
    protected string $defaultDomain = &#039;orders&#039;;
}

// Specific exceptions.
class OrderNotFoundException extends OrderException {}
class OrderValidationException extends OrderException {}
class OrderStateException extends OrderException {}
```




---

## ICU Formatting

ICU Message Formatting Guide

# ICU Message Formatting Guide

This guide covers ICU (International Components for Unicode) message formatting in the context of the Derafu Translation library. ICU provides powerful tools for handling pluralization, gender, and complex message patterns.

## Basic Placeholders

The simplest form of ICU formatting uses named placeholders:

```php
throw new ValidationException(
    &#039;The field {field} is required.&#039;,
    [&#039;field&#039; =&gt; &#039;email&#039;],
);
// Output: &quot;The field email is required.&quot;.
```

Multiple placeholders are supported:

```php
throw new ValidationException(
    &#039;Value {value} for field {field} is invalid.&#039;,
    [
        &#039;value&#039; =&gt; &#039;test@&#039;,
        &#039;field&#039; =&gt; &#039;email&#039;,
    ],
);
// Output: &quot;Value test@ for field email is invalid.&quot;
```

## Pluralization

ICU provides robust pluralization support with multiple forms:

```php
// Basic pluralization.
$message = &#039;{count, plural, =0{No messages} one{# message} other{# messages}}&#039;;
$translator-&gt;trans($message, [&#039;count&#039; =&gt; 2]);  // &quot;2 messages&quot;
$translator-&gt;trans($message, [&#039;count&#039; =&gt; 1]);  // &quot;1 message&quot;
$translator-&gt;trans($message, [&#039;count&#039; =&gt; 0]);  // &quot;No messages&quot;

// With exact matches and ranges.
$message = &#039;{count, plural,
    =0 {No items}
    =1 {One item}
    =2 {A couple of items}
    other {# items}
}&#039;;
```

Available plural categories:

- `zero`: For languages with special zero forms.
- `one`: Singular form.
- `two`: Dual form (for languages that have it).
- `few`: For languages with special handling of small numbers.
- `many`: For languages with special handling of large numbers.
- `other`: Default form (required).
- `=n`: Exact number matches.

## Gender Selection

Gender-based message formatting:

```php
$message = &#039;{gender, select,
    female {She liked your post}
    male {He liked your post}
    other {They liked your post}
}&#039;;

$translator-&gt;trans($message, [&#039;gender&#039; =&gt; &#039;female&#039;]);
// Output: &quot;She liked your post&quot;.
```

Complex gender example with variables:

```php
$message = &#039;{gender, select,
    female {{name} added her comment}
    male {{name} added his comment}
    other {{name} added their comment}
}&#039;;

$translator-&gt;trans($message, [
    &#039;gender&#039; =&gt; &#039;female&#039;,
    &#039;name&#039; =&gt; &#039;Alice&#039;
]);
// Output: &quot;Alice added her comment&quot;.
```

## Number Formatting

ICU supports various number formats:

```php
// Basic number.
&#039;{value, number}&#039;

// With minimum decimals.
&#039;{value, number, .00}&#039;

// Percentage.
&#039;{value, number, percent}&#039;

// Currency.
&#039;{value, number, currency}&#039;
```

Example in context:
```php
throw new ValidationException(
    &#039;Balance must be greater than {min, number, currency}.&#039;,
    [&#039;min&#039; =&gt; 100],
);
// Output: &quot;Balance must be greater than $100.00&quot;.
```

## Nested Formatting

ICU patterns can be nested for complex scenarios:

```php
$message = &#039;{gender, select,
    female {
        {count, plural,
            =0 {She has no messages}
            one {She has # message}
            other {She has # messages}
        }
    }
    male {
        {count, plural,
            =0 {He has no messages}
            one {He has # message}
            other {He has # messages}
        }
    }
    other {
        {count, plural,
            =0 {They have no messages}
            one {They have # message}
            other {They have # messages}
        }
    }
}&#039;;

$translator-&gt;trans($message, [
    &#039;gender&#039; =&gt; &#039;female&#039;,
    &#039;count&#039; =&gt; 5,
]);
// Output: &quot;She has 5 messages&quot;.
```

## Common Patterns

Here are some common patterns used in validation messages:

```php
// Range validation.
&#039;Value must be between {min} and {max}.&#039;

// List validation.
&#039;{count, plural,
    =0 {List cannot be empty}
    one {At least one item is required}
    other {At least # items are required}
}&#039;

// Status messages.
&#039;{status, select,
    pending {Waiting for approval}
    approved {Approved on {date}}
    rejected {Rejected: {reason}}
    other {Unknown status}
}&#039;

// File validation.
&#039;{type, select,
    image {Only images are allowed}
    document {Only documents are allowed}
    other {Invalid file type}
}, max size: {size}&#039;
```

## Troubleshooting

Common issues and solutions:

1. **Missing &#039;other&#039; category**
    ```php
    // Wrong.
    &#039;{gender, select, male{He} female{She}}&#039;

    // Correct.
    &#039;{gender, select, male{He} female{She} other{They}}&#039;
    ```

2. **Invalid nesting**
    ```php
    // Wrong.
    &#039;{outer, select, a{{inner}}&#039;

    // Correct.
    &#039;{outer, select, a {{inner}} other {default}}&#039;
    ```

3. **Unmatched braces**
    ```php
    // Wrong.
    &#039;Hello {name&#039;

    // Correct.
    &#039;Hello {name}&#039;
    ```

4. **Missing parameters**
    ```php
    // Will fail if &#039;count&#039; is not provided.
    &#039;{count} items&#039;
    ```

---

Remember:

- Always include the &#039;other&#039; case in select/plural patterns.
- Check braces are properly matched.
- Ensure all used parameters are provided.
- Test with different values and locales.

---

For more information about ICU message format:

- [ICU User Guide](https://unicode-org.github.io/icu/userguide/format_parse/messages/)
- [MessageFormat Guide](https://messageformat.github.io/messageformat/)




---

## Custom Providers

Creating Custom Message Providers

# Creating Custom Message Providers

This guide explains how to create custom message providers for different translation storage formats and sources.

## Provider Interface

All message providers must implement the `MessageProviderInterface`:

```php
interface MessageProviderInterface
{
    /**
     * Returns all messages for a given locale and domain.
     *
     * @param string $locale The locale to load messages for.
     * @param string $domain The translation domain.
     * @return array&lt;string, string&gt; Array of messages where key is the message id.
     */
    public function getMessages(string $locale, string $domain = &#039;messages&#039;): array;

    /**
     * Returns all available locales for a given domain.
     *
     * @param string $domain The translation domain.
     * @return array&lt;string&gt; List of available locales.
     */
    public function getAvailableLocales(string $domain = &#039;messages&#039;): array;
}
```

## Basic Implementation

Here&#039;s a simple example of a custom provider that loads messages from a database:

```php
use Derafu\Translation\Contract\MessageProviderInterface;
use PDO;

final class DatabaseMessageProvider implements MessageProviderInterface
{
    public function __construct(
        private readonly PDO $db,
        private readonly string $table = &#039;translations&#039;
    ) {
    }

    public function getMessages(string $locale, string $domain = &#039;messages&#039;): array
    {
        $stmt = $this-&gt;db-&gt;prepare(
            &quot;SELECT message_key, message_text
             FROM {$this-&gt;table}
             WHERE locale = ? AND domain = ?&quot;
        );
        $stmt-&gt;execute([$locale, $domain]);

        return $stmt-&gt;fetchAll(PDO::FETCH_KEY_PAIR);
    }

    public function getAvailableLocales(string $domain = &#039;messages&#039;): array
    {
        $stmt = $this-&gt;db-&gt;prepare(
            &quot;SELECT DISTINCT locale
             FROM {$this-&gt;table}
             WHERE domain = ?&quot;
        );
        $stmt-&gt;execute([$domain]);

        return $stmt-&gt;fetchAll(PDO::FETCH_COLUMN);
    }
}
```

## Using the Abstract Provider

For file-based providers, you can extend the `AbstractMessageProvider`:

```php
use Derafu\Translation\Abstract\AbstractMessageProvider;

final class IniMessageProvider extends AbstractMessageProvider
{
    protected function getFileExtension(): string
    {
        return &#039;ini&#039;;
    }

    protected function parseFile(string $file): array
    {
        $messages = parse_ini_file($file, false);
        if ($messages === false) {
            throw new RuntimeException(
                sprintf(&#039;Could not parse INI file &quot;%s&quot;.&#039;, $file)
            );
        }

        return $messages;
    }
}
```

The abstract provider handles:

- Directory structure validation.
- File path generation.
- Available locales discovery.

You only need to implement:

- `getFileExtension()`: Returns the file extension.
- `parseFile()`: Parses the file content into messages array.

## Other Examples

### Redis Provider

```php
use Redis;
use Derafu\Translation\Contract\MessageProviderInterface;

final class RedisMessageProvider implements MessageProviderInterface
{
    public function __construct(
        private readonly Redis $redis,
        private readonly string $prefix = &#039;translations:&#039;
    ) {
    }

    public function getMessages(string $locale, string $domain = &#039;messages&#039;): array
    {
        $key = &quot;{$this-&gt;prefix}{$domain}:{$locale}&quot;;
        $messages = $this-&gt;redis-&gt;hGetAll($key);

        return $messages ?: [];
    }

    public function getAvailableLocales(string $domain = &#039;messages&#039;): array
    {
        $pattern = &quot;{$this-&gt;prefix}{$domain}:*&quot;;
        $keys = $this-&gt;redis-&gt;keys($pattern);

        return array_map(
            fn($key) =&gt; substr($key, strrpos($key, &#039;:&#039;) + 1),
            $keys
        );
    }
}
```

### API Provider

```php
use GuzzleHttp\Client;
use Derafu\Translation\Contract\MessageProviderInterface;

final class ApiMessageProvider implements MessageProviderInterface
{
    public function __construct(
        private readonly Client $client,
        private readonly string $baseUrl
    ) {
    }

    public function getMessages(string $locale, string $domain = &#039;messages&#039;): array
    {
        $response = $this-&gt;client-&gt;get(
            &quot;{$this-&gt;baseUrl}/translations/{$domain}/{$locale}&quot;
        );

        return json_decode(
            $response-&gt;getBody()-&gt;getContents(),
            true
        );
    }

    public function getAvailableLocales(string $domain = &#039;messages&#039;): array
    {
        $response = $this-&gt;client-&gt;get(
            &quot;{$this-&gt;baseUrl}/translations/{$domain}/locales&quot;
        );

        return json_decode(
            $response-&gt;getBody()-&gt;getContents(),
            true
        );
    }
}
```

## Best Practices

1. **Error Handling**
    ```php
    protected function parseFile(string $file): array
    {
        try {
            // Parse file.
        } catch (Exception $e) {
            throw new RuntimeException(
                sprintf(&#039;Error parsing file &quot;%s&quot;: %s&#039;, $file, $e-&gt;getMessage())
            );
        }
    }
    ```

2. **Caching Support**
    ```php
    final class CachedProvider implements MessageProviderInterface
    {
        public function __construct(
            private readonly MessageProviderInterface $provider,
            private readonly CacheInterface $cache,
            private readonly int $ttl = 3600
        ) {
        }

        public function getMessages(string $locale, string $domain = &#039;messages&#039;): array
        {
            $key = &quot;translations:{$domain}:{$locale}&quot;;

            return $this-&gt;cache-&gt;remember($key, $this-&gt;ttl, function() use ($locale, $domain) {
                return $this-&gt;provider-&gt;getMessages($locale, $domain);
            });
        }
    }
    ```

3. **Validation**
    ```php
    private function validateMessages(array $messages): void
    {
        foreach ($messages as $key =&gt; $value) {
            if (!is_string($key)) {
                throw new RuntimeException(&#039;Message keys must be strings.&#039;);
            }
            if (!is_string($value)) {
                throw new RuntimeException(&#039;Message values must be strings.&#039;);
            }
        }
    }
    ```

4. **Logging and Debugging**
    ```php
    public function getMessages(string $locale, string $domain = &#039;messages&#039;): array
    {
        $messages = $this-&gt;loadMessages($locale, $domain);

        if (empty($messages)) {
            $this-&gt;logger-&gt;warning(
                &#039;No messages found for locale {locale} and domain {domain}.&#039;,
                [&#039;locale&#039; =&gt; $locale, &#039;domain&#039; =&gt; $domain]
            );
        }

        return $messages;
    }
    ```

---

Remember:

- Always validate input and output.
- Handle errors gracefully.
- Consider implementing caching for performance.
- Add logging for debugging.
- Keep providers focused and single-purpose.
- Use dependency injection for external services.




---

## Advanced Usage

Advanced Usage Guide

# Advanced Usage Guide

This guide covers advanced patterns and usage scenarios for the Derafu Translation library.

## Message Composition

### Base Messages

```php
class MessageTemplates
{
    public const REQUIRED = &#039;validation.required&#039;;
    public const INVALID = &#039;validation.invalid&#039;;
    public const FORMAT = &#039;validation.format&#039;;

    public static function required(string $field): TranslatableInterface
    {
        return new TranslatableMessage(self::REQUIRED, [&#039;field&#039; =&gt; $field]);
    }

    public static function invalid(string $field, mixed $value): TranslatableInterface
    {
        return new TranslatableMessage(self::INVALID, [
            &#039;field&#039; =&gt; $field,
            &#039;value&#039; =&gt; (string) $value
        ]);
    }

    public static function format(string $field, string $format): TranslatableInterface
    {
        return new TranslatableMessage(self::FORMAT, [
            &#039;field&#039; =&gt; $field,
            &#039;format&#039; =&gt; $format
        ]);
    }
}
```

### Composite Messages

```php
class CompositeMessage implements TranslatableInterface
{
    /** @var TranslatableInterface[] */
    private array $messages;

    /**
     * @param TranslatableInterface[] $messages
     */
    public function __construct(array $messages)
    {
        $this-&gt;messages = $messages;
    }

    public function trans(TranslatorInterface $translator, ?string $locale = null): string
    {
        return implode(&#039; &#039;, array_map(
            fn(TranslatableInterface $message) =&gt; $message-&gt;trans($translator, $locale),
            $this-&gt;messages
        ));
    }

    public function __toString(): string
    {
        return implode(&#039; &#039;, array_map(
            fn(TranslatableInterface $message) =&gt; (string) $message,
            $this-&gt;messages
        ));
    }
}

// Usage
throw new ValidationException(new CompositeMessage([
    MessageTemplates::required(&#039;email&#039;),
    MessageTemplates::format(&#039;email&#039;, &#039;user@example.com&#039;)
]));
```

## Dynamic Translation Loading

### Translation Strategy

```php
interface TranslationStrategy
{
    public function getMessages(string $locale): array;
    public function supports(string $domain): bool;
}

class ApiTranslationStrategy implements TranslationStrategy
{
    public function __construct(
        private readonly HttpClientInterface $client,
        private readonly string $apiUrl
    ) {
    }

    public function getMessages(string $locale): array
    {
        $response = $this-&gt;client-&gt;request(&#039;GET&#039;, sprintf(
            &#039;%s/translations/%s&#039;,
            $this-&gt;apiUrl,
            $locale
        ));

        return json_decode($response-&gt;getBody()-&gt;getContents(), true);
    }

    public function supports(string $domain): bool
    {
        return $domain === &#039;remote&#039;;
    }
}

class DynamicProvider implements MessageProviderInterface
{
    /** @var TranslationStrategy[] */
    private array $strategies = [];

    public function addStrategy(TranslationStrategy $strategy): void
    {
        $this-&gt;strategies[] = $strategy;
    }

    public function getMessages(string $locale, string $domain = &#039;messages&#039;): array
    {
        foreach ($this-&gt;strategies as $strategy) {
            if ($strategy-&gt;supports($domain)) {
                return $strategy-&gt;getMessages($locale);
            }
        }
        return [];
    }
}
```

## Exception Hierarchies

### Base Domain Exception

```php
abstract class DomainException extends TranslatableException
{
    protected function getDefaultDomain(): string
    {
        return &#039;domain&#039;;
    }

    protected function getMessageContext(): array
    {
        return [
            &#039;timestamp&#039; =&gt; date(&#039;Y-m-d H:i:s&#039;),
            &#039;trace_id&#039; =&gt; $this-&gt;getTraceId()
        ];
    }

    private function getTraceId(): string
    {
        return bin2hex(random_bytes(16));
    }
}
```

### Specific Exceptions

```php
class OrderException extends DomainException
{
    protected function getDefaultDomain(): string
    {
        return &#039;orders&#039;;
    }

    public static function insufficientStock(
        Product $product,
        int $requested,
        int $available
    ): self {
        return new self([
            &#039;order.insufficient_stock&#039;,
            &#039;product&#039; =&gt; $product-&gt;getName(),
            &#039;requested&#039; =&gt; $requested,
            &#039;available&#039; =&gt; $available
        ]);
    }
}

class PaymentException extends DomainException
{
    protected function getDefaultDomain(): string
    {
        return &#039;payments&#039;;
    }

    public static function insufficientFunds(
        Money $amount,
        Money $balance
    ): self {
        return new self([
            &#039;payment.insufficient_funds&#039;,
            &#039;amount&#039; =&gt; $amount-&gt;format(),
            &#039;balance&#039; =&gt; $balance-&gt;format(),
            &#039;currency&#039; =&gt; $amount-&gt;getCurrency()
        ]);
    }
}
```

## Context-Aware Translation

### Translation Context

```php
class TranslationContext
{
    public function __construct(
        private readonly array $data = []
    ) {
    }

    public function get(string $key, mixed $default = null): mixed
    {
        return $this-&gt;data[$key] ?? $default;
    }

    public function merge(array $data): self
    {
        return new self(array_merge($this-&gt;data, $data));
    }
}

class ContextAwareTranslator implements TranslatorInterface
{
    private ?TranslationContext $context = null;

    public function __construct(
        private readonly TranslatorInterface $translator
    ) {
    }

    public function withContext(TranslationContext $context): self
    {
        $clone = clone $this;
        $clone-&gt;context = $context;
        return $clone;
    }

    public function trans(
        ?string $id,
        array $parameters = [],
        ?string $domain = null,
        ?string $locale = null
    ): string {
        if ($this-&gt;context) {
            $parameters = array_merge(
                $parameters,
                $this-&gt;context-&gt;toArray()
            );
        }

        return $this-&gt;translator-&gt;trans($id, $parameters, $domain, $locale);
    }
}
```

## Translation Decorators

### Logging Decorator

```php
class LoggingTranslator implements TranslatorInterface
{
    public function __construct(
        private readonly TranslatorInterface $translator,
        private readonly LoggerInterface $logger
    ) {
    }

    public function trans(
        ?string $id,
        array $parameters = [],
        ?string $domain = null,
        ?string $locale = null
    ): string {
        $result = $this-&gt;translator-&gt;trans($id, $parameters, $domain, $locale);

        $this-&gt;logger-&gt;debug(&#039;Translation performed&#039;, [
            &#039;id&#039; =&gt; $id,
            &#039;parameters&#039; =&gt; $parameters,
            &#039;domain&#039; =&gt; $domain,
            &#039;locale&#039; =&gt; $locale,
            &#039;result&#039; =&gt; $result
        ]);

        return $result;
    }
}
```

### Fallback Chain Decorator

```php
class FallbackChainTranslator implements TranslatorInterface
{
    /** @var TranslatorInterface[] */
    private array $translators;

    public function __construct(TranslatorInterface ...$translators)
    {
        $this-&gt;translators = $translators;
    }

    public function trans(
        ?string $id,
        array $parameters = [],
        ?string $domain = null,
        ?string $locale = null
    ): string {
        $lastException = null;

        foreach ($this-&gt;translators as $translator) {
            try {
                return $translator-&gt;trans($id, $parameters, $domain, $locale);
            } catch (Exception $e) {
                $lastException = $e;
                continue;
            }
        }

        throw new RuntimeException(
            &#039;No translator could handle the translation&#039;,
            0,
            $lastException
        );
    }
}
```

### Caching Decorator

```php
class CachingTranslator implements TranslatorInterface
{
    public function __construct(
        private readonly TranslatorInterface $translator,
        private readonly CacheInterface $cache,
        private readonly int $ttl = 3600
    ) {
    }

    public function trans(
        ?string $id,
        array $parameters = [],
        ?string $domain = null,
        ?string $locale = null
    ): string {
        $key = $this-&gt;getCacheKey($id, $parameters, $domain, $locale);

        return $this-&gt;cache-&gt;get($key, function() use ($id, $parameters, $domain, $locale) {
            return $this-&gt;translator-&gt;trans($id, $parameters, $domain, $locale);
        }, $this-&gt;ttl);
    }

    private function getCacheKey(
        string $id,
        array $parameters,
        ?string $domain,
        ?string $locale
    ): string {
        return md5(serialize([
            &#039;id&#039; =&gt; $id,
            &#039;parameters&#039; =&gt; $parameters,
            &#039;domain&#039; =&gt; $domain,
            &#039;locale&#039; =&gt; $locale
        ]));
    }
}
```

---

Remember:

- Use composition to extend functionality.
- Keep single responsibility principle.
- Make exceptions domain-specific.
- Consider performance implications.
- Add proper logging and monitoring.
- Handle edge cases gracefully.




---

## Real World

Real World Examples

# Real World Examples

This guide provides practical examples of using the Derafu Translation library in real-world scenarios.

## REST API Error Handling

### Error Response Structure

```php
class ApiException extends TranslatableException
{
    public function toArray(): array
    {
        return [
            &#039;error&#039; =&gt; [
                &#039;message&#039; =&gt; $this-&gt;getMessage(),
                &#039;code&#039; =&gt; $this-&gt;getCode(),
                &#039;type&#039; =&gt; $this-&gt;getType(),
            ],
        ];
    }

    protected function getType(): string
    {
        return (new ReflectionClass($this))-&gt;getShortName();
    }
}

class ValidationApiException extends ApiException
{
    private array $errors;

    public function __construct(array $errors, int $code = 422)
    {
        $this-&gt;errors = $errors;
        parent::__construct(&#039;validation.failed&#039;, $code);
    }

    public function toArray(): array
    {
        return [
            &#039;error&#039; =&gt; [
                &#039;message&#039; =&gt; $this-&gt;getMessage(),
                &#039;code&#039; =&gt; $this-&gt;getCode(),
                &#039;type&#039; =&gt; $this-&gt;getType(),
                &#039;errors&#039; =&gt; $this-&gt;errors,
            ],
        ];
    }
}
```

### API Error Handler

```php
class ApiErrorHandler
{
    public function __construct(
        private readonly TranslatorInterface $translator
    ) {
    }

    public function handle(Throwable $error): JsonResponse
    {
        if ($error instanceof ApiException) {
            $data = $error-&gt;toArray();
            if ($error instanceof TranslatableException) {
                $data[&#039;error&#039;][&#039;message&#039;] = $error-&gt;trans(
                    $this-&gt;translator,
                    $this-&gt;getLocaleFromRequest()
                );
            }
            return new JsonResponse($data, $error-&gt;getCode());
        }

        // Handle other errors...
    }
}
```

### Usage in Controllers

```php
class UserController
{
    public function create(Request $request): JsonResponse
    {
        $data = $request-&gt;toArray();

        if (empty($data[&#039;email&#039;])) {
            throw new ValidationApiException([
                &#039;email&#039; =&gt; new TranslatableMessage(
                    &#039;validation.required&#039;,
                    [&#039;field&#039; =&gt; &#039;email&#039;]
                ),
            ]);
        }

        try {
            // Create user...
        } catch (DuplicateEmailException $e) {
            throw new ValidationApiException([
                &#039;email&#039; =&gt; new TranslatableMessage(
                    &#039;validation.email.duplicate&#039;,
                    [&#039;email&#039; =&gt; $data[&#039;email&#039;]]
                ),
            ]);
        }
    }
}
```

## Form Validation

### Form Type

```php
class RegistrationFormType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options): void
    {
        $builder
            -&gt;add(&#039;email&#039;, EmailType::class, [
                &#039;constraints&#039; =&gt; [
                    new NotBlank([
                        &#039;message&#039; =&gt; new TranslatableMessage(
                            &#039;validation.required&#039;,
                            [&#039;field&#039; =&gt; &#039;email&#039;]
                        ),
                    ]),
                    new Email([
                        &#039;message&#039; =&gt; new TranslatableMessage(
                            &#039;validation.email.invalid&#039;,
                            [&#039;email&#039; =&gt; &#039;{{ value }}&#039;]
                        ),
                    ])
                ]
            ])
            -&gt;add(&#039;password&#039;, PasswordType::class, [
                &#039;constraints&#039; =&gt; [
                    new Length([
                        &#039;min&#039; =&gt; 8,
                        &#039;minMessage&#039; =&gt; new TranslatableMessage(
                            &#039;validation.password.min_length&#039;,
                            [&#039;min&#039; =&gt; 8]
                        ),
                    ])
                ]
            ]);
    }
}
```

### Form Handler

```php
class RegistrationFormHandler
{
    public function handle(FormInterface $form): User
    {
        if (!$form-&gt;isSubmitted()) {
            throw new FormException(&#039;form.not_submitted&#039;);
        }

        if (!$form-&gt;isValid()) {
            $errors = [];
            foreach ($form-&gt;getErrors(true) as $error) {
                $errors[$error-&gt;getOrigin()-&gt;getName()] = $error-&gt;getMessage();
            }
            throw new ValidationApiException($errors);
        }

        return $this-&gt;createUser($form-&gt;getData());
    }
}
```

## Domain-Specific Validation

### Order Processing

```php
class OrderProcessor
{
    public function process(Order $order): void
    {
        // Check stock.
        if (!$this-&gt;hasStock($order)) {
            throw new OrderException([
                &#039;order.insufficient_stock&#039;,
                &#039;product&#039; =&gt; $order-&gt;getProduct()-&gt;getName(),
                &#039;requested&#039; =&gt; $order-&gt;getQuantity(),
                &#039;available&#039; =&gt; $this-&gt;getAvailableStock($order-&gt;getProduct()),
            ]);
        }

        // Check status transitions.
        if (!$this-&gt;canTransition($order, $status)) {
            throw new OrderException([
                &#039;order.invalid_transition&#039;,
                &#039;from&#039; =&gt; $order-&gt;getStatus(),
                &#039;to&#039; =&gt; $status,
                &#039;allowed&#039; =&gt; implode(&#039;, &#039;, $this-&gt;getAllowedTransitions($order)),
            ]);
        }
    }
}
```

### Financial Validation

```php
class PaymentValidator
{
    public function validate(Payment $payment): void
    {
        // Balance check.
        if ($payment-&gt;getAmount() &gt; $this-&gt;getBalance()) {
            throw new PaymentException([
                &#039;payment.insufficient_funds&#039;,
                &#039;amount&#039; =&gt; $payment-&gt;getAmount(),
                &#039;balance&#039; =&gt; $this-&gt;getBalance(),
                &#039;currency&#039; =&gt; $payment-&gt;getCurrency(),
            ]);
        }

        // Limit check.
        if ($payment-&gt;getAmount() &gt; $this-&gt;getDailyLimit()) {
            throw new PaymentException([
                &#039;payment.limit_exceeded&#039;,
                &#039;amount&#039; =&gt; $payment-&gt;getAmount(),
                &#039;limit&#039; =&gt; $this-&gt;getDailyLimit(),
                &#039;period&#039; =&gt; &#039;daily&#039;,
            ]);
        }
    }
}
```

## Complex Business Rules

### Document Workflow

```php
class DocumentWorkflow
{
    public function validate(Document $document): void
    {
        // Check permissions.
        if (!$this-&gt;canUserAccess($document)) {
            throw new DocumentException([
                &#039;document.access_denied&#039;,
                &#039;document&#039; =&gt; $document-&gt;getTitle(),
                &#039;user&#039; =&gt; $this-&gt;getCurrentUser()-&gt;getName(),
                &#039;required_role&#039; =&gt; $document-&gt;getRequiredRole(),
            ]);
        }

        // Check workflow state.
        if (!$this-&gt;canTransition($document, $action)) {
            throw new DocumentException([
                &#039;document.invalid_workflow_transition&#039;,
                &#039;document&#039; =&gt; $document-&gt;getTitle(),
                &#039;current&#039; =&gt; $document-&gt;getState(),
                &#039;action&#039; =&gt; $action,
                &#039;required_approvals&#039; =&gt; $document-&gt;getRequiredApprovals(),
                &#039;current_approvals&#039; =&gt; $document-&gt;getCurrentApprovals(),
            ]);
        }
    }
}
```

## Multiple Translation Sources

### Combining Providers

```php
class CompositeProvider implements MessageProviderInterface
{
    /** @var MessageProviderInterface[] */
    private array $providers;

    public function __construct(array $providers)
    {
        $this-&gt;providers = $providers;
    }

    public function getMessages(string $locale, string $domain = &#039;messages&#039;): array
    {
        $messages = [];
        foreach ($this-&gt;providers as $provider) {
            $messages = array_merge(
                $messages,
                $provider-&gt;getMessages($locale, $domain)
            );
        }
        return $messages;
    }
}

// Usage.
$provider = new CompositeProvider([
    new PhpMessageProvider(__DIR__ . &#039;/translations&#039;),
    new DatabaseMessageProvider($db),
    new RedisMessageProvider($redis),
]);
```

### Cache Layer

```php
class CachedProvider implements MessageProviderInterface
{
    public function __construct(
        private readonly MessageProviderInterface $provider,
        private readonly CacheInterface $cache,
        private readonly int $ttl = 3600
    ) {
    }

    public function getMessages(string $locale, string $domain = &#039;messages&#039;): array
    {
        $key = &quot;translations:{$domain}:{$locale}&quot;;

        return $this-&gt;cache-&gt;get($key, function() use ($locale, $domain) {
            return $this-&gt;provider-&gt;getMessages($locale, $domain);
        }, $this-&gt;ttl);
    }
}
```

---

Remember:

- Keep error messages user-friendly but informative.
- Include relevant context in error messages.
- Use consistent message structure.
- Consider performance with caching.
- Handle nested validations properly.
- Plan for internationalization from the start.




---

## Symfony Integration

Symfony Integration Guide

# Symfony Integration Guide

This guide explains how to integrate the Derafu Translation library with Symfony&#039;s translation system.

## Basic Integration

The simplest way to use Derafu Translation with Symfony is to use Symfony&#039;s translator directly:

```php
use Symfony\Component\Translation\Translator;
use Symfony\Component\Translation\Loader\YamlFileLoader;

class ErrorHandler
{
    public function __construct(
        private readonly Translator $translator
    ) {
    }

    public function handle(TranslatableException $e): void
    {
        $message = $e-&gt;trans($this-&gt;translator);
        // Handle translated message.
    }
}
```

## Using Symfony&#039;s Translator

### Configuration

```yaml
# config/packages/translation.yaml
framework:
    default_locale: &#039;en&#039;
    translator:
        default_path: &#039;%kernel.project_dir%/translations&#039;
        fallbacks:
            - &#039;en&#039;
        paths:
            - &#039;%kernel.project_dir%/vendor/your-vendor/your-package/translations&#039;
```

### Translation Files

```yaml
# translations/errors.en.yaml
validation:
    required: &#039;The field {field} is required&#039;
    email:
        invalid: &#039;The email {email} is not valid&#039;
    min_length: &#039;The field {field} must be at least {min} characters&#039;

# translations/errors.es.yaml
validation:
    required: &#039;El campo {field} es requerido&#039;
    email:
        invalid: &#039;El email {email} no es válido&#039;
    min_length: &#039;El campo {field} debe tener al menos {min} caracteres&#039;
```

### Exception Handler

```php
namespace App\EventListener;

use Symfony\Component\HttpKernel\Event\ExceptionEvent;
use Symfony\Contracts\Translation\TranslatorInterface;
use Derafu\Translation\Exception\Core\TranslatableException;

class TranslatableExceptionListener
{
    public function __construct(
        private readonly TranslatorInterface $translator
    ) {
    }

    public function onKernelException(ExceptionEvent $event): void
    {
        $throwable = $event-&gt;getThrowable();

        if (!$throwable instanceof TranslatableException) {
            return;
        }

        $response = new JsonResponse([
            &#039;error&#039; =&gt; $throwable-&gt;trans(
                $this-&gt;translator,
                $this-&gt;translator-&gt;getLocale()
            ),
        ]);

        $event-&gt;setResponse($response);
    }
}
```

### Service Configuration

```yaml
# config/services.yaml
services:
    App\EventListener\TranslatableExceptionListener:
        tags:
            - { name: kernel.event_listener, event: kernel.exception }
```

## Advanced Usage

### Custom Exception Response Format

```php
class ApiExceptionListener
{
    public function onKernelException(ExceptionEvent $event): void
    {
        $throwable = $event-&gt;getThrowable();

        if (!$throwable instanceof TranslatableException) {
            return;
        }

        $response = new JsonResponse([
            &#039;status&#039; =&gt; &#039;error&#039;,
            &#039;message&#039; =&gt; [
                &#039;text&#039; =&gt; $throwable-&gt;trans($this-&gt;translator),
                &#039;translation_key&#039; =&gt; $throwable-&gt;getTranslatableMessage()-&gt;getId(),
                &#039;parameters&#039; =&gt; $throwable-&gt;getTranslatableMessage()-&gt;getParameters(),
            ],
            &#039;code&#039; =&gt; $throwable-&gt;getCode(),
        ]);

        $event-&gt;setResponse($response);
    }
}
```

### Locale Based on User Preferences

```php
class LocaleListener
{
    public function onKernelRequest(RequestEvent $event): void
    {
        $request = $event-&gt;getRequest();

        // Get locale from user preferences.
        $locale = $request-&gt;getPreferredLanguage([&#039;en&#039;, &#039;es&#039;]);

        // Set for current request.
        $request-&gt;setLocale($locale);

        // Set for translator.
        $this-&gt;translator-&gt;setLocale($locale);
    }
}
```

## Best Practices

1. **Organize Translation Files**
    ```
    translations/
    ├── errors/
    │   ├── validators.en.yaml
    │   ├── validators.es.yaml
    │   ├── forms.en.yaml
    │   └── forms.es.yaml
    └── messages/
        ├── app.en.yaml
        └── app.es.yaml
    ```

2. **Use Constants for Translation Keys**
    ```php
    final class TranslationKeys
    {
        public const VALIDATION_REQUIRED = &#039;validation.required&#039;;
        public const VALIDATION_EMAIL = &#039;validation.email.invalid&#039;;
        // ...
    }
    ```

3. **Create Exception Factory**
    ```php
    final class ValidationExceptionFactory
    {
        public static function required(string $field): ValidationException
        {
            return new ValidationException([
                TranslationKeys::VALIDATION_REQUIRED,
                &#039;field&#039; =&gt; $field
            ]);
        }
    }
    ```

4. **Log Missing Translations**
    ```php
    $this-&gt;translator-&gt;setFallbackLocales([&#039;en&#039;]);
    $this-&gt;translator-&gt;addListener(
        TranslationEvents::MISSING_TRANSLATION,
        function(MissingTranslationEvent $event) {
            $this-&gt;logger-&gt;warning(
                &#039;Missing translation: {key} for locale {locale}&#039;,
                [
                    &#039;key&#039; =&gt; $event-&gt;getMessageId(),
                    &#039;locale&#039; =&gt; $event-&gt;getLocale()
                ]
            );
        }
    );
    ```

---

Remember:

- Keep translation files organized by domain.
- Use constants for translation keys to avoid typos.
- Create factories for common exceptions.
- Log missing translations in development.
- Use fallback locales appropriately.
- Consider user preferences for locale selection.




---

## Testing

Testing Guide

# Testing Guide

This guide covers how to test applications using the Derafu Translation library, focusing on testing translatable exceptions and message handling.

## Testing Exceptions

### Basic Exception Testing

```php
use Derafu\Translation\TranslatableMessage;
use PHPUnit\Framework\TestCase;

class ValidationExceptionTest extends TestCase
{
    public function testBasicException(): void
    {
        $exception = new ValidationException(&#039;Email is invalid.&#039;);

        // Without translation, should use original message.
        $this-&gt;assertEquals(&#039;Email is invalid.&#039;, $exception-&gt;getMessage());
    }

    public function testExceptionWithParameters(): void
    {
        $exception = new ValidationException([
            &#039;validation.email&#039;,
            &#039;email&#039; =&gt; &#039;test@example&#039;,
        ]);

        // Test ICU formatting without translator.
        $this-&gt;assertEquals(
            &#039;Invalid email: test@example&#039;,
            $exception-&gt;getMessage()
        );
    }

    public function testWithTranslatableMessage(): void
    {
        $message = new TranslatableMessage(
            &#039;validation.required&#039;,
            [&#039;field&#039; =&gt; &#039;email&#039;],
        );

        $exception = new ValidationException($message);
        $this-&gt;assertInstanceOf(
            TranslatableMessage::class,
            $exception-&gt;getTranslatableMessage()
        );
    }
}
```

### Using Mock Translator

```php
class OrderExceptionTest extends TestCase
{
    private MockObject&amp;TranslatorInterface $translator;

    protected function setUp(): void
    {
        $this-&gt;translator = $this-&gt;createMock(TranslatorInterface::class);
    }

    public function testOrderValidation(): void
    {
        $this-&gt;translator
            -&gt;expects($this-&gt;once())
            -&gt;method(&#039;trans&#039;)
            -&gt;with(
                &#039;order.invalid_status&#039;,
                [&#039;status&#039; =&gt; &#039;pending&#039;],
                &#039;errors&#039;,
                &#039;en&#039;
            )
            -&gt;willReturn(&#039;Invalid order status: pending&#039;);

        $exception = new OrderException([
            &#039;order.invalid_status&#039;,
            &#039;status&#039; =&gt; &#039;pending&#039;
        ]);

        $this-&gt;assertEquals(
            &#039;Invalid order status: pending&#039;,
            $exception-&gt;trans($this-&gt;translator, &#039;en&#039;)
        );
    }
}
```

## Testing Translations

### Testing Message Files

```php
class TranslationFilesTest extends TestCase
{
    private string $fixturesDir;

    protected function setUp(): void
    {
        $this-&gt;fixturesDir = __DIR__ . &#039;/../fixtures/translations&#039;;
    }

    #[DataProvider(&#039;dataProvider&#039;)]
    public function testMessageFiles(string $file): void
    {
        $this-&gt;assertFileExists($file);

        // Test file format.
        $messages = require $file;
        $this-&gt;assertIsArray($messages);

        // Test message format.
        foreach ($messages as $key =&gt; $value) {
            $this-&gt;assertIsString($key);
            $this-&gt;assertIsString($value);

            // Test ICU format.
            $formatter = new MessageFormatter(&#039;en&#039;, $value);
            $this-&gt;assertNotFalse(
                $formatter,
                &quot;Invalid ICU format in message: $value&quot;
            );
        }
    }

    public function provideMessageFiles(): array
    {
        return [
            &#039;en messages&#039; =&gt; [$this-&gt;fixturesDir . &#039;/messages/en.php&#039;],
            &#039;es messages&#039; =&gt; [$this-&gt;fixturesDir . &#039;/messages/es.php&#039;],
            &#039;en errors&#039; =&gt; [$this-&gt;fixturesDir . &#039;/errors/en.php&#039;],
            &#039;es errors&#039; =&gt; [$this-&gt;fixturesDir . &#039;/errors/es.php&#039;],
        ];
    }
}
```

### Testing Translation Consistency

```php
class TranslationConsistencyTest extends TestCase
{
    private array $locales = [&#039;en&#039;, &#039;es&#039;];

    private array $domains = [&#039;messages&#039;, &#039;errors&#039;];

    private MessageProviderInterface $provider;

    protected function setUp(): void
    {
        $this-&gt;provider = new PhpMessageProvider(
            __DIR__ . &#039;/../fixtures/translations&#039;
        );
    }

    public function testAllLocalesHaveSameKeys(): void
    {
        foreach ($this-&gt;domains as $domain) {
            $baseMessages = $this-&gt;provider-&gt;getMessages(&#039;en&#039;, $domain);
            $baseKeys = array_keys($baseMessages);

            foreach ($this-&gt;locales as $locale) {
                if ($locale === &#039;en&#039;) {
                    continue;
                }

                $messages = $this-&gt;provider-&gt;getMessages($locale, $domain);
                $keys = array_keys($messages);

                $this-&gt;assertEquals(
                    sort($baseKeys),
                    sort($keys),
                    &quot;Missing translations in $locale for domain $domain&quot;
                );
            }
        }
    }

    public function testIcuParametersConsistency(): void
    {
        foreach ($this-&gt;domains as $domain) {
            $baseMessages = $this-&gt;provider-&gt;getMessages(&#039;en&#039;, $domain);

            foreach ($this-&gt;locales as $locale) {
                if ($locale === &#039;en&#039;) {
                    continue;
                }

                $messages = $this-&gt;provider-&gt;getMessages($locale, $domain);

                foreach ($baseMessages as $key =&gt; $baseMessage) {
                    $message = $messages[$key];

                    // Extract parameters from ICU messages.
                    preg_match_all(&#039;/{(\w+)}/&#039;, $baseMessage, $baseParams);
                    preg_match_all(&#039;/{(\w+)}/&#039;, $message, $params);

                    $this-&gt;assertEquals(
                        sort($baseParams[1]),
                        sort($params[1]),
                        &quot;Parameters mismatch in key &#039;$key&#039; for locale $locale&quot;
                    );
                }
            }
        }
    }
}
```

## Testing Custom Providers

```php
class CustomProviderTest extends TestCase
{
    public function testProvider(): void
    {
        $provider = new CustomProvider();

        // Test basic functionality.
        $messages = $provider-&gt;getMessages(&#039;en&#039;);
        $this-&gt;assertIsArray($messages);

        // Test locales.
        $locales = $provider-&gt;getAvailableLocales();
        $this-&gt;assertNotEmpty($locales);

        // Test domains.
        $errors = $provider-&gt;getMessages(&#039;en&#039;, &#039;errors&#039;);
        $this-&gt;assertIsArray($errors);
    }

    public function testErrorHandling(): void
    {
        $provider = new CustomProvider();

        $this-&gt;expectException(RuntimeException::class);
        $provider-&gt;getMessages(&#039;invalid-locale&#039;);
    }
}
```

## Test Helpers

```php
trait TranslationTestTrait
{
    private function createTestTranslator(): TranslatorInterface
    {
        return new class implements TranslatorInterface {
            public function trans(
                ?string $id,
                array $parameters = [],
                string $domain = null,
                string $locale = null
            ): string {
                return strtr($id, $parameters);
            }

            public function getLocale(): string
            {
                return &#039;en&#039;;
            }
        };
    }

    private function assertTranslationEquals(
        string $expected,
        TranslatableException $exception,
        string $message = &#039;&#039;
    ): void {
        $translator = $this-&gt;createTestTranslator();
        $this-&gt;assertEquals(
            $expected,
            $exception-&gt;trans($translator),
            $message
        );
    }
}
```

## Common Patterns

1. **Test Exception Factory Methods**
    ```php
    public function testExceptionFactory(): void
    {
        $exception = ValidationExceptionFactory::required(&#039;email&#039;);

        $this-&gt;assertInstanceOf(ValidationException::class, $exception);
        $this-&gt;assertEquals(
            &#039;The field email is required.&#039;,
            $exception-&gt;getMessage()
        );
    }
    ```

2. **Test Translation Fallbacks**
    ```php
    public function testFallbackTranslation(): void
    {
        $translator = new Translator(
            $this-&gt;provider,
            &#039;fr&#039;,
            [&#039;es&#039;, &#039;en&#039;]
        );

        $exception = new ValidationException(&#039;test.message&#039;);

        // Should fallback to English.
        $this-&gt;assertEquals(
            &#039;Test message&#039;,
            $exception-&gt;trans($translator)
        );
    }
    ```

3. **Test ICU Edge Cases**
    ```php
    public function testComplexIcuPattern(): void
    {
        $exception = new ValidationException(
            &#039;{count, plural, =0{Empty} one{# item} other{# items}}&#039;,
            [&#039;count&#039; =&gt; 0]
        );

        $this-&gt;assertEquals(&#039;Empty&#039;, $exception-&gt;getMessage());
    }
    ```

---

Remember:

- Always test both translated and untranslated scenarios.
- Test parameter substitution.
- Verify ICU message formatting.
- Check translation consistency across locales.
- Test error handling.
- Use data providers for multiple test cases.





---
Last updated on 21/08/2026
#php
