monkeyscloud/monkeyslegion-i18n 问题修复 & 功能扩展

解决BUG、新增功能、兼容多环境部署,快速响应你的开发需求

邮箱:yvsm@zunyunkeji.com | QQ:316430983 | 微信:yvsm316

monkeyscloud/monkeyslegion-i18n

Composer 安装命令:

composer require monkeyscloud/monkeyslegion-i18n

包简介

Production-ready I18n & localization component for the MonkeysLegion PHP framework — v2

README 文档

README

Production-ready internationalization & localization for the MonkeysLegion PHP framework.

PHP 8.4+ Tests License v2 Standards

Translate, pluralize, format numbers/dates/currencies, and manage locales with security and performance at the core. Built with PHP 8.4 property hooks and asymmetric visibility.

Table of Contents

Installation

composer require monkeyscloud/monkeyslegion-i18n

Requirements

  • PHP 8.4+ (property hooks, asymmetric visibility)
  • ext-json (JSON translation files)
  • ext-mbstring (Unicode support)

Optional Extensions

# Advanced number/currency/date formatting
ext-intl

# For database translations
monkeyscloud/monkeyslegion-database

# For cache-backed loading
monkeyscloud/monkeyslegion-cache

Quick Start

use MonkeysLegion\I18n\TranslatorFactory;

// One-line setup
$translator = TranslatorFactory::create([
    'locale'   => 'en',
    'fallback' => 'en',
    'path'     => __DIR__ . '/resources/lang',
]);

echo $translator->trans('messages.welcome');
// → "Welcome!"

echo $translator->trans('messages.greeting', ['name' => 'Yorch']);
// → "Hello, Yorch!"

echo $translator->choice('messages.items', 5);
// → "5 items"

Translation File Structure

resources/lang/
├── en/
│   ├── messages.json
│   └── validation.json
├── es/
│   ├── messages.json
│   └── validation.json
└── fr/
    └── messages.json

Example messages.json

{
  "welcome": "Welcome!",
  "greeting": "Hello, :name!",
  "farewell": "Goodbye, :NAME!",
  "items": "{0} No items|{1} One item|[2,*] :count items",
  "nested": {
    "key": "Nested value",
    "deep": {
      "value": "Deep nested value"
    }
  }
}

Translator

Basic Translation

use MonkeysLegion\I18n\Translator;
use MonkeysLegion\I18n\Loaders\FileLoader;

$translator = new Translator('en', 'en');
$translator->addLoader(new FileLoader('/path/to/lang'));

// Simple key
echo $translator->trans('messages.welcome');
// → "Welcome!"

// Nested key
echo $translator->trans('messages.nested.deep.value');
// → "Deep nested value"

// Check if translation exists
if ($translator->has('messages.welcome')) {
    // Key exists
}

// Returns the key itself when not found
echo $translator->trans('messages.nonexistent');
// → "messages.nonexistent"

Parameter Replacement

// Lowercase :name → exact value
echo $translator->trans('messages.greeting', ['name' => 'Yorch']);
// → "Hello, Yorch!"

// Uppercase :NAME → UPPERCASED
echo $translator->trans('messages.farewell', ['name' => 'Yorch']);
// → "Goodbye, YORCH!"

// Ucfirst :Name → Capitalized
// Message: "Welcome :Name"
echo $translator->trans('messages.title', ['name' => 'yorch']);
// → "Welcome Yorch"

// Multiple parameters
echo $translator->trans('order.summary', [
    'product' => 'Widget',
    'count'   => 3,
    'total'   => '29.97',
]);
// → "3x Widget — Total: $29.97"

Pluralization

Supports ICU plural categories for 200+ languages.

// Explicit count forms
// "{0} No items|{1} One item|[2,*] :count items"
echo $translator->choice('messages.items', 0);  // → "No items"
echo $translator->choice('messages.items', 1);  // → "One item"
echo $translator->choice('messages.items', 42); // → "42 items"

// Range forms
// "[0,3] A few|[4,10] Several|[11,*] Many"
echo $translator->choice('messages.range', 2);  // → "A few"
echo $translator->choice('messages.range', 7);  // → "Several"
echo $translator->choice('messages.range', 50); // → "Many"

// Simple pipe-delimited (singular|plural)
// "apple|apples"
echo $translator->choice('messages.fruit', 1); // → "apple"
echo $translator->choice('messages.fruit', 5); // → "apples"

// With additional replacements
echo $translator->choice('messages.items', 3, ['color' => 'red']);
// "{0} No :color items|{1} One :color item|[2,*] :count :color items"
// → "3 red items"

Namespaced Translations

// Register a namespace
$translator->addNamespace('billing', '/path/to/billing/lang');
$translator->addLoader(new FileLoader('/path/to/billing/lang'));

