定制 gabepri/auto-html-i18n 二次开发

按需修改功能、优化性能、对接业务系统,提供一站式技术支持

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

gabepri/auto-html-i18n

Composer 安装命令:

composer require gabepri/auto-html-i18n

包简介

Server-side automatic translation for PHP-rendered HTML. Walks markup, masks variables, and resolves translations via a user-supplied backend. ICU MessageFormat supported.

README 文档

README

Server-side automatic translation for PHP-rendered HTML. Walks markup, masks dynamic values (numbers, dates, names, URLs, emails, symbols, inline tags) into stable cache keys, looks them up in a translation cache, and falls back to a user-supplied backend for cache misses. Returns translated HTML in a single synchronous pass — no async bookkeeping, no client-side JS required.

This is the PHP sibling of packages/js. Both packages share the same masking algorithm and a corpus of cross-port test fixtures so behavior stays identical.

Install

composer require gabepri/auto-html-i18n

Requires PHP 8.1+, the intl and mbstring extensions.

Quickstart

use AutoHtmlI18n\I18nTranslator;
use AutoHtmlI18n\TranslationItem;

$i18n = new I18nTranslator([
    'locale' => 'es',
    'onMissingTranslation' => function (array $items, string $locale): array {
        // $items: TranslationItem[] — one entry per unique missing key
        // Return: ['masked_key' => 'translation', ...]
        // Either look these up in your DB or hand them off to a translation service.
        return MyTranslationService::fetch($items, $locale);
    },
]);

$html = $i18n->translateHtml('<p>You have 5 apples</p>');
// → '<p>Tienes 5 manzanas</p>'

The onMissingTranslation callback is invoked once per translateHtml() call with the full deduplicated batch of unknown keys. Translations are cached in memory for the lifetime of the I18nTranslator instance — pass initialCache to seed it from your persistent store.

How masking works

Before lookup, each translatable string is normalized into a stable key with dynamic values replaced by {{N}} placeholders:

Input Cache key Variables
You have 5 apples You have {{0}} apples [{value: "5", type: "number"}]
Visit https://acme.com Visit {{0}} [{value: "https://acme.com", type: "url"}]
Email mary@acme.com Email {{0}} [{value: "mary@acme.com", type: "email"}]
Click <a href="/x">here</a> Click <a0>here</a0> [] (tag attrs preserved separately)
See <svg id="x9">…</svg> now See {{0}}…{{1}} now [{value: "<svg id=\"x9\">", type: "markup"}, …]
HELLO WORLD hello world (case is restored on output)

This means you only translate the abstract sentence shape onceYou have {{0}} apples works for any number — and your translations don't have to know about specific values.

Recognized value types, matched in priority order, are ignoreWord, url, email, date, number, and symbol — identical to the JS port. URLs and emails are matched ahead of dates and numbers so they mask as a single unit rather than fragmenting. Strings containing no Unicode letter are skipped outright rather than masked. None of these ever reach your translation backend, so ignoreAttribute is unnecessary for them; reserve it for letter-bearing content the masker cannot recognize by shape, such as personal names or user-authored prose.

Tags outside allowedInlineTags (an <input>, <svg>, <div>, …) are captured as opaque markup variables instead of being left in the key — so their volatile attributes (random ids, gradient refs) never destabilize the cache key, and the original markup is restored verbatim on output. Nested same-name inline tags (<span><span>…</span></span>) are matched opener-to-closer by a stack, so their indices never cross.

An element's markup is aggregated into a single translatable unit only when its entire descendant subtree is inline-allowed and it has direct interleaved text of its own. A pure container of inline elements with no direct text — a nav menu, link list, or button group — is treated as structural, so each child is translated on its own and keeps its own cache key rather than collapsing the whole container into one key.

API

new I18nTranslator(array $config)

Key Type Default Notes
locale string Required. Initial active locale.
onMissingTranslation callable Required. (TranslationItem[] $items, string $locale): array<string, string|array>
allowedInlineTags string[] ['a','b','i','u','strong','em','span','small','mark','del','sup','sub'] Tags that may appear inside translatable text and round-trip through translation.
translatableAttributes string[] ['title','placeholder','alt','aria-label'] Attributes to translate on every element.
ignoreSelectors string[] ['script','style','code'] Skip subtrees matching these. Tag names or [attr] form.
ignoreWords array [] Words preserved verbatim during masking. Plain strings or ['word' => 'X', 'meta' => [...]].
initialCache array<string,string|array> [] Pre-populated translations for the active locale.
originalAttribute string 'data-i18n-original' Reserved (currently unused server-side).
keyAttribute string 'data-i18n-key' When set on an element, overrides the masked key for that element's content.
ignoreAttribute string 'data-i18n-ignore' Setting this attribute on an element skips its subtree.
scopeAttribute string 'data-i18n-scope' Names a scope that scope-keyed translations resolve against.
skipUnrenderedValues bool true Never report strings a component painted before its data arrived ("Level undefined", "about NaN minutes", "results for ''"). See Half-rendered values.
isUnrenderedValue callable built-in (string $masked, string $original): bool. Overrides the half-rendered detection. Ignored when skipUnrenderedValues is false.
debug bool false When true, each TranslationItem includes a debug payload (DOM context).

