Search by

migears / i18n

samxxu

Minimalist internationalization library — array and gettext translations, plus timezone-aware date rendering

2.0.0 2026-10-01 13:24 UTC

This package is auto-updated.

Last update: 2026-10-01 14:03:17 UTC


README

Version

A minimalist internationalization (i18n) translation library. Zero mandatory dependencies, PHP 8.1+, based on PHP array translation files, with optional gettext support.

Background: miGears is the open-source successor of TinyGears, a self-developed PHP framework. It was renamed and open-sourced recently because the name TinyGears is already taken in the open-source community.

Features

  • Zero mandatory dependencies - Works out of the box, no extensions required
  • Minimalist API - new ArrayTranslator($translations) is all you need
  • PHP array translation files - No .mo/.po files, easy to understand and maintain
  • Variable interpolation - translate('HELLO_USER', ['user' => 'Alice']) → "Hello, Alice"; values must be scalar, Stringable or null, anything else throws instead of printing Array
  • Multi-domain support - Organize translations by module
  • Text object - Deferred translation text object with JSON serialization support
  • Localized dates - LocalizedDate presents a timestamp in the viewer's timezone, rendering through the translator
  • English by default - Returns the key itself as fallback when translation is not found
  • Thoroughly unit-tested core API - Every environment-independent path has a test; gettext tests skip themselves when the extension or the locale is missing

Boundaries

In scope

  • TranslatorInterface::translate() and its two drivers: ArrayTranslator over flat or per-domain PHP array files, and the optional GettextTranslator when ext-gettext is installed.
  • Key lookup with %param% interpolation (values must be scalar, Stringable or null), multi-domain selection, and returning the key itself as the fallback when no translation is found.
  • Deferred text via Text (with JSON serialization), driver selection via TranslatorFactory, and timezone-aware rendering of the date.* keys by LocalizedDate (serialization stays raw data).

Not in scope (by design)

  • Choosing the locale — no HTTP request, Accept-Language, cookie or session parsing; the caller picks the locale and injects the translator.
  • Routing and URL localization — no route table, no path matching, nothing that maps a request to a language.
  • Persisting or caching translations — no database, file store or cache backend; that belongs to migears/dao / migears/sql and migears/cache.
  • Logging, and any dependency beyond PHP itself (ext-gettext is optional) — logging belongs to migears/log.

Installation

composer require migears/i18n

Optional: install ext-gettext to use GettextTranslator.

Quick Start

Basic Usage

use MiGears\I18n\ArrayTranslator;

$translator = new ArrayTranslator([
    'HELLO' => 'Hello',
    'HELLO_USER' => 'Hello, %user%',
    'WELCOME_BACK' => 'Welcome back, %user%! You have %count% new messages.',
]);

echo $translator->translate('HELLO');                          // Hello
echo $translator->translate('HELLO_USER', ['user' => 'Alice']); // Hello, Alice
echo $translator->translate('WELCOME_BACK', [
    'user' => 'Bob',
    'count' => 5,
]); // Welcome back, Bob! You have 5 new messages.

Loading Translations from PHP Files

$translator = ArrayTranslator::fromFile('/path/to/translations.php');

Translation file translations.php:

<?php
return [
    'HELLO' => 'Hello',
    'HELLO_USER' => 'Hello, %user%',
];

Multiple Domains

$translator = new ArrayTranslator([
    'messages' => [
        'HELLO' => 'Hello',
    ],
    'errors' => [
        'NOT_FOUND' => 'Page not found',
        'FORBIDDEN' => 'User %user% is not authorized',
    ],
]);

echo $translator->translate('HELLO', [], 'messages');        // Hello
echo $translator->translate('NOT_FOUND', [], 'errors');      // Page not found
echo $translator->translate('FORBIDDEN', ['user' => 'admin'], 'errors'); // User admin is not authorized

Text Object (Deferred Translation)

use MiGears\I18n\Text;

$text = new Text('HELLO_USER', ['user' => 'Alice']);

// Inject translator later
$text->setTranslator($translator);

echo $text; // Hello, Alice

Text objects support JSON serialization/deserialization, making it easy to pass translatable text in APIs:

$json = json_encode($text); // {"key":"HELLO_USER","params":{"user":"Alice"},"domain":null}
$restored = Text::fromJson(json_decode($json, true));

Gettext Support (Optional)

Requires the ext-gettext extension:

Process-wide side effects: the constructor calls putenv(), setlocale(LC_ALL, ...), bindtextdomain() and textdomain(). gettext is not instance-isolated, so two GettextTranslator instances with different locales in the same process overwrite each other. Set the locale once at bootstrap rather than switching per request, and prefer ArrayTranslator unless you actually need gettext catalogues.

LANGUAGE outranks locale:: GNU gettext consults the LANGUAGE environment variable before LC_ALL/LANG, so where LANGUAGE is already set the locale: argument does not decide which catalogue is read — with LANGUAGE=de_DE, an en_US.UTF-8 translator falls back to returning the key. This class does not touch LANGUAGE; unset it if locale: is meant to be authoritative.

use MiGears\I18n\GettextTranslator;

$translator = new GettextTranslator(
    defaultDomain: 'messages',
    locale: 'zh_CN.UTF-8',
    directory: '/path/to/locale',
);

$translator->addDomain('errors', '/path/to/locale');

echo $translator->translate('HELLO_USER', ['user' => 'Alice']);

Driver Factory

Use TranslatorFactory to create a translator from a configuration array — ideal when the driver choice depends on runtime config or environment:

use MiGears\I18n\TranslatorFactory;

// Array driver
$translator = TranslatorFactory::create([
    'driver' => 'array',
    'translations' => [
        'HELLO' => 'Hello',
        'HELLO_USER' => 'Hello, %user%',
    ],
]);

// Array driver — load from file
$translator = TranslatorFactory::create([
    'driver' => 'array',
    'file' => '/path/to/translations.php',
]);

// Gettext driver
$translator = TranslatorFactory::create([
    'driver'    => 'gettext',
    'locale'    => 'zh_CN.UTF-8',
    'domain'    => 'messages',
    'directory' => '/path/to/locale',
]);

Localized Dates

LocalizedDate binds a timestamp to a viewer's timezone and translator. It reports neutral facts and renders text through the translator, so the class itself carries no language.

Rendering is for the server — use it in a template. Serialization stays raw, because a payload carrying rendered text would lock the client into this server's language:

echo $createdAt->humanize();   // 今天 10:30
echo $createdAt->relative();   // 2 小时前

echo $createdAt;               // "2026-09-21 14:30" — locale-neutral

echo json_encode(['created_at' => $createdAt]);
// {"created_at":{"timestamp":1758450600,"iso":"2026-09-21T14:30:00+08:00","timezone":"Asia/Shanghai"}}
use MiGears\I18n\ArrayTranslator;
use MiGears\I18n\LocalizedDate;

$translator = new ArrayTranslator([
    'date.weekday.0' => '周日',   // ... through date.weekday.6
    'date.humanize.today' => '今天 %time%',
    'date.humanize.yesterday' => '昨天 %time%',
    'date.humanize.tomorrow' => '明天 %time%',
    'date.humanize.weekday' => '%weekday% %time%',
    'date.humanize.date' => '%month%月%day%日 %time%',
    'date.humanize.dateOtherYear' => '%year%年%month%月%day%日 %time%',
    'date.relative.past.hour' => '%count% 小时前',
    // ... the rest of the date.relative.* keys
]);

$createdAt = new LocalizedDate($row['created_at'], $timezone, $translator);

The keys it looks up:

Key Params Used for
date.relative.{direction}.{unit} %count% direction is past or future; unit is moment, minute, hour, day, week, month, year
date.weekday.{0-6} — A weekday name, 0 = Sunday; feeds date.humanize.weekday
date.humanize.today / .yesterday / .tomorrow %time% The three days around now
date.humanize.weekday %weekday%, %time% The recent past, within six days
date.humanize.date %month%, %day%, %time%, %date% Older than that, still in the current year
date.humanize.dateOtherYear %year%, %month%, %day%, %time%, %date% Anything older

%month% and %day% arrive as unpadded numbers, so 3月15日 does not turn into 03月15日; %date% carries the ISO Y-m-d string for tables that prefer it. relative() and humanize() throw a LogicException when no translator was supplied; relativeParts() and humanizeParts() need none.

API Reference

TranslatorInterface

interface TranslatorInterface
{
    public function translate(string $key, array $params = [], ?string $domain = null): string;
}

ArrayTranslator

// Constructor - supports flat array (single domain) or nested array (multiple domains)
new ArrayTranslator(array $translations, string $defaultDomain = 'messages');

// Load from PHP file
ArrayTranslator::fromFile(string $file, string $defaultDomain = 'messages'): self;

// Translate
$translator->translate(string $key, array $params = [], ?string $domain = null): string;

Text

new Text(string $key, array $params = [], ?string $domain = null);