// Use namespace::group.key format
echo $translator->trans('billing::invoices.title');
// → Loads from /path/to/billing/lang/{locale}/invoices.json

Fallback Locale

$translator = new Translator('fr', 'en');

// If 'fr' translation missing, falls back to 'en'
echo $translator->trans('messages.welcome');
// French translation exists → "Bienvenue!"

echo $translator->trans('messages.rare_key');
// French missing, English fallback → "Rare English Value"

Missing Translation Tracking

$translator->setTrackMissing(true);

$translator->trans('messages.missing1');
$translator->trans('messages.missing2');

$missing = $translator->getMissingTranslations();
// ["en.messages.missing1", "en.messages.missing2"]

$translator->clearMissingTranslations();

Warm-Up

Pre-load translation groups for production performance:

$translator->warmUp('en', ['messages', 'validation', 'auth']);

// Check what's loaded
$groups = $translator->getLoadedGroups();
// ["messages.en", "validation.en", "auth.en"]

Property Hooks (PHP 8.4)

$translator = new Translator('en');

// Use property directly
echo $translator->locale; // → "en"

// Setter triggers validation + event dispatch
$translator->locale = 'es';

// Invalid locale throws InvalidLocaleException
$translator->locale = '../etc/passwd'; // ❌ InvalidLocaleException

Locale Changed Events

$translator->setEventDispatcher(function (LocaleChangedEvent $event): void {
    log("Locale changed: {$event->previousLocale}{$event->newLocale}");
});

$translator->setLocale('fr'); // Event fires automatically

MessageFormatter

Parameter Modifiers

Use braced syntax with modifiers for advanced formatting:

use MonkeysLegion\I18n\MessageFormatter;

$formatter = new MessageFormatter();

// Uppercase
echo $formatter->format('Name: {name|upper}', ['name' => 'yorch']);
// → "Name: YORCH"

// Lowercase
echo $formatter->format('Email: {email|lower}', ['email' => 'USER@EXAMPLE.COM']);
// → "Email: user@example.com"

// Title case
echo $formatter->format('Title: {title|title}', ['title' => 'hello world']);
// → "Title: Hello World"

// Capitalize first
echo $formatter->format('{msg|ucfirst}', ['msg' => 'hello']);
// → "Hello"

// Truncate
echo $formatter->format('{desc|truncate:20}', ['desc' => 'Very long description text']);
// → "Very long descriptio..."

// Number formatting
echo $formatter->format('Total: {amount|number:2}', ['amount' => 1234.5]);
// → "Total: 1,234.50"

// Currency
echo $formatter->format('Price: {price|currency:EUR}', ['price' => 42.50]);
// → "Price: €42.50"

// Percentage
echo $formatter->format('Rate: {rate|percent}', ['rate' => 0.156]);
// → "Rate: 15.60%"

// Date formatting
echo $formatter->format('Date: {date|date:medium}', ['date' => '2026-01-15']);
// → "Date: Jan 15, 2026"

// Default fallback
echo $formatter->format('Name: {name|default:Anonymous}', ['name' => '']);
// → "Name: Anonymous"

XSS Protection

// Auto-escape disabled by default for backward compatibility
$formatter = new MessageFormatter();
echo $formatter->format('Hello :name', ['name' => '<script>alert(1)</script>']);
// → "Hello <script>alert(1)</script>"

// Enable auto-escape for user-facing output
$safe = new MessageFormatter(autoEscape: true);
echo $safe->format('Hello :name', ['name' => '<script>alert(1)</script>']);
// → "Hello &lt;script&gt;alert(1)&lt;/script&gt;"

// Custom sanitizer
$custom = new MessageFormatter(
    autoEscape: true,
    sanitizer: new MyCustomSanitizer(),
);

NumberFormatter

Locale-aware number formatting with graceful ext-intl fallback.

use MonkeysLegion\I18n\NumberFormatter;

$nf = new NumberFormatter();

Decimal Formatting

echo $nf->decimal(1234567, 'en');       // → "1,234,567"
echo $nf->decimal(1234567, 'de');       // → "1.234.567" (with ext-intl)
echo $nf->decimal(3.14159, 'en', 2);    // → "3.14"

Currency Formatting

echo $nf->currency(42.50, 'USD', 'en');  // → "$42.50"
echo $nf->currency(42.50, 'EUR', 'de');  // → "42,50 €" (with ext-intl)
echo $nf->currency(42.50, 'GBP', 'en');  // → "£42.50"
echo $nf->currency(42.50, 'JPY', 'ja');  // → "¥42.50"
echo $nf->currency(42.50, 'BRL', 'pt');  // → "R$42.50"

