libresign / jigsaw-localization
Localization and translation toolkit for Tighten Jigsaw with JSON catalogs, extraction, validation, and locale-aware routing
Requires
- php: ^8.3
- symfony/intl: ^6.0|^7.0|^8.0
Requires (Dev)
- bamarni/composer-bin-plugin: ^1.9
- tightenco/jigsaw: ^1.7
Suggests
- ext-intl: For locale display name resolution
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-09-28 21:19:50 UTC
README
Localization support for Jigsaw using JSON translation catalogs.
This project is a maintained fork of elaborate-code/jigsaw-localization, originally created by Elaborate Code.
The package provides:
- JSON translation loading for Jigsaw;
- locale-aware path and URL helpers;
- source-string collection and extraction;
- deterministic catalog read/write operations;
- translation catalog synchronization and validation.
Installation
composer require libresign/jigsaw-localization
Register the localization loader in bootstrap.php:
use LibreSign\JigsawLocalization\LoadLocalization; $events->beforeBuild([LoadLocalization::class]);
By default, translation catalogs are loaded from /lang.
Translation catalogs
Create one directory per locale:
lang/
├── en/
│ └── main.json
├── es/
│ └── main.json
└── pt-BR/
└── main.json
Each catalog is a flat JSON object whose keys are source strings:
{
"Good morning": "Bom dia",
"Sign document": "Assinar documento"
}
The source locale can use the source text as both key and value:
{
"Good morning": "Good morning",
"Sign document": "Sign document"
}
Translation files can be maintained manually or generated by any localization workflow.
Custom translation directory
use LibreSign\JigsawLocalization\LoadLocalization; $loader = new LoadLocalization('/path/to/translations'); $translations = $loader->load();
Custom localization loader
Projects that store translations somewhere other than JSON files can implement LocalizationLoader and inject it into the Jigsaw listener:
use LibreSign\JigsawLocalization\Contracts\LocalizationLoader; use LibreSign\JigsawLocalization\LoadLocalization; $loader = new class implements LocalizationLoader { public function load(): array { return [ 'en' => ['Hello' => 'Hello'], ]; } }; $localization = new LoadLocalization(loader: $loader);
Default locale
Set defaultLocale in Jigsaw's configuration:
return [ 'defaultLocale' => 'en', ];
If omitted, the default locale is en.
Locales are resolved from the locales actually loaded into the project. The package does not impose a fixed locale naming convention.
Translating strings
Use the __ helper to retrieve a translation:
echo __($page, 'Good morning');
Pass a locale explicitly when needed:
echo __($page, 'Good morning', 'pt-BR');
When no locale is provided, the package resolves it from the current page path and falls back to the configured default locale.
Locale-aware paths and URLs
current_path_locale()
Returns the locale resolved for the current page:
$currentLocale = current_path_locale($page);
translate_path()
Returns the equivalent page path for another locale:
translate_path($page, 'fr');
| Current path | Target locale | Result |
|---|---|---|
/contact |
fr |
/fr/contact |
/fr/contact |
en |
/contact |
/es/contact |
fr-CA |
/fr-CA/contact |
translate_url()
Equivalent to translate_path(), but returns a URL using Jigsaw's configured base URL:
translate_url($page, 'fr');
locale_path()
Builds a path for the current locale:
locale_path($page, '/contact');
locale_url()
Equivalent to locale_path(), but returns a URL:
locale_url($page, '/contact');
Source-string collection
For runtime collection, use SourceStringCollector:
use LibreSign\JigsawLocalization\Catalog\SourceStringCollector; $collector = new SourceStringCollector('en'); $collector->collect('en', 'Hello'); $collector->collect('pt-BR', 'Olá'); $sourceStrings = $collector->all(); // ['Hello' => 'Hello']
Only strings from the configured source locale are collected.
Source extraction
Extraction is based on a generic source/extractor contract.
A TranslationSource identifies the locale, source and contents to inspect. A TranslationStringExtractor implementation decides how strings are found inside that source.
use LibreSign\JigsawLocalization\Extraction\CallbackStringExtractor; use LibreSign\JigsawLocalization\Extraction\ExtractionPipeline; use LibreSign\JigsawLocalization\Extraction\TranslationSource; $extractor = new CallbackStringExtractor( fn (TranslationSource $source): bool => str_ends_with($source->identifier(), '.md'), fn (TranslationSource $source): array => [$source->contents()], ); $pipeline = new ExtractionPipeline('en', [$extractor]); $catalog = $pipeline->catalog([ new TranslationSource('en', 'posts/hello.md', 'Hello'), new TranslationSource('pt-BR', 'posts/ola.md', 'Olá'), ]);
The pipeline ignores non-source locales before invoking extractors, preventing translated content from being collected into the source catalog.
For reusable extraction logic, implement TranslationStringExtractor directly.
Catalog operations
Reading and writing JSON
use LibreSign\JigsawLocalization\Catalog\JsonTranslationCatalog; $catalog = new JsonTranslationCatalog('lang/en/main.json'); $catalog->write([ 'Good morning' => 'Good morning', ]); $translations = $catalog->read();
Catalog output is deterministic and uses JSON objects with UTF-8 content.
Synchronizing catalogs
use LibreSign\JigsawLocalization\Catalog\TranslationCatalogSynchronizer; $synchronizer = new TranslationCatalogSynchronizer(); $source = $synchronizer->source([ 'Hello', 'Goodbye', ]); $translation = $synchronizer->translation( $source, ['Hello' => 'Olá'], );
Existing translations are preserved. New source keys use the source text as fallback.
Obsolete translated keys are preserved by default. Pruning must be requested explicitly:
$translation = $synchronizer->translation( $source, $translation, pruneObsolete: true, );
Validating placeholders
use LibreSign\JigsawLocalization\Catalog\TranslationCatalogValidator; $validator = new TranslationCatalogValidator(); $errors = $validator->validatePlaceholders( ['%s signed %d documents' => '%s signed %d documents'], ['%s signed %d documents' => '%2$d documents signed by %1$s'], );
The validator supports positional printf/vsprintf placeholders so arguments can be reordered safely in translated text.
Safe catalog paths
use LibreSign\JigsawLocalization\Catalog\TranslationCatalogLocator; $locator = new TranslationCatalogLocator('lang'); $sourcePath = $locator->path('en'); $messagesPath = $locator->path('pt-BR', 'messages');
The locator rejects unsafe path segments while leaving locale naming policy to the project.