Search by

lsr / text-catalog

Heroyt

Standalone NEON source-copy catalog, gettext compiler, and optional framework integrations.

0.1.2 2026-09-08 12:12 UTC

This package is auto-updated.

Last update: 2026-09-08 12:12:55 UTC


README

lsr/text-catalog provides standalone NEON source-copy loading, source lookup, gettext compilation, and optional Nette/Symfony Console integration. Namespace: Lsr\TextCatalog\.

The independently versioned JavaScript package provides optional Vue/gettext helpers and Vite integration.

Installation

Install with Composer from Packagist:

composer require lsr/text-catalog

The standard Packagist installation needs no custom Composer repository. To use the optional Vue/Vite integration, install the independently versioned lsr-text-catalog npm package as well.

Requirements

  • PHP >=8.5, ext-dom, ext-libxml, nette/neon ^3.4, and gettext/gettext ^5.7.
  • No mandatory LSR framework, container, native gettext, Node, Vue, Redis, or application bootstrap dependency.
  • Optional DI: install nette/di ^3.2.
  • Optional command: install symfony/console ^7.4 || ^8.0; lsr/console ^0.2 can discover the registered service.
  • Optional native translation adapter: enable ext-gettext; the application configures locale, domain binding and encoding.

For local package development, use a Composer path repository with options.symlink: true and an explicit development version constraint. Consumer applications should normally use released packages rather than local links.

Standalone compilation

use Lsr\TextCatalog\CatalogConfig;
use Lsr\TextCatalog\TextCatalog;
use Lsr\TextCatalog\TextCatalogCompiler;
use Lsr\TextCatalog\TextCatalogLoader;

$root = __DIR__;
$config = new CatalogConfig(
    sourceDirectory: $root . '/copy',
    cacheFile: $root . '/var/catalog.php',
    languageDirectory: $root . '/languages',
    sourceRoot: $root,
    domain: 'example',
    locales: ['en_US'], // bootstrap with the source locale; add target locales after preparing their PO files
    sourceLocale: 'en_US',
    frontendDirectory: $root . '/generated', // null for PHP-only consumers
    // potFile: $root . '/languages/example.pot', // this is the default
);
$loader = new TextCatalogLoader($config->sourceDirectory);
$result = new TextCatalogCompiler($loader, $config)->compile();

$catalog = new TextCatalog($loader, $config->cacheFile, useCompiledCache: true);
echo $catalog->text('example.title');

A source file such as copy/example.neon:

example:
    title: 'Welcome %{name}'
    count:
        one: '%{count} item'
        plural: '%{count} items'
    rich:
        html: '<strong>%{name}</strong>'

Keys combine the relative directory/file namespace with nested NEON keys. Directory names and file basenames use camelCase; each file contains exactly one root matching its basename (example.neonexample). Source text is nonempty and validated; rich text requires an explicit html leaf, and plural groups use sibling one/plural leaves with matching placeholders. html, one, and plural are structural names, not ordinary nested key segments.

TextCatalogDefinition exposes texts, htmlKeys, plurals, and sourceFiles. TextCatalog exposes source lookup and HTML membership; it never translates, interpolates, checks freshness, or compiles during lookup. It memoizes the selected definition. Compiled PHP caches are trusted executable build output. Compile during deployment, and restart/reset application-owned service lifetimes as needed when replacing a catalog.

CompilationResult contains the definition and, when frontend generation is enabled, the generation digest and manifest path. Compilation maintains POT/PO catalogs and emits MO, PHP cache, and optional frontend artifacts. Translation contexts are semantic keys; plural contexts are the parent key. Existing translator content is merged rather than replaced with empty translations. Every configured locale must have complete, non-fuzzy active translations, including all required plural forms, and each translation must preserve the source placeholder set. The source locale is populated from canonical NEON copy.

Bootstrap with only the source locale to generate the POT, then prepare complete translator-owned target PO files before adding their locales to locales. Missing or fuzzy translations, missing plural forms and placeholder mismatches fail compilation before any outputs are published; a failed compile does not publish a starter target PO.

All content is validated and rendered before publication. Outputs are staged, the manifest is replaced last, and handled publication failures restore replaced outputs. Do not treat this as a distributed transaction across processes or machines; npm validates artifact digests and rejects an incomplete generation.

Frontend artifact contract