Methods

// Walk an HTML fragment, translate everything translatable, return HTML.
$i18n->translateHtml(string $html): string

// Imperative: look up a pre-masked key and substitute positional variables.
// (No masking, no ICU — just {{N}} substitution.)
$i18n->translate(string $text, array $variables = [], ?string $scope = null): string

// Dry-run validation: check a translation string will render, the same way
// translateHtml() consumes it. See "Validating translations" below.
$i18n->validateIcu(string $translated, array $variables = [], ?string $locale = null): IcuValidationResult
$i18n->validateTranslation(string $original, string $translated, ?string $locale = null): IcuValidationResult

// Locale management
$i18n->getLocale(): string
$i18n->setLocale(string $locale): void

// Writing direction of a locale ('ltr'|'rtl' backed enum), defaulting to the
// instance locale. See "RTL support" below.
$i18n->getDirection(?string $locale = null): TextDirection

// Cache management
$i18n->setCache(string $locale, array $entries): void
$i18n->getCache(?string $locale = null): array
$i18n->clearCache(?string $locale = null): void
$i18n->getTranslation(string $key, ?string $locale = null): string|array|null

// IgnoreWords management
$i18n->getIgnoreWords(): array
$i18n->addIgnoreWords(array $words): void
$i18n->removeIgnoreWords(array $words): void
$i18n->setIgnoreWords(array $words): void

Half-rendered values

Markup rendered before its data arrived carries the stringified absence of the value — Level undefined, Read time about NaN minutes, No results found for ''. Those tokens are not numbers or dates, so masking bakes the broken value into the key as literal text ("Level undefined", not "Level {{0}}").

By default such text is rendered untranslated but never reportedonMissingTranslation never sees it, and nothing about the skip is cached, so the correct mask reports normally on the next render. A translation you have cached for such a key still applies; the gate is on reporting, not on lookup.

A mask counts as half-rendered when it contains undefined, null or NaN as a standalone word, or an empty quote pair ('', "", «», ‘’, “”). Word boundaries are respected, so Annulled contracts reports as usual.

// Turn the gate off entirely.
$i18n = new I18nTranslator([..., 'skipUnrenderedValues' => false]);

// Or keep it and supply your own predicate — here copy may legitimately say
// "null", but "undefined" is always a rendering artifact.
$i18n = new I18nTranslator([
    ...,
    'isUnrenderedValue' => fn(string $masked, string $original): bool
        => preg_match('/\bundefined\b/', $masked) === 1,
]);

Scoped translations

A translation entry can be either a plain string or a scope-keyed array. The walker resolves the active scope by walking up the DOM tree from the element containing the text and reading the data-i18n-scope attribute.

// Backend response
[
    'greeting' => [
        'formal' => 'Bienvenido',
        'casual' => '¡Hola!',
    ],
]

// HTML
'<div data-i18n-scope="formal"><p data-i18n-key="greeting">Hello</p></div>'
// → '<div data-i18n-scope="formal"><p data-i18n-key="greeting">Bienvenido</p></div>'

ICU MessageFormat

Translations may use ICU MessageFormat for plurals, gender, and conditional structure. PHP's built-in MessageFormatter (the intl extension) handles evaluation.

// Backend returns
['You have {{0}} apples' => '{0, plural, one {Tienes # manzana} other {Tienes # manzanas}}']

// "You have 1 apples" → "Tienes 1 manzana"
// "You have 5 apples" → "Tienes 5 manzanas"

When an ignoreWord carries metadata (e.g. ['word' => 'Mary', 'meta' => ['gender' => 'female']]), that metadata is exposed to ICU as {N_key} arguments — letting translations branch on {0_gender, select, female {...} other {...}}.

If an ICU pattern fails to parse or evaluate — including when it references a variable index or metadata key that doesn't exist (PHP's MessageFormatter would otherwise render a literal {1} and report success) — the affected text falls back to its original untranslated source. Neither the raw pattern nor unfilled placeholders are ever rendered into the output HTML.