// 32 built-in currency symbols
// USD, EUR, GBP, JPY, CAD, AUD, CHF, CNY, MXN, BRL,
// INR, KRW, RUB, TRY, SEK, NOK, DKK, PLN, CZK, HUF,
// RON, BGN, HRK, THB, PHP, MYR, IDR, VND, ZAR, EGP,
// NGN, KES, ARS, CLP, COP, PEN

Compact Notation

echo $nf->compact(500);           // → "500"
echo $nf->compact(1_234);         // → "1.2K"
echo $nf->compact(1_500_000);     // → "1.5M"
echo $nf->compact(2_345_000_000); // → "2.3B"
echo $nf->compact(-1_234);        // → "-1.2K"

Ordinals

echo $nf->ordinal(1);   // → "1st"
echo $nf->ordinal(2);   // → "2nd"
echo $nf->ordinal(3);   // → "3rd"
echo $nf->ordinal(11);  // → "11th"
echo $nf->ordinal(21);  // → "21st"
echo $nf->ordinal(112); // → "112th"

Percentage

echo $nf->percent(0.156, 'en', 1); // → "15.6%"
echo $nf->percent(0.5, 'en');      // → "50%"

File Size

echo $nf->fileSize(0);         // → "0 B"
echo $nf->fileSize(1024);      // → "1.00 KB"
echo $nf->fileSize(1_572_864); // → "1.50 MB"
echo $nf->fileSize(5e9);       // → "4.66 GB"

Spell Out

echo $nf->spellOut(123, 'en');
// → "one hundred twenty-three" (requires ext-intl)

echo $nf->spellOut(42, 'es');
// → "cuarenta y dos" (requires ext-intl)

DateFormatter

Locale-aware date/time formatting with relative time support.

use MonkeysLegion\I18n\DateFormatter;

$df = new DateFormatter();

Named Formats

$date = new DateTimeImmutable('2026-01-15');

echo $df->format($date, 'short');    // → "1/15/26"
echo $df->format($date, 'medium');   // → "Jan 15, 2026"
echo $df->format($date, 'long');     // → "January 15, 2026"
echo $df->format($date, 'full');     // → "Thursday, January 15, 2026"
echo $df->format($date, 'iso');      // → "2026-01-15"
echo $df->format($date, 'time');     // → "12:00 AM"
echo $df->format($date, 'datetime'); // → "Jan 15, 2026 12:00 AM"

// Custom format
echo $df->format($date, 'Y/m/d');    // → "2026/01/15"

// From timestamp
echo $df->format(1705276800, 'medium'); // → "Jan 15, 2024"

// From string
echo $df->format('2026-01-15', 'long'); // → "January 15, 2026"

// With timezone
echo $df->format($date, 'datetime', 'en', 'America/New_York');

Relative Time

$now = new DateTimeImmutable('2026-01-15 12:00:00');

// Past — English
echo $df->relative('2026-01-15 11:58:00', 'en', $now);
// → "2 minutes ago"

echo $df->relative('2026-01-15 10:00:00', 'en', $now);
// → "2 hours ago"

echo $df->relative('2026-01-14 12:00:00', 'en', $now);
// → "1 day ago"

echo $df->relative('2025-11-15 12:00:00', 'en', $now);
// → "2 months ago"

// Past — Spanish
echo $df->relative('2026-01-15 10:00:00', 'es', $now);
// → "hace 2 horas"

echo $df->relative('2026-01-14 12:00:00', 'es', $now);
// → "hace 1 día"

// Future
echo $df->relative('2026-01-15 14:00:00', 'en', $now);
// → "in 2 hours"

// Just now (< 10 seconds)
echo $df->relative('2026-01-15 11:59:55', 'en', $now);
// → "just now"

Diff for Humans

echo $df->diffForHumans(
    '2026-01-15 10:00:00',
    '2026-01-15 12:30:00',
    'en',
);
// → "2 hours ago"

Day and Month Names

$date = new DateTimeImmutable('2026-01-15');

echo $df->dayOfWeek($date);             // → "Thursday"
echo $df->dayOfWeek($date, short: true); // → "Thu"

echo $df->monthName($date);             // → "January"
echo $df->monthName($date, short: true); // → "Jan"

ISO 8601

echo $df->iso('2026-01-15 12:00:00');
// → "2026-01-15T12:00:00+00:00"

LocaleManager

Manages locale detection, validation, and state.

use MonkeysLegion\I18n\LocaleManager;

$manager = new LocaleManager(
    defaultLocale: 'en',
    supportedLocales: ['en', 'es', 'fr', 'de', 'ja'],
    fallbackLocale: 'en',
);

Detection Chain

use MonkeysLegion\I18n\Detectors\{
    UrlDetector,
    QueryDetector,
    SessionDetector,
    CookieDetector,
    HeaderDetector,
    SubdomainDetector,
};