With frontendDirectory enabled, compilation emits:

  • catalog.ts: source lookup map, TextKey, HtmlTextKey, HTML membership, locale configuration and contextual translations.
  • catalog.compiled.ts: authoring key types, a declaration-only text macro, locale configuration and translations, without a runtime source lookup map.
  • catalog.build.json: format version 1, configuration identity, source/HTML/plural snapshot, SHA-256 artifact digests and a deterministic generation digest.

Generated types import lsr-text-catalog/types; generated files belong to the consumer. They are ordinary TypeScript files visible to tsc/vue-tsc before Vite runs. Declaration-only compiled macros require the compiled Vite transform; they are not callable JavaScript fallbacks. Regenerate frontend artifacts when upgrading the compiler or npm package rather than hand-editing generated import paths.

Generation hashing uses recursively key-sorted JSON with compact separators and unescaped Unicode, slashes and line terminators, excluding generation itself. Artifact digests cover exact bytes. The npm reader accepts format 1 and rejects missing/unknown versions, mismatched configuration, malformed references, tampering and mixed generations. Package versions remain independent of the artifact format.

Optional DI and console integration

extensions:
    textCatalog: Lsr\TextCatalog\Di\TextCatalogExtension

textCatalog:
    sourceDirectory: %appDir%/copy
    cacheFile: %appDir%/var/catalog.php
    languageDirectory: %appDir%/languages
    sourceRoot: %appDir%
    domain: example
    locales: [en_US, cs_CZ]
    sourceLocale: en_US
    frontendDirectory: %appDir%/generated
    useCompiledCache: true
    command: true

The extension registers the same config, loader, lookup and compiler services. command: true adds Lsr\TextCatalog\Console\CompileTextCatalogCommand, named texts:cache:compile. Register it with Symfony Console directly or let lsr/console discover it in the consumer container. The consumer owns its actual bin/console; the package does not bootstrap an application. Command descriptions and diagnostics require no catalog keys. Compilation errors produce a failure exit status.

Native translation and HTML

Lsr\TextCatalog\Translation\TextTranslator is an injected adapter, not a global helper autoload:

$translator = new Lsr\TextCatalog\Translation\TextTranslator(
    $catalog,
    domain: 'example',
    sanitizeHtml: $applicationSanitizer,
);

$label = $translator->langText('example.title', format: ['name' => 'Ada']);
$count = $translator->langText(
    'example.count.one', 'example.count.plural', 3, ['count' => 3],
);
$html = $translator->langHtmlText('example.rich', ['name' => $untrustedName]);

Configure native gettext (setlocale, environment where required, bindtextdomain, encoding) in application bootstrap. Named %{name} interpolation and numeric sprintf arguments are supported; mixed numeric/named argument sets are rejected. Plain strings remain unescaped data: use the consumer's normal escaped rendering.

HTML eligibility is checked, translation/interpolation runs, and then the application sanitizer processes the final output. Missing sanitizer rejects HTML calls. Source HTML validation is not a replacement for output sanitization. The package supplies no no-op sanitizer or application-specific policy. Native gettext locale/domain state is process-global: long-running applications must serialize/reset it appropriately; constructing this adapter does not isolate concurrent native locale changes.

Verify native locale reset on the deployment platform. On the macOS verification host, changing setlocale and environment variables alone retained a cached translation; explicitly changing the default textdomain invalidated that native cache in the sequential-request smoke check. This is application/process setup, not a portable per-request isolation guarantee supplied by the adapter. Use isolated workers when native global state cannot be reset safely.

Development and verification

composer install
composer validate --strict --no-check-publish
composer phpstan
composer test
composer cs
composer cs:fix # applies formatting; composer cbf is the conventional alias

Coding standards are defined in .php-cs-fixer.php, covering src, tests, and the configuration itself. PHPStan runs at level 8. Tests use synthetic catalogs, translations, directories and containers.

Agent skill

Use the lsr-text-catalog skill for source-copy ownership, compiler/build integration, translation adapters and the optional Vue/Vite companion.

npx skills add Heroyt/lsr-skills --skill lsr-text-catalog

The skill is guidance, not a runtime dependency; installing it does not install or publish either package.

License and maintenance

MIT, copyright (c) 2026 Tomáš Vojík. Source and issue tracking: Heroyt/lsr-text-catalog. Composer manifest versions match their Git release tags; they are independent of npm versions and artifact format versions.

Packagist registration and distribution through the LSR Satis repository are separate release operations. Creating a Git tag does not publish metadata to either channel.