Validating translations

To catch bad patterns before they reach your cache or backend responses, dry-run them the same way translateHtml() consumes them. Both methods return an IcuValidationResult (->valid, ->format, ->error, ->output):

use AutoHtmlI18n\VariableInfo;
use AutoHtmlI18n\VariableType;

$r = $i18n->validateIcu(
    '{0, plural, one {# oveja} other {# ovejas}}',
    [new VariableInfo('5', VariableType::Number)],
);
// $r->valid === true, $r->format === TranslationFormat::Icu, $r->output === '5 ovejas'

// Or derive the variables from a source string, using the instance's ignoreWords config:
$r = $i18n->validateTranslation('John has 3 cats', '{{0}} tiene {{1}} gatos');

format reports how the string will be consumed — icu (single-brace {0}), simple (double-brace {{0}} substitution), or plain — check it matches your intent. Invalid results carry an engine-specific error (malformed pattern, unfilled arguments, out-of-range {{N}} index).

RTL support

Right-to-left locales (Hebrew, Arabic, Persian, Urdu, …) work out of the box.

Document direction. The server owns the markup, so set dir/lang yourself using getDirection(). Direction is resolved from the locale tag: the language subtag decides (he, ar, fa, ur, …) and an explicit script subtag overrides it (az-Arab → rtl, ar-Latn → ltr). TextDirection::forLocale() is also available statically.

$i18n = new I18nTranslator(['locale' => 'he-IL', /* ... */]);

echo '<html dir="' . $i18n->getDirection()->value . '" lang="' . $i18n->getLocale() . '">';
// <html dir="rtl" lang="he-IL">

Bidi isolation of variables. When the target locale is RTL, values re-injected into {{N}} placeholders — numbers, dates, URLs, emails, and ignoreWords (typically Latin brand names) — are wrapped in Unicode first-strong isolate characters (U+2068…U+2069). Without this, the bidi algorithm can visually scramble LTR fragments inside an RTL sentence (e.g. a date like 12/31/2024 rendering reversed). The isolates are invisible, carried into the output HTML, and stripped again during masking, so cache keys stay stable.

Isolation applies to simple {{N}} substitution (and validateIcu/validateTranslation output). ICU patterns are rendered by the ICU engine as-is — add directional marks inside the pattern if a specific argument needs them.

What's deliberately different from the JS package

JS (browser) PHP (server)
Walks Live DOM via MutationObserver One HTML string per call
onMissingTranslation Async, possibly many calls (debounced/batched) Sync, exactly one call per translateHtml()
Pending state Yes — text is replaced when async callback resolves No — translation always completes before serialization
Re-render on locale change Re-walks the DOM in place Caller re-runs translateHtml() on cached source
ICU locale handling Intl strictly validates BCP 47 tags; ill-formed locales degrade stepwise (es-41esund) so the translation still renders. und resolves to the runtime's default locale. ICU accepts any locale id natively (es-41, es_419, even garbage) and resolves through its own fallback chain, ending at ICU's root locale. Both ports always render; only the plural rules chosen for a wholly invalid locale can differ (runtime default vs. root).
Output-side bookkeeping attributes data-i18n-original / data-i18n-pending written to elements Not used (single-pass output)
Document direction (RTL) Optional manageDirection config keeps dir/lang on the live document in sync Caller embeds getDirection() into the markup it renders

Both ports honor the same data-i18n-* input attributes (-key, -scope, -ignore).

Development

composer install
vendor/bin/phpunit

Tests include a fixture-driven suite (tests/FixtureTest.php) that runs the same Masker assertions as the JS package against the shared corpus in fixtures/masker/. Adding a fixture there exercises both ports immediately.

License

MIT

gabepri/auto-html-i18n 适用场景与选型建议

gabepri/auto-html-i18n 是一款 基于 PHP 开发的 Composer 扩展包,目前已累计 73 次下载、GitHub Stars 达 0, 最近一次更新时间为 2026 年 05 月 02 日, 在 PHP 生态内属于活跃度较高的组件。

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

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

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

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

BUG 修复 & 性能优化

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

项目外包 & 长期维护

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

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

统计信息

  • 总下载量: 73
  • 月度下载量: 0
  • 日度下载量: 0
  • 收藏数: 0
  • 点击次数: 37
  • 依赖项目数: 0
  • 推荐数: 0

GitHub 信息

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

其他信息

  • 授权协议: MIT
  • 更新时间: 2026-05-02