$text->setTranslator(TranslatorInterface $translator): self;
$text->getKey(): string;
$text->getParams(): array;
$text->getDomain(): ?string;
(string) $text; // triggers translation

// JSON serialization
$text->jsonSerialize(): array;
Text::fromJson(array $data): self;

TranslatorFactory

// Create from config array — driver: "array" or "gettext"
TranslatorFactory::create(array $config): TranslatorInterface;

LocalizedDate

Method Description
new LocalizedDate($input = null, $timezone = null, $translator = null) Bind a timestamp to a viewer's timezone and translator
LocalizedDate::fromTimestamp($ts, $tz = null, $translator = null) Create from a Unix timestamp
LocalizedDate::fromString($str, $tz = null, $translator = null) Create from a datetime string
relative() Rendered distance from now, e.g. 2 小时前
humanize() Rendered friendly timestamp, e.g. 今天 10:30
relativeParts() Language-free distance from now
humanizeParts() Language-free bucket for a friendly timestamp
toDateTime() / timestamp() Underlying DateTimeImmutable / Unix timestamp
toDateString() / toDateTimeString() / format($pattern) Locale-neutral formatting
dayOfWeek() 0 (Sun) - 6 (Sat)
isToday() / isYesterday() / isTomorrow() Comparison in the date's own timezone
withTimezone($tz) Convert timezone (immutable; translator carried over)
timezone() Get current timezone
jsonSerialize() Raw timestamp / iso / timezone, never rendered text
__toString() Locale-neutral Y-m-d H:i

Design Principles

  • No singletons - Translators are plain objects, freely instantiable and injectable
  • No global state - Does not depend on any Context or Registry. The core API (ArrayTranslator, Text, TranslatorFactory) touches no process state; only the optional GettextTranslator driver does, because the native extension itself is process-wide (see Gettext Support above)
  • No logging dependency - Does not log anything, letting the caller decide how to handle it
  • English by default - Translation keys themselves are in English, returning the key directly when no translation is found
  • Optional translator injection - Text objects work even without a translator (returns key + interpolation)

Testing

composer install
./vendor/bin/phpunit

License

MIT

migears/i18n

Version

极简国际化(i18n)翻译库。零强制依赖,PHP 8.1+,基于 PHP 数组的翻译文件,也可选支持 gettext。

特性

  • 零强制依赖 - 开箱即用,不需要任何扩展
  • 极简 API - new ArrayTranslator($translations) 就能用
  • PHP 数组翻译文件 - 不用 .mo/.po,易于理解和维护
  • 变量插值 - translate('HELLO_USER', ['user' => 'Alice']) → "Hello, Alice";参数值须为标量、Stringable 或 null,其余类型直接抛异常而不是输出 Array
  • 多 domain 支持 - 按模块组织翻译
  • Text 对象 - 可延迟翻译的文本对象,支持 JSON 序列化
  • 本地化日期 - LocalizedDate 按用户时区呈现时间戳,并通过翻译器渲染文案
  • 默认英文 - 找不到翻译时返回 key 本身作为降级
  • 核心 API 测试充分 - 所有不依赖环境的路径都有测试覆盖;gettext 相关测试在扩展或 locale 缺失时自动跳过

边界

范围内

  • TranslatorInterface::translate() 及其两个驱动:基于扁平或按 domain 分组的 PHP 数组翻译文件的 ArrayTranslator,以及安装 ext-gettext 后可选使用的 GettextTranslator。
  • key 查找与 %param% 插值(值须为标量、Stringable 或 null)、多 domain 选择,以及找不到译文时返回 key 本身的降级。
  • 通过 Text 延迟翻译(含 JSON 序列化)、通过 TranslatorFactory 选择驱动,以及由 LocalizedDate 按用户时区渲染 date.* 文案(序列化保持原始数据)。

范围外(刻意不做)

  • 决定 locale —— 不解析 HTTP 请求、Accept-Language、cookie 或 session;locale 由调用方选定并注入翻译器。
  • 路由与 URL 本地化 —— 没有路由表、没有 path 匹配,也不负责把请求映射到某种语言。
  • 持久化或缓存译文 —— 不访问数据库、文件存储或缓存后端;这些属于 migears/dao / migears/sql 与 migears/cache。
  • 日志,以及 PHP 之外的任何依赖(ext-gettext 为可选扩展)—— 日志属于 migears/log。

安装

composer require migears/i18n

可选:安装 ext-gettext 以使用 GettextTranslator。

快速开始

基本用法

use MiGears\I18n\ArrayTranslator;

