Skip to content

About

Unicode MessageFormat 2 for PHP: the syntax, data model, pattern selection and default function registry, validated against the Unicode working group's own conformance suite.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

meraki/messageformat

Unicode MessageFormat 2 for PHP.

MF2 formats one message, already in the target language, against some data and a locale. It is not a translation system and not a catalogue: message ids, resource files and locale lookup are deliberately outside its specification, which is why they live in a separate package (meraki/messageformat-resource) tracking the W3C's message resource work.

The specification became Stable in CLDR 47 (March 2025). There is no other PHP implementation: ext-intl binds MessageFormat 1, and MF2 lives in icu::message2, which the extension does not expose.

Status: in development. The scanner, the grammar's character classes and the error hierarchy exist. The parser, data model, selection engine and function registry do not yet. It is not usable and not released.

Working on it

Everything the tests need — PHP 8.5, ext-intl, a pinned ICU, Xdebug, Composer — comes from .devcontainer/. Nothing has to be enabled in a global php.ini.

In VS Code: Reopen in Container. Dependencies install on create.

Without VS Code:

docker compose run --rm dev composer ci      # everything CI runs
docker compose run --rm dev composer test    # just the suite
docker compose run --rm dev bash             # a shell

Why a container and not a local PHP

This library's correctness is partly defined by a data version. ICU carries CLDR, CLDR carries the plural rules and the number and date formats, and some of the conformance suite's expected strings are CLDR data rather than library logic. "Whatever ICU this machine happens to have" is therefore not a good enough answer, so the version is declared in a file next to the code.

The trap that makes this concrete: on Alpine, icu-libs depends on a virtual icu-data package that either icu-data-en (English only, 2.9 MB) or icu-data-full (31 MB) can satisfy. Get the former and nothing fails — intl loads, INTL_ICU_VERSION reports correctly, and every non-English locale quietly falls back to root. Czech would select one/other instead of one/few/many/other, so the plural tests would be wrong rather than red. composer test:environment exists to catch exactly that, and runs first in CI.

The ICU matrix

ICU version is the only knob, because it comes from the base image's distribution:

service base ICU CLDR role
dev php:8.5-cli-trixie 76 46 development and the CI gate
icu72 php:8.5-cli-bookworm 72 42 oldest supported
icu78 php:8.5-cli-alpine3.24 78 48 newest; where ICU4J's MF2 sits
docker compose run --rm icu72 composer test
docker compose run --rm icu78 composer test

Only dev is a gate. The other two are drift detection: when an assertion passes on one ICU and fails on another, the difference is CLDR data rather than this library, and such an assertion belongs in the icu PHPUnit group. CI marks the matrix continue-on-error for that reason — a difference there is a review prompt, not a broken build.

No distribution packages ICU 77 (CLDR 47), the release MF2 stabilised in. That is accepted rather than worked around: the conformance fixtures are generated by the working group's JavaScript implementation, not by ICU, so pinning ICU 77 would not make them more authoritative — it would only change what :number and :date print.

The conformance suite

tests/fixtures/conformance/ is a verbatim copy of the Unicode working group's own fixtures, pinned to one commit in tests/fixtures/conformance/UPSTREAM. Do not edit it.

docker compose run --rm dev composer suite:update   # refresh and re-pin
docker compose run --rm dev composer suite:check    # fail if the pin is behind upstream

A copy rather than a submodule, so that git clone plus composer install is enough to run the tests, and so that an upstream change shows up in review as the expectations that moved rather than as one line of hex. suite:check runs weekly on a schedule instead of on every push, because upstream moving is not a reason to fail somebody's unrelated commit.

tests/fixtures/conformance/message.abnf is pinned from the same commit, and tests/Syntax/AbnfAgreementTest.php re-derives every character class from it and fails if Syntax\CodePoints has drifted — so the grammar cannot quietly disagree with the transcription of it.

One thing the container cannot catch

A Docker bind mount on Windows inherits the host filesystem's case-insensitivity, so a path that differs from another only by case resolves to the wrong one inside the container and the tests still pass. Linux CI is the only place that surfaces. It has already happened once, between tests/conformance/ and a planned tests/Conformance/, which is why the fixtures now live under tests/fixtures/. Treat a green local run as necessary and not sufficient.

Licence

MIT.

About

Unicode MessageFormat 2 for PHP: the syntax, data model, pattern selection and default function registry, validated against the Unicode working group's own conformance suite.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages