Download the PHP package italix/i18n without Composer
On this page you can find all versions of the php package italix/i18n. It is possible to download/install these versions without Composer. Possible dependencies are resolved automatically.
Package i18n
Short Description Translation and formatting on ICU: CLDR plural categories, locale-aware numbers, money and dates, and a key extractor that refuses to guess
License MPL-2.0
Homepage https://github.com/italix-net/i18n
Informations about the package i18n
Italix I18n
Translation and formatting on ICU. Plural categories that are correct in Russian, numbers and money the way each locale writes them, and a key extractor that refuses to guess.
Why the rules come from ICU, and why ext-intl is still only suggested
Plural categories are a property of a language, not of a number. Russian has four, Arabic six,
Japanese one, and $n === 1 ? 'a' : 'b' is wrong for most of the world — including for 21 in
Russian, which takes the one category. So the answers are always ICU's.
They do not always come from ICU at runtime. The official php:8.x images ship without intl, so
requiring it was the friction everybody met in their first five minutes. When the extension is
present IcuEngine asks it; when it is not, PortableEngine reads tables baked from ICU by
ix i18n:bake and answers the same thing. Formatter::engine_code() says which one is answering, and
using_engine('portable') forces the other — on a machine with intl the portable branch would
otherwise never execute, and code that never executes is code that does not work.
The portable engine refuses what it cannot do honestly — collation, spellout, display names, non-Gregorian calendars — rather than approximating it. Those calls raise a message naming the extension. Everything this README shows below works on both.
Three ways a lookup goes wrong, and none of them break the page
| what you get | where you find it | |
|---|---|---|
| missing key | the key itself | problems() |
| unparseable message | the raw message | problems() |
| missing parameter | the placeholder | problems() |
A screen showing admin.users.title is obviously broken; one showing nothing is a mystery nobody can
locate. And throwing would turn a typo in a translation file into a 500 on a page that was otherwise
fine.
ix lang:check reports the same information across the whole codebase.
The apostrophe
ICU treats ' as a quoting character, so "L'utente non esiste" loses letters if it goes through
the parser. A message with no braces has nothing for ICU to do and is returned as-is — which is why
Italian and French messages survive.
Formatting
percent() does not round by default, and that is a deliberate departure. ICU's own percent
formatter uses zero fraction digits, so 0.075 comes out as 8% — measured. A rate quietly rounded
from 7.5 to 8 is the kind of wrong that reaches an invoice. Pass $decimals_n to narrow it.
Relative time is computed, not worded
PHP's ext-intl does not expose ICU's RelativeDateTimeFormatter — IntlRelativeDateTimeFormatter
does not exist. Wording it here would mean shipping translations for every language this library
claims to support, which is a promise it cannot keep. So it computes and your catalogue words it:
Locales
The chain falls back to a related language before the default one: an Austrian visitor is better served by German than by English.
Catalogues
lang/{locale}/{group}.php, each returning a nested array. messages.php is the default group and
contributes no prefix, so the common case reads admin.title rather than messages.admin.title and
a project that never needs a second group never meets the concept.
Nesting is flattened once at load. A boolean in a language file is refused, not cast — PHP turns
true into "1" and false into "", and either reaches production looking like a translation.
Printing into a page: the two halves of a message
A message has a template, which lives in a file in your repository, and parameters, which
routinely come from a column somebody filled in through a form. Only one of the two is trusted, and
get() returns a single string that cannot tell them apart:
The second is what a developer reaches for on the day a translator writes <strong>{customer}</strong>
and the page shows <strong> in plain sight. It looks like a rendering fix.
e() and choice_e() return Italix\Encode\Html, so composition is decided by the type rather than
by the caller remembering: H::e() around one is a no-op, and H::j() around one throws — a
JavaScript literal is not an HTML context, and quietly accepting one there is how & ends up
inside a string the browser shows to a user.
get(), choice() and format_message() are unchanged and still return plain strings. That is
deliberate: the same catalogue serves e-mail subjects, PDFs, CSV files and CLI output, where HTML
encoding would be a bug.
Counts go through the encoder like everything else. The obvious worry — that '3' breaks the plural
selector — was measured and is false: plural, number, currency and percent all coerce a
numeric string identically to the number, scientific notation included. The special case that had
been written for it was deleted.
One request routinely needs two languages — the page in the operator's, the e-mail in the
recipient's. in() is the primitive and returns a copy, so the surrounding page never moves. Through
the facade, where a shared partial has no $t to switch:
The restore is in a finally, because an exception on the way out is exactly the path that would
otherwise leave the rest of the request in the recipient's language — and it is the path nobody
writes a test for.
group() is the bulk form, for handing a whole branch to something that is not PHP — a client-side
validator that needs the sentence for every code a rule can return. Messages come back unformatted,
because the parameters are known at the other end.
The extractor
It does not evaluate anything. $t->get($key) and $t->get('admin.' . $suffix) are reported as
dynamic. A tool that guessed would produce a missing list containing keys that do not exist, and
that list would then be ignored — which is worse than not having one.
The receiver has to be named ($t, $translator, $i18n, $lang, configurable). Matching a
bare ->get('…') reported $view->get('main') and $form->get('email') as missing translations —
found by running it against a real codebase, not by reasoning about it.
Extractor::untranslated($en, $it) is the translator's worklist; identical() surfaces messages that
are the same in both, which is either a real cognate or a line somebody copied and never translated.
The tool cannot tell them apart and does not pretend to.
Requirements
php >= 7.4, ext-mbstring, italix/contracts, italix/encode. ext-intl is suggested: with it
you get every locale and every formatter, without it the baked tables. Tested against ICU 66.
italix/contracts is a require because Translator implements Italix\Contracts\Translator — an
interface that is missing at load time is a fatal error, not a degraded feature. It carries interfaces
only and requires nothing itself.
italix/encode is a require rather than a suggest because e() returns one of its types, and a
method that fatals when somebody calls it is not an optional feature. It requires only php >= 7.4
itself, so nothing transitive arrives — but it does ship bin/encode-lint, which will appear in
vendor/bin whether or not your project has templates.
All versions of i18n with dependencies
ext-mbstring Version *
italix/contracts Version ^2.0
italix/encode Version ^1.0