$translator = new ArrayTranslator([
    'HELLO' => '你好',
    'HELLO_USER' => '你好,%user%',
    'WELCOME_BACK' => '欢迎回来,%user%!你有 %count% 条新消息。',
]);

echo $translator->translate('HELLO');                          // 你好
echo $translator->translate('HELLO_USER', ['user' => 'Alice']); // 你好,Alice
echo $translator->translate('WELCOME_BACK', [
    'user' => 'Bob',
    'count' => 5,
]); // 欢迎回来,Bob!你有 5 条新消息。

从 PHP 文件加载翻译

$translator = ArrayTranslator::fromFile('/path/to/translations.php');

翻译文件 translations.php:

<?php
return [
    'HELLO' => '你好',
    'HELLO_USER' => '你好,%user%',
];

多 Domain

$translator = new ArrayTranslator([
    'messages' => [
        'HELLO' => '你好',
    ],
    'errors' => [
        'NOT_FOUND' => '页面未找到',
        'FORBIDDEN' => '用户 %user% 无权访问',
    ],
]);

echo $translator->translate('HELLO', [], 'messages');        // 你好
echo $translator->translate('NOT_FOUND', [], 'errors');      // 页面未找到
echo $translator->translate('FORBIDDEN', ['user' => 'admin'], 'errors'); // 用户 admin 无权访问

Text 对象(延迟翻译)

use MiGears\I18n\Text;

$text = new Text('HELLO_USER', ['user' => 'Alice']);

// 稍后注入翻译器
$text->setTranslator($translator);

echo $text; // 你好,Alice

Text 对象支持 JSON 序列化/反序列化,便于在 API 中传递可翻译文本:

$json = json_encode($text); // {"key":"HELLO_USER","params":{"user":"Alice"},"domain":null}
$restored = Text::fromJson(json_decode($json, true));

Gettext 支持(可选)

需要 ext-gettext 扩展:

进程级副作用:构造函数会调用 putenv()、setlocale(LC_ALL, ...)、bindtextdomain() 和 textdomain()。gettext 并非实例隔离,同一进程内两个不同 locale 的 GettextTranslator 实例会互相覆盖。建议在引导阶段一次性设定 locale,不要按请求切换;除非确实需要 gettext 的 .mo 目录,否则优先使用 ArrayTranslator。

LANGUAGE 的优先级高于 locale::GNU gettext 在 LC_ALL/LANG 之前先查 LANGUAGE 环境变量,因此当 LANGUAGE 已被设置时,locale: 参数并不决定读取哪个目录—— 例如 LANGUAGE=de_DE 时,一个 en_US.UTF-8 的翻译器会直接回退为返回 key。本类不会改动 LANGUAGE;若希望 locale: 成为权威值,请先将其 unset。

use MiGears\I18n\GettextTranslator;

$translator = new GettextTranslator(
    defaultDomain: 'messages',
    locale: 'zh_CN.UTF-8',
    directory: '/path/to/locale',
);

$translator->addDomain('errors', '/path/to/locale');

echo $translator->translate('HELLO_USER', ['user' => 'Alice']);

驱动工厂

使用 TranslatorFactory 通过配置数组创建翻译器——适合驱动选择取决于运行时配置或环境的场景:

use MiGears\I18n\TranslatorFactory;

// Array 驱动
$translator = TranslatorFactory::create([
    'driver' => 'array',
    'translations' => [
        'HELLO' => '你好',
        'HELLO_USER' => '你好,%user%',
    ],
]);

// Array 驱动 — 从文件加载
$translator = TranslatorFactory::create([
    'driver' => 'array',
    'file' => '/path/to/translations.php',
]);

// Gettext 驱动
$translator = TranslatorFactory::create([
    'driver'    => 'gettext',
    'locale'    => 'zh_CN.UTF-8',
    'domain'    => 'messages',
    'directory' => '/path/to/locale',
]);

本地化日期

LocalizedDate 把时间戳绑定到用户的时区与翻译器。它给出中立的事实,文案则通过翻译器渲染,因此类本身不含任何语言。

渲染用于服务端——在模板里调用即可。序列化保持原始数据,因为载荷里带着渲染后的文案,等于把客户端锁死在服务端的语言上:

echo $createdAt->humanize();   // 今天 10:30
echo $createdAt->relative();   // 2 小时前

echo $createdAt;               // "2026-09-21 14:30" — 与语言无关

echo json_encode(['created_at' => $createdAt]);
// {"created_at":{"timestamp":1758450600,"iso":"2026-09-21T14:30:00+08:00","timezone":"Asia/Shanghai"}}
use MiGears\I18n\ArrayTranslator;
use MiGears\I18n\LocalizedDate;