// Priority order (first match wins)
$manager->addDetector(new UrlDetector(segment: 0));      // /es/products
$manager->addDetector(new QueryDetector(paramName: 'lang')); // ?lang=es
$manager->addDetector(new SessionDetector(key: 'locale'));
$manager->addDetector(new CookieDetector(cookieName: 'locale'));
$manager->addDetector(new HeaderDetector());              // Accept-Language
$manager->addDetector(new SubdomainDetector());           // es.example.com

$locale = $manager->detectLocale();
// Tries each detector in order, returns first supported match

Supported Locales

$manager->isSupported('es');     // → true
$manager->isSupported('xx');     // → false

$manager->addSupportedLocale('pt');
$manager->isSupported('pt');     // → true

$manager->setLocale('es');       // ✅ Switches locale
$manager->setLocale('xx');       // ❌ throws InvalidArgumentException

// Asymmetric visibility (PHP 8.4)
echo $manager->defaultLocale;    // → "en" (read-only from outside)
echo $manager->fallbackLocale;   // → "en"
print_r($manager->supportedLocales); // → ["en", "es", "fr", "de", "ja"]

Locale Parsing

echo $manager->parseLocale('en-US');  // → "en"
echo $manager->parseLocale('en_US');  // → "en"
echo $manager->parseLocale('pt-BR');  // → "pt"
echo $manager->parseLocale('es');     // → "es"

LocaleInfo

Static metadata for 50+ locales.

use MonkeysLegion\I18n\Support\LocaleInfo;

Native Names

echo LocaleInfo::name('es');       // → "Spanish"
echo LocaleInfo::nativeName('es'); // → "Español"
echo LocaleInfo::nativeName('ja'); // → "日本語"
echo LocaleInfo::nativeName('ar'); // → "العربية"
echo LocaleInfo::nativeName('ru'); // → "Русский"
echo LocaleInfo::nativeName('hi'); // → "हिन्दी"
echo LocaleInfo::nativeName('ko'); // → "한국어"
echo LocaleInfo::nativeName('zh'); // → "中文"

RTL Detection

echo LocaleInfo::isRtl('ar'); // → true (Arabic)
echo LocaleInfo::isRtl('he'); // → true (Hebrew)
echo LocaleInfo::isRtl('fa'); // → true (Persian)
echo LocaleInfo::isRtl('ur'); // → true (Urdu)
echo LocaleInfo::isRtl('en'); // → false

echo LocaleInfo::direction('ar'); // → Direction::RTL
echo LocaleInfo::direction('en'); // → Direction::LTR

Flag Emojis

echo LocaleInfo::flag('en'); // → 🇺🇸
echo LocaleInfo::flag('es'); // → 🇪🇸
echo LocaleInfo::flag('fr'); // → 🇫🇷
echo LocaleInfo::flag('jp'); // → 🇯🇵
echo LocaleInfo::flag('br'); // → 🇧🇷

Script & Knowledge

echo LocaleInfo::script('en'); // → "Latn"
echo LocaleInfo::script('ar'); // → "Arab"
echo LocaleInfo::script('ru'); // → "Cyrl"
echo LocaleInfo::script('ja'); // → "Jpan"
echo LocaleInfo::script('ko'); // → "Kore"

echo LocaleInfo::isKnown('en'); // → true
echo LocaleInfo::isKnown('xx'); // → false

$allCodes = LocaleInfo::allCodes(); // 50+ locale codes

Loaders

FileLoader

Loads from JSON and PHP files with security hardening.

use MonkeysLegion\I18n\Loaders\FileLoader;

$loader = new FileLoader('/path/to/lang');

// Loads from /path/to/lang/{locale}/{group}.json
$messages = $loader->load('en', 'messages');

// Add namespace path
$loader->addNamespace('billing', '/path/to/billing/lang');

// Security features:
// ✅ Path traversal prevention (realpath validation)
// ✅ Null byte injection prevention
// ✅ Max file size limit (2MB)
// ✅ JSON_THROW_ON_ERROR on all json_decode
// ✅ Symlink resolution and validation

DatabaseLoader

Load translations from any SQL database — MySQL, MariaDB, PostgreSQL, and SQLite.

Install the Schema

Ready-to-use SQL files are included in schema/:

# MySQL / MariaDB
mysql -u root -p your_database < schema/mysql.sql

# PostgreSQL
psql -U postgres -d your_database -f schema/pgsql.sql

# SQLite
sqlite3 storage/database.sqlite < schema/sqlite.sql

Schema (MySQL)

CREATE TABLE IF NOT EXISTS translations (
    id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    locale VARCHAR(10) NOT NULL,
    `group` VARCHAR(50) NOT NULL,
    namespace VARCHAR(50) NOT NULL DEFAULT '',
    `key` VARCHAR(255) NOT NULL,
    value TEXT NOT NULL,
    source VARCHAR(50) NOT NULL DEFAULT 'file',
    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    UNIQUE INDEX idx_translation_unique (locale, `group`, namespace, `key`),
    INDEX idx_translation_locale (locale),
    INDEX idx_translation_group (locale, `group`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

Schema (PostgreSQL)

CREATE TABLE IF NOT EXISTS translations (
    id BIGSERIAL PRIMARY KEY,
    locale VARCHAR(10) NOT NULL,
    "group" VARCHAR(50) NOT NULL,
    namespace VARCHAR(50) NOT NULL DEFAULT '',
    "key" VARCHAR(255) NOT NULL,
    value TEXT NOT NULL,
    source VARCHAR(50) NOT NULL DEFAULT 'file',
    created_at TIMESTAMP NOT NULL DEFAULT NOW(),
    updated_at TIMESTAMP NOT NULL DEFAULT NOW(),
    UNIQUE (locale, "group", namespace, "key")
);

Schema (SQLite)

CREATE TABLE IF NOT EXISTS translations (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    locale TEXT NOT NULL,
    "group" TEXT NOT NULL,
    namespace TEXT NOT NULL DEFAULT '',
    "key" TEXT NOT NULL,
    value TEXT NOT NULL,
    source TEXT NOT NULL DEFAULT 'file',
    created_at TEXT NOT NULL DEFAULT (datetime('now')),
    updated_at TEXT NOT NULL DEFAULT (datetime('now')),
    UNIQUE (locale, "group", namespace, "key")
);

Usage

use MonkeysLegion\I18n\Loaders\DatabaseLoader;

// Works with any PDO connection — auto-detects driver
$loader = new DatabaseLoader($pdo, 'translations');
$messages = $loader->load('en', 'messages');

// Stack with FileLoader (DB translations override file translations)
$translator = new Translator('en', 'en');
$translator->addLoader(new FileLoader('/path/to/lang'));  // Base
$translator->addLoader($loader);                           // Override

// Or use TranslatorFactory (recommended)
$translator = TranslatorFactory::create([
    'path' => '/path/to/lang',
    'pdo'  => $pdo,           // Enables DatabaseLoader automatically
]);

// Security features:
// ✅ Table name validated against regex pattern
// ✅ All queries use parameterized statements
// ✅ Cross-database UPSERT (ON CONFLICT / ON DUPLICATE KEY)
// ✅ Readonly PDO property

CacheLoader

Decorator that caches translations from any other loader.

use MonkeysLegion\I18n\Loaders\CacheLoader;

$cached = new CacheLoader(
    loader: $fileLoader,
    cache: $psr16Cache,
    ttl: 3600,
    prefix: 'i18n',
);

// Features:
// ✅ TTL jitter (±10%) to prevent thundering herd
// ✅ Selective cache invalidation
// ✅ Flush all cached translations

$cached->forget('en', 'messages');     // Clear specific group
$cached->flush();                       // Clear all

CompiledLoader

Opcache-friendly compiled PHP files — 10-50x faster than JSON decode.

use MonkeysLegion\I18n\Loaders\CompiledLoader;

$compiled = new CompiledLoader(
    sourceLoader: $fileLoader,
    compilePath: '/var/cache/i18n',
);

// Compile all translations for a locale
$compiled->compile('en', '/path/to/lang');
// → Creates /var/cache/i18n/en.compiled.php

// Check if compiled cache is fresh
if (!$compiled->isFresh('en', '/path/to/lang')) {
    $compiled->compile('en', '/path/to/lang');
}

// Invalidate
$compiled->invalidate('en');

// Atomic writes via temp file + rename
// Auto opcache invalidation

MlcLoader

Zero-dependency alternative to YAML — uses a simple key=value format.

use MonkeysLegion\I18n\Loaders\MlcLoader;

$loader = new MlcLoader('/path/to/lang');
$messages = $loader->load('en', 'messages');
// Loads from /path/to/lang/en/messages.mlc

MLC file format (flat — one group per file):

# resources/lang/en/messages.mlc

# Comments start with # or ;
welcome = Welcome!
greeting = Hello, :name!
farewell = "Goodbye, :NAME!"

# Dot notation creates nested arrays
nested.key = Nested value
nested.deep.value = Deep nested value

# Escape sequences in double-quoted values
multiline = "Line one\nLine two"

MLC file format (sectioned — multiple groups per file):

# resources/lang/es.mlc

[messages]
welcome = ¡Bienvenido!
greeting = ¡Hola, :name!

[validation]
required = El campo :field es obligatorio.
email = Ingrese un correo electrónico válido.

Why MLC over YAML?

  • Zero external dependencies (no symfony/yaml)
  • Simpler syntax, less error-prone
  • Translators don't need to learn YAML indentation rules
  • Same security hardening as FileLoader

Middleware

LocaleMiddleware

Auto-detect and set locale for each request.

use MonkeysLegion\I18n\Middleware\LocaleMiddleware;

$middleware = new LocaleMiddleware(
    manager: $localeManager,
    translator: $translator,
    setSession: true,
    setCookie: true,
    cookieTtl: 31536000, // 1 year
);

// Security features:
// ✅ SameSite=Lax on cookies
// ✅ HttpOnly flag
// ✅ Secure flag when HTTPS
// ✅ headers_sent() guard

LocaleUrlMiddleware

Extract locale from URL path segment.

use MonkeysLegion\I18n\Middleware\LocaleUrlMiddleware;

// /es/products → sets locale to "es"
$middleware = new LocaleUrlMiddleware($manager, $translator, segment: 0);

LocaleRedirectMiddleware

Auto-redirect to localized URL if locale prefix is missing.

use MonkeysLegion\I18n\Middleware\LocaleRedirectMiddleware;

// /products → 302 → /en/products
$middleware = new LocaleRedirectMiddleware($manager, segment: 0);

// No exit() call — returns redirect response array
// ✅ headers_sent() guard

Enums

PluralCategory

use MonkeysLegion\I18n\Enum\PluralCategory;

// ICU plural categories
PluralCategory::Zero;   // "zero"
PluralCategory::One;    // "one"
PluralCategory::Two;    // "two"
PluralCategory::Few;    // "few"
PluralCategory::Many;   // "many"
PluralCategory::Other;  // "other"

PluralCategory::Other->isDefault(); // → true
PluralCategory::ordered();          // Ordered list

// Get plural category for a count in a locale
$pluralizer = new Pluralizer();
$cat = $pluralizer->getCategoryForCount(5, 'en');
// → PluralCategory::Other

$cat = $pluralizer->getCategoryForCount(2, 'ar');
// → PluralCategory::Two

Direction

use MonkeysLegion\I18n\Enum\Direction;

Direction::LTR; // "ltr"
Direction::RTL; // "rtl"

Direction::fromLocale('en'); // → Direction::LTR
Direction::fromLocale('ar'); // → Direction::RTL
Direction::fromLocale('he'); // → Direction::RTL
Direction::fromLocale('fa'); // → Direction::RTL

Direction::RTL->cssAttribute(); // → 'dir="rtl"'

Attributes

#[Translatable]

Mark entity properties for automatic translation:

use MonkeysLegion\I18n\Attribute\Translatable;

class Product
{
    #[Translatable(group: 'products', keyPrefix: 'title')]
    public string $title;

    #[Translatable(group: 'products', keyPrefix: 'description', fallbackToValue: true)]
    public string $description;
}

#[Locale]

Auto-inject the detected locale into controller parameters:

use MonkeysLegion\I18n\Attribute\Locale;

class ProductController
{
    public function index(#[Locale] string $locale): Response
    {
        // $locale is auto-populated from the detected locale
    }
}

Events

use MonkeysLegion\I18n\Event\LocaleChangedEvent;

// Immutable event (readonly class)
$translator->setEventDispatcher(function (LocaleChangedEvent $event): void {
    echo $event->previousLocale; // → "en"
    echo $event->newLocale;      // → "es"

    // Update user preferences, reconfigure formatters, etc.
});

Template Directives

For use with MonkeysLegion-Template engine:

use MonkeysLegion\I18n\Template\I18nDirectives;

$directives = new I18nDirectives($translator);

// Register all directives
foreach ($directives->getDirectives() as $name => $handler) {
    $engine->directive($name, $handler);
}

In templates:

{{-- Translation --}}
@lang('welcome.message')
@lang('welcome.user', ['name' => $user->name])

{{-- Pluralization --}}
@choice('messages.count', $count)

{{-- Current locale --}}
@locale

{{-- Formatting --}}
@date($order->created_at, 'long')
@currency($product->price, 'USD')
@number($total, 2)

Helper Functions

Global helper functions for convenience:

// Translation
echo trans('messages.welcome');
echo trans('messages.greeting', ['name' => 'Yorch']);

// Shorthand alias (__)
echo __('messages.welcome');
echo __('messages.greeting', ['name' => 'Yorch']);

// Pluralization
echo trans_choice('messages.items', 5);
echo trans_choice('messages.items', 1, ['color' => 'red']);

// Get/set locale
echo lang();        // → "en"
lang('es');          // Sets locale to "es"
echo lang();        // → "es"

TranslatorFactory

One-line creation with all features:

use MonkeysLegion\I18n\TranslatorFactory;

// Basic
$translator = TranslatorFactory::create([
    'locale'   => 'en',
    'fallback' => 'en',
    'path'     => '/path/to/lang',
]);

// With caching
$translator = TranslatorFactory::create([
    'locale'    => 'en',
    'path'      => '/path/to/lang',
    'cache'     => $psr16Cache,
    'cache_ttl' => 3600,
]);

// With compiled loader (production)
$translator = TranslatorFactory::create([
    'locale'        => 'en',
    'path'          => '/path/to/lang',
    'compiled_path' => '/var/cache/i18n',
]);

// With database
$translator = TranslatorFactory::create([
    'locale'    => 'en',
    'path'      => '/path/to/lang',
    'pdo'       => $pdo,
    'cache'     => $cache,
]);

// With namespaces
$translator = TranslatorFactory::create([
    'locale'     => 'en',
    'path'       => '/path/to/lang',
    'namespaces' => [
        'billing' => '/path/to/billing/lang',
        'email'   => '/path/to/email/lang',
    ],
]);

// Full system (translator + manager)
['translator' => $t, 'manager' => $m] = TranslatorFactory::createSystem([
    'default'   => 'en',
    'supported' => ['en', 'es', 'fr'],
    'path'      => '/path/to/lang',
    'detectors' => ['url', 'session', 'cookie', 'header'],
]);

// Number & Date formatters
$nf = TranslatorFactory::createNumberFormatter();
$df = TranslatorFactory::createDateFormatter();

Translation Management

Full CRUD management for file and database translations:

use MonkeysLegion\I18n\Management\TranslationManager;

$manager = new TranslationManager($pdo, $translator, '/path/to/lang');

// CRUD
$manager->set('en', 'messages', 'welcome', 'Hello!');
echo $manager->get('en', 'messages', 'welcome'); // → "Hello!"
$manager->delete('en', 'messages', 'welcome');

// Import/Export
$manager->importFromFile('en', 'messages');
$manager->exportToFile('en', 'messages', 'json');
$manager->importArray('en', 'messages', ['key' => 'value'], overwrite: true);

// Sync
$manager->sync('en', 'messages', 'file_to_db');
$manager->sync('en', 'messages', 'db_to_file');

// Merged (DB overrides file)
$all = $manager->getAllMerged('en', 'messages');

// Search
$results = $manager->search('welcome', locale: 'en');

// Statistics
$stats = $manager->getStats();
// ['total' => 150, 'by_locale' => [...], 'by_group' => [...], 'by_source' => [...]]

// Batch update
$manager->batchUpdate([
    ['locale' => 'en', 'group' => 'messages', 'key' => 'welcome', 'value' => 'Hi!'],
    ['locale' => 'en', 'group' => 'messages', 'key' => 'goodbye', 'value' => 'Bye!'],
]);

// Find missing (file keys not in DB)
$missing = $manager->findMissing('en', 'messages');

CLI Commands

use MonkeysLegion\I18n\Console\TranslationCommand;

$cmd = new TranslationCommand($translator, '/path/to/lang');

// Extract translation keys from source code
$cmd->extract('/path/to/src', '/path/to/output.json');
// Scans for trans(), __(), @lang(), @choice() calls

// Find missing translations
$cmd->missing('es');
// ✗ Found 5 missing translations:
//   - messages.new_feature
//   - validation.custom_rule

// Compare two locales
$cmd->compare('en', 'es');
// Missing in es (3):
//   - messages.new_key
//   - validation.rule

// Export translations
$cmd->export('en', 'json', '/path/to/export.json');
$cmd->export('en', 'csv', '/path/to/export.csv');
$cmd->export('en', 'php', '/path/to/export.php');

Security

Path Traversal Prevention

// FileLoader validates all path segments
$loader->load('../etc', 'passwd');     // ❌ LoaderException
$loader->load("en\0", 'messages');     // ❌ LoaderException
$loader->load('en/../../', 'msg');     // ❌ LoaderException

Locale Injection Prevention

// Translator validates locale format: /^[a-z]{2,3}(_[A-Z]{2})?$/
new Translator('../etc/passwd');        // ❌ InvalidLocaleException
new Translator("en\0");                 // ❌ InvalidLocaleException
$translator->locale = '<script>';       // ❌ InvalidLocaleException

SQL Injection Prevention

// DatabaseLoader validates table names
new DatabaseLoader($pdo, 'DROP TABLE users; --'); // ❌ InvalidArgumentException

// All queries use parameterized statements

XSS Protection

// Enable auto-escaping in MessageFormatter
$formatter = new MessageFormatter(autoEscape: true);
// All :param replacements are HTML-escaped via htmlspecialchars()

Cookie Security

// LocaleMiddleware sets secure cookie flags
// SameSite=Lax, HttpOnly, Secure (when HTTPS)

Performance

Compiled Loader (Production)

// 10-50x faster than JSON decode per request
$compiled = new CompiledLoader($fileLoader, '/var/cache/i18n');
$compiled->compile('en', '/path/to/lang');

// Uses PHP's opcache for near-zero overhead
// Atomic writes (temp file + rename)
// Auto mtime-based freshness checks

Cache with Jitter

// TTL jitter prevents thundering herd (cache stampede)
// ±10% variation: TTL 3600 → random 3240-3960
$cached = new CacheLoader($loader, $cache, ttl: 3600);

Warm-Up

// Pre-load all groups at boot time
$translator->warmUp('en', ['messages', 'validation', 'auth', 'errors']);

Const Array Pluralizer

// Locale-to-rule mapping is a const array (PHP 8.4)
// Zero overhead — compiled into opcache
private const array LOCALE_RULES = [
    'pl' => 'polish',
    'ru' => 'russian',
    // ...
];

Migration from v1

Namespace Changes

- use MonkeysLegion\I18n\Contracts\LoaderInterface;
+ use MonkeysLegion\I18n\Contract\LoaderInterface;

Note: Backward-compatible aliases exist at src/Contracts/aliases.php.

Property Hooks

- $translator->getLocale();
+ $translator->locale;         // Read via property
+ $translator->getLocale();    // Still works (BC)

- $translator->setLocale('es');
+ $translator->locale = 'es';  // Write via property hook
+ $translator->setLocale('es'); // Still works (BC)

Locale Validation

// v1: No validation
$translator = new Translator('../etc/passwd');

// v2: Strict validation
+ $translator = new Translator('../etc/passwd');
+ // ❌ InvalidLocaleException

Middleware Split

// v1: All middleware in single file
- // src/Middleware/LocaleMiddleware.php contained 3 classes

// v2: One class per file
+ src/Middleware/LocaleMiddleware.php
+ src/Middleware/LocaleUrlMiddleware.php
+ src/Middleware/LocaleRedirectMiddleware.php

Testing

# Run all tests
composer test

# Run with testdox output
vendor/bin/phpunit --testdox

# Run specific test file
vendor/bin/phpunit tests/Unit/I18nV2Test.php

# Run specific test
vendor/bin/phpunit --filter="translator_translates_basic_key"

Test Coverage

  • 139 tests, 250+ assertions
  • Covers: Translator, Pluralizer, MessageFormatter, NumberFormatter, DateFormatter, LocaleManager, LocaleInfo, FileLoader, MlcLoader, CompiledLoader, Enums, Attributes, Events, Factory, CLI

License

MIT License. See LICENSE for details.

Built with ❤️ by MonkeysCloud

monkeyscloud/monkeyslegion-i18n 适用场景与选型建议

monkeyscloud/monkeyslegion-i18n 是一款 基于 PHP 开发的 Composer 扩展包,目前已累计 1.85k 次下载、GitHub Stars 达 1, 最近一次更新时间为 2025 年 05 月 24 日, 在 PHP 生态内属于活跃度较高的组件。

它主要适用于以下技术方向: 「i18n」 「translation」 「localization」 「internationalization」 「php84」 「monkeyslegion」 等业务场景。在实际项目中,围绕这些方向常见需要落地的问题包括:接口对接、性能调优、并发安全、与既有框架(Laravel / ThinkPHP / Yii / Webman 等)的兼容适配,以及生产环境的日志埋点与稳定性保障。

我们在过去多个企业项目中使用过 monkeyscloud/monkeyslegion-i18n 或与其功能相近的方案,如果你在选型或落地过程中遇到问题,例如 版本兼容、二次改造、私有化封装、与内部系统对接、生产 BUG 排查,欢迎联系我们协助评估。

围绕 monkeyscloud/monkeyslegion-i18n 我们能提供哪些服务?
定制开发 / 二次开发

基于 monkeyscloud/monkeyslegion-i18n 在你已有业务上做功能扩展、字段裁剪、UI 适配、与内部账号 / 权限 / 日志系统的深度对接。

BUG 修复 & 性能优化

线上偶发问题、内存泄漏、慢查询、并发异常等排查修复;针对高流量场景做缓存、队列、索引层面的调优。

项目外包 & 长期维护

承接完整的项目从需求 → 设计 → 开发 → 上线 → 长期运维;也可按月提供技术保姆服务。

yvsm@zunyunkeji.com QQ:316430983 微信:yvsm316 西安尊云信息科技 · 专注 PHP / Go / 分布式系统研发

统计信息

  • 总下载量: 1.85k
  • 月度下载量: 0
  • 日度下载量: 0
  • 收藏数: 1
  • 点击次数: 12
  • 依赖项目数: 1
  • 推荐数: 0

GitHub 信息

  • Stars: 1
  • Watchers: 0
  • Forks: 0
  • 开发语言: PHP

其他信息

  • 授权协议: MIT
  • 更新时间: 2025-05-24