PHPackages                             celema/verba - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Localization &amp; i18n](/categories/localization)
4. /
5. celema/verba

ActiveLibrary[Localization &amp; i18n](/categories/localization)

celema/verba
============

Gettext-style internationalization for PHP without the gettext extension

0.4.0(1mo ago)055↓66.7%MITPHPPHP ^8.5

Since Jul 13Pushed 1w agoCompare

[ Source](https://github.com/celemas/verba)[ Packagist](https://packagist.org/packages/celema/verba)[ Docs](https://celema.dev/verba)[ RSS](/packages/celema-verba/feed)WikiDiscussions main Synced 2w ago

READMEChangelogDependencies (4)Versions (6)Used By (0)

Celema Verba
============

[](#celema-verba)

[![ci](https://camo.githubusercontent.com/6d31360ebc4b38ddfba80e3bf2372c5d9e06e0d763938d12b8124be96ce37f4f/68747470733a2f2f636f6465666c6f652e636f6d2f63656c656d612f76657262612f6261646765732f776f726b666c6f77732f63692e796d6c2f62616467652e7376673f7374796c653d666c6174266c6f676f3d666f7267656a6f266c6f676f436f6c6f723d7768697465266c6162656c3d6369)](https://codefloe.com/celema/verba/actions)[![code coverage](https://camo.githubusercontent.com/ad22d32953e3730b4a24305c3f4ad8dfbc7acfc57b735cacde911412ad5449a7/68747470733a2f2f696d672e736869656c64732e696f2f656e64706f696e743f75726c3d6874747073253341253246253246636f762e63656c656d612e64657625324663656c656d612532467665726261253246636f646525324662616467652e6a736f6e)](https://cov.celema.dev/celema/verba/code)[![type coverage](https://camo.githubusercontent.com/1caf9dabe9d4ed13ea8cfcc9fb55f935f2316e436c0f18dedae23fb4d78fa605/68747470733a2f2f696d672e736869656c64732e696f2f656e64706f696e743f75726c3d6874747073253341253246253246636f762e63656c656d612e64657625324663656c656d612532467665726261253246747970657325324662616467652d636f7665722e6a736f6e)](https://cov.celema.dev/celema/verba/types)[![psalm level](https://camo.githubusercontent.com/51ec5e37e9647a35b546cd07746593a2839c09ff9f4136466c323191d542c18d/68747470733a2f2f696d672e736869656c64732e696f2f656e64706f696e743f75726c3d6874747073253341253246253246636f762e63656c656d612e64657625324663656c656d612532467665726261253246747970657325324662616467652d6c6576656c2e6a736f6e)](https://cov.celema.dev/celema/verba/types)[![Software License](https://camo.githubusercontent.com/7013272bd27ece47364536a221edb554cd69683b68a46fc0ee96881174c4214c/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6c6963656e73652d4d49542d626c75652e737667)](LICENSE.md)

Gettext-style internationalization for PHP without the `gettext` extension.

Verba keeps the workflow you know from gettext — mark strings in code, extract them, translate, ship — but drops the parts that make the extension awkward to operate:

- **No server restarts.** Catalogs are plain PHP array files served from opcache; editing one takes effect on the next request.
- **No system locales.** Plural rules ship with Verba; nothing to install with `dpkg-reconfigure locales`.
- **Works in CLI and CI.** Extraction and status checks are pure PHP.

The interchange format is plain PHP arrays, not `.po`/`.mo`, and there is no fuzzy matching: a changed message id is simply a new entry.

Verba translates messages; it does not format numbers, dates, or currency. That is localization, and PHP already has the ICU-backed tool for it — `ext-intl` (`NumberFormatter`, `IntlDateFormatter`, `MessageFormatter`), the PHP counterpart to Babel. Verba deliberately stays out of it: formatting needs CLDR data and the intl extension, which would undo its "no extension, no system locales" premise. Pair the two when you need both.

Marking strings
---------------

[](#marking-strings)

Eight global functions are always available:

```
__('Save');                                      // simple
__n('one file', '%d files', $count);             // plural
__p('button', 'Open');                           // context
__np('inventory', 'one file', '%d files', $count);
__d('shop', 'Checkout');                         // explicit domain
__dn('shop', 'one order', '%d orders', $count);
__dp('shop', 'button', 'Open');                  // domain + context
__dnp('shop', 'inventory', 'one item', '%d items', $count);
```

Arguments interpolate in one of two styles. A single array argument fills named `:placeholder` tokens; anything else is passed to `sprintf`:

```
__('Hello :name', ['name' => $user->name]);
__('Found %d results', $count);
```

In `__n`/`__np`/`__dn`/`__dnp`, `:count` is bound to the count automatically, so `__n(':count file', ':count files', $n)` needs no extra argument.

A context distinguishes uses of the same message id without becoming part of the output. For example, German may translate `__p('menu', 'Open')` as `Öffnen` but `__p('state', 'Open')` as `Offen`. Context is an exact lookup axis: a miss never falls back to an uncontextual translation or another context.

With no translator active the functions return the message id itself (after interpolation), so calls are safe in tests, CLI, and early boot.

### Escaping

[](#escaping)

Verba does not escape anything — it cannot, since the same catalog feeds HTML, attributes, JSON, and the terminal, and only the boundary that emits a string knows the right encoding. Two rules follow:

- **Translations are trusted, author-controlled content.** A `msgstr` may contain markup (``, links) on purpose; Verba passes it through untouched. Your template decides whether the result is rendered as raw HTML.
- **Placeholder values are inserted verbatim.** A `:name` value goes in as-is, so escape untrusted input for its output context at the call site: `__('Hi :name', ['name' => htmlspecialchars($user->name)])` for HTML, and no escaping for CLI.

Runtime
-------

[](#runtime)

A `Translator` is bound to one locale and an ordered cascade of domains, each mapping to the directory that holds its catalog files. The first domain with a translation wins; a miss falls back to the message id.

```
use Celema\Verba\Translator;
use Celema\Verba\Verba;

$translator = new Translator('de', [
    'app' => __DIR__ . '/i18n',       // application strings, searched first
    'cosray' => $cosrayDir . '/i18n', // framework strings
]);

Verba::activate($translator);         // wire the global functions
// ... handle the request ...
Verba::deactivate();                  // reset (matters for long-running workers)
```

`__d('cosray', …)` and the other `__d*` helpers pin the `cosray` domain; bare helpers search the cascade. A domain selects a catalog, while a context selects a translation variant inside each catalog.

A third argument names fallback locales, tried in order whenever the primary locale lacks a string. Resolution is per id and stays within a domain before the cascade continues, so the domain cascade outranks the locale fallback:

```
$translator = new Translator('es', $domains, ['en', 'de']); // es → en → de → id
```

Locale ids may contain ASCII letters, digits, hyphens, and underscores, for example `zh-Hant` or `pt_BR`. They become catalog filename segments.

This keeps a partially translated locale usable — untranslated ids surface in `en` instead of as raw message ids. `Translator::exportMany()` ships locale-specific payload entries in resolution order, each with its own plural rule; entries with no reachable messages are omitted. The JavaScript runtime therefore resolves the same chain. Fallback is a runtime concern only: extraction and `i18n:status` stay per catalog file, so an empty `es` catalog still reports as untranslated.

Catalog files
-------------

[](#catalog-files)

One file per domain and locale, named `..php`. See the [catalog format reference](docs/catalog-format.md) for the complete schema and its design rationale.

```