$translator = new ArrayTranslator([
    'date.weekday.0' => '周日',   // ... 到 date.weekday.6
    'date.humanize.today' => '今天 %time%',
    'date.humanize.yesterday' => '昨天 %time%',
    'date.humanize.tomorrow' => '明天 %time%',
    'date.humanize.weekday' => '%weekday% %time%',
    'date.humanize.date' => '%month%月%day%日 %time%',
    'date.humanize.dateOtherYear' => '%year%年%month%月%day%日 %time%',
    'date.relative.past.hour' => '%count% 小时前',
    // ... 其余 date.relative.* 键
]);

$createdAt = new LocalizedDate($row['created_at'], $timezone, $translator);

它会查找的键:

键 参数 用途
date.relative.{direction}.{unit} %count% direction 为 past 或 future;unit 为 moment、minute、hour、day、week、month、year
date.weekday.{0-6} — 星期名,0 为周日;供 date.humanize.weekday 使用
date.humanize.today / .yesterday / .tomorrow %time% 今天前后三天
date.humanize.weekday %weekday%、%time% 近期过去,六天内
date.humanize.date %month%、%day%、%time%、%date% 更早,且仍在当年
date.humanize.dateOtherYear %year%、%month%、%day%、%time%、%date% 更早的年份

%month% 与 %day% 传入的是不补零的数字,因此 3月15日 不会被写成 03月15日;%date% 提供 ISO 的 Y-m-d 字符串备用。未注入翻译器时 relative() 与 humanize() 抛 LogicException;relativeParts() 与 humanizeParts() 不需要翻译器。

API 参考

TranslatorInterface

interface TranslatorInterface
{
    public function translate(string $key, array $params = [], ?string $domain = null): string;
}

ArrayTranslator

// 构造函数 - 支持扁平数组(单 domain)或嵌套数组(多 domain)
new ArrayTranslator(array $translations, string $defaultDomain = 'messages');

// 从 PHP 文件加载
ArrayTranslator::fromFile(string $file, string $defaultDomain = 'messages'): self;

// 翻译
$translator->translate(string $key, array $params = [], ?string $domain = null): string;

Text

new Text(string $key, array $params = [], ?string $domain = null);

$text->setTranslator(TranslatorInterface $translator): self;
$text->getKey(): string;
$text->getParams(): array;
$text->getDomain(): ?string;
(string) $text; // 触发翻译

// JSON 序列化
$text->jsonSerialize(): array;
Text::fromJson(array $data): self;

TranslatorFactory

// 从配置数组创建 — driver: "array" 或 "gettext"
TranslatorFactory::create(array $config): TranslatorInterface;

LocalizedDate

方法 说明
new LocalizedDate($input = null, $timezone = null, $translator = null) 把时间戳绑定到用户时区与翻译器
LocalizedDate::fromTimestamp($ts, $tz = null, $translator = null) 从 Unix 时间戳创建
LocalizedDate::fromString($str, $tz = null, $translator = null) 从日期时间字符串创建
relative() 渲染后的时间距离,如 2 小时前
humanize() 渲染后的友好时间戳,如 今天 10:30
relativeParts() 与当前时间的距离,不含语言
humanizeParts() 友好时间戳所属的分组,不含语言
toDateTime() / timestamp() 底层 DateTimeImmutable / Unix 时间戳
toDateString() / toDateTimeString() / format($pattern) 与语言无关的格式化
dayOfWeek() 0 (周日) - 6 (周六)
isToday() / isYesterday() / isTomorrow() 按对象自身时区比较
withTimezone($tz) 转换时区(不可变,翻译器随之携带)
timezone() 获取当前时区
jsonSerialize() 原始 timestamp / iso / timezone,绝不含渲染文案
__toString() 与语言无关的 Y-m-d H:i

设计原则

  • 没有单例 - 翻译器是普通对象,可自由实例化和注入
  • 没有全局状态 - 不依赖任何 Context 或 Registry。核心 API(ArrayTranslator、Text、TranslatorFactory)不触碰任何进程状态;只有可选的 GettextTranslator 驱动会,因为原生扩展本身就是进程级的(见上文 Gettext 支持)
  • 没有日志依赖 - 不记录日志,让调用方决定如何处理
  • 默认英文 - 翻译 key 本身就是英文,找不到翻译时直接返回 key
  • 翻译可选注入 - Text 对象在没有翻译器时也能工作(返回 key + 插值)

测试

composer install
./vendor/bin/phpunit

License

MIT