magicsunday / webtrees-module-base
Shared PHP base classes (date, name, image, place and fact processors plus module helpers) for the magicsunday/webtrees-* chart modules.
Package info
github.com/magicsunday/webtrees-module-base
pkg:composer/magicsunday/webtrees-module-base
Fund package maintenance!
Requires
- php: 8.3 - 8.5
- ext-dom: *
- ext-intl: *
- ext-mbstring: *
- fisharebest/webtrees: ~2.2.0
Requires (Dev)
- infection/infection: ^0.35.2
- magicsunday/coding-standard: ^3.0
- phpat/phpat: ^0.12.4
Suggests
- phpunit/phpunit: Needed to extend MagicSunday\Webtrees\ModuleBase\Testing\AbstractCatalogueStructureTestCase in the consuming module's own test suite
Provides
None
Conflicts
None
Replaces
None
README
webtrees-module-base
Shared PHP base classes for the magicsunday family of webtrees chart modules. Centralises the name, image and date processing logic, common models, and module helpers (GitHub release-version checking with file cache) so the chart modules do not have to reimplement the shared pieces.
This package ships no UI of its own — it is consumed as a Composer dependency by:
- webtrees-fan-chart — SVG ancestor fan chart
- webtrees-pedigree-chart — SVG pedigree chart
- webtrees-descendants-chart — SVG descendants chart
- webtrees-statistics — genealogy statistics dashboard (consumes only the shared core plus
IsoCountryMap)
Scope note: not every base component is consumed by every module. The statistics module uses only the shared core plus
IsoCountryMap, and the chart modules adopt the place, compact date and fact components as they need them. A component can live in the base before its second consumer exists, so that the consumer can adopt it without a namespace move. To see which module uses a class, search the sibling checkouts, for examplegrep -rl 'ModuleBase.Processor.PlaceProcessor' ../webtrees-*/src | grep -v '^../webtrees-module-base/'.
Requirements
- PHP 8.3 - 8.5 with extensions
dom,intlandmbstring - webtrees
~2.2.0
Installation
This package is pulled in automatically when you install any of the chart modules above. To depend on it directly from your own webtrees module:
composer require magicsunday/webtrees-module-base
If your module uses ImageProcessor (silhouette URL handling), declare the marker interface so the constructor's intersection type is satisfied:
use Fisharebest\Webtrees\Module\AbstractModule; use Fisharebest\Webtrees\Module\ModuleCustomInterface; use Fisharebest\Webtrees\Module\ModuleCustomTrait; use MagicSunday\Webtrees\ModuleBase\Contract\ModuleAssetUrlInterface; class Module extends AbstractModule implements ModuleCustomInterface, ModuleAssetUrlInterface { use ModuleCustomTrait; // ... }
ModuleCustomTrait already provides the required assetUrl() method — only the interface declaration is new.
What's inside
src/Processor/
DateProcessor— generation-aware date formatting (compactgetFormatted*/get*FullAPI for tight chart labels; locale-aware legacygetBirth*/getDeath*/getLifetimeDescriptionAPI for everything else; marriage dates only via the compact API:getFormattedMarriageDate(),getFormattedMarriageDateOfParents(),getMarriageDateFull()). The staticformatFactDate()gives the text of the date of any fact: compact for a complete date, the webtrees wording for a partial, qualified or ranged one and nothing for a date without a year, which webtrees cannot place on a calendarNameProcessor— name extraction from webtrees name HTML (DOM/XPath based — splits first/last/preferred, handles starredname, alternative and married names).getNameSegments()returns the name as ordered, drawableNameSegmentobjectsImageProcessor— highlight image + silhouette URL resolutionPlaceProcessor— place name shortening (configurable parts) for chart labelsFactResolver: resolves the facts a chart box shows, one slot per row except for a family tag. Slot 0 is the first existing birth equivalent and slot 1 the first existing death equivalent, then, when the caller asks for the optional facts, the marriage and the divorce of the families of the person unless the preference names them itself, and the optional tags of the tree preference "Other facts to show in charts". ReturnsFactViewobjects,nullfor a missing fact. A marriage or divorce tag (MARR,_NMR,DIV,ANUL,_SEPR) is read from the families of the person as spouse and gives one entry per family with the fact, naming the other spouse inpartner. The class docblock ofFactResolverholds the rules for a preference that names the marriage or the divorce itself, for the null entries and for the order. The value of a tag with free text, such asNOTE, is not shortened, so a chart that lists such a tag has to shorten the row itself
src/Model/
Symbols— backed enum for genealogical symbols (Birth ★, Death †, en-dash separator, MarriageDateUnknown sentinel)NameAbbreviation— backed enum +resolve()helper for the name-abbreviation strategy used in chart labels (auto / given-first / surname-first)PlaceStyle— enum for the way a place name is shortened (Full,Levels,CityCountry,CityIso2,CityIso3)PlaceFormatSpec—final readonlyvalue object holding a fully resolved place-formatting instruction (style, level count, from-which-end)PlaceFormatChoice— backed enum for the place-detail options a module offers in its configuration; label-free, so the consuming module supplies its own translationsNodeData— the person DTO of the linked tree charts (pedigree and descendants), serialised to JSON for the front end. An optionalfactslist ofFactViewornullentries, as theFactResolverreturns it, is appended to the payload only when it is not emptyFactView:final readonlyvalue object for one fact of a chart box (tag, label, date, place, full place, value, partner, sort day). The sort day is the Julian day number of the first day of the date as written, without regard to a qualifier such as "after" or "before", zero without a usable date, so a consumer can order facts by time. Label, date and partner are plain text, and so is the name of a linked record that stands in for a pointer value, the other fields are not reducedNameSegment/NameSegmentKind—final readonlyvalue object and backed enum for one drawable part of a name (given, surname, nickname, married)
src/Support/
CompactDateFormat— derives a locale-aware, compact (numeric) date format string from a locale's CLDR/ICU short-date pattern, forDateProcessor's compact APITextDirection— resolves script direction (LTR/RTL) for arbitrary stringsPlainText: reduces webtrees markup to plain text (strips the tags, decodes the entities)Locale/IsoCountryMap— maps free-text country names from GEDCOM PLAC lines to ISO-3166-1 codes, built onext-intl; used byPlaceProcessor's country-resolving styles
src/Module/
VersionInformation— checks GitHub releases for newer module versions, with file cache. No chart module references it directly; it is instantiated by this library's ownTraits\ModuleCustomTrait::customModuleLatestVersion()(which overrides webtrees' core method), and the webtrees control panel invokes that trait method
src/Traits/
ModuleCustomTrait— overrides webtrees'customModuleLatestVersion()to route throughVersionInformation, and provides theassetUrl()helper the marker interface declaresModuleChartTrait— shared chart-module helpers for the consuming modules
src/Facade/
ModuleAwareDataFacadeTrait/RouteAwareDataFacadeTrait— traits a chart module's DataFacade uses to receive the module instance and route access
src/Contract/
ModuleAssetUrlInterface— marker interface that declares webtrees'assetUrl()helper soImageProcessorcan be type-narrowed withoutmethod_existsruntime checks
src/Catalogue/
PunctuationResolver,CatalogueToolchainandFuzzyResolverCommandclear the fuzzy entries of a module's translation catalogues whose source text only changed in punctuation or spacing.msgmergepairs the old translation with a slightly changed source text and marks it fuzzy, andmsgfmtleaves fuzzy entries out of the compiled catalogue unless it is told to use them (see the gettext manual on fuzzy entries and onmsgfmt). Every other fuzzy entry stays fuzzy and is counted and, up to a limit per catalogue, listed for a person to judge. An entry with a context is listed with the context and a bar in front of its source text. A translation whose separator was swapped is listed as well, behind a tilde, because the swap reads the order of the separators and cannot prove that a separator of the translation stands for the same place as one of the source text. The gettext tools write the catalogue, so its layout stays the onemsgmergeproduces. Run it from the language pipeline of a module between the merge and the compilation, with the binary Composer installs for this package, shown here under the default bin directory:
vendor/bin/resolve-fuzzy resources/lang/messages.pot resources/lang/*/messages.po
It needs msgcat, msgattrib and msgmerge on the path. Resolution is the value object the resolver returns, ParsedEntry and ResolvedEntry carry the entries inside it, PrintableText filters control characters out of the report and CatalogueToolException is what a failing tool raises. The command reads every catalogue through msgcat --no-wrap first, so the resolver sees one canonical layout whatever layout the file had. A catalogue that gettext cannot read is named with a line of its own and left as it is, and so is one whose layout the resolver does not expect afterwards (CatalogueLayoutException), so run msgmerge over it first. A catalogue that cannot be rewritten, because its directory is not writable or a tool fails on it, is named the same way while the others go on. A catalogue that is missing, not readable or a link is named with a line of its own and left as it is, because the new file would replace a link, so name the file it points to. A rewritten catalogue keeps its permissions, and its owner and group where the process may set them. A comment block after the last entry belongs to no entry and is not kept when a catalogue is rewritten. Before a catalogue is read, it is merged once more from the template in a scratch copy, and a catalogue that this merge would change in any byte is refused the same way. To bring it into that layout, write it once with msgmerge --previous --output-file=<new file> <catalogue> <template> and replace the catalogue with the new file, because msgmerge --update leaves a file alone that it reads to the same entries. A catalogue that an older gettext wrapped differently, in long lines with a web address for example, is refused the same way until it is written once again. That covers a catalogue written by another tool or by hand, one laid out with other merge options, one merged from another template, and one with text the tools would cut or join when they read it, a NUL byte or a carriage return for example. The command exits with 1 as soon as one catalogue is refused or declined, and with 0 when every catalogue was processed, whatever stays fuzzy. When a separator of the source text was swapped and the translation holds a mark of the separator family with a space other than the plain one next to it (a no-break space for example), the entry stays fuzzy. Catalogues that are not UTF-8 and catalogues of tens of megabytes are outside what the command is built for, and nothing guards against them beyond the refusals above. The template has to be the one the catalogue was merged from. When it holds other entries, because a source text changed again or an entry is missing, or when the last merge would change or lose the translation of an entry or mark one fuzzy, the run names that catalogue, leaves it as it is and fails.
-
WriteTranslationsCommandwrites the translations a module keeps in one JSON file into its catalogues, with the right number of plural forms for each locale. The file maps a source text to its translation per locale, a plural text lists one string per form of the plural rule of the locale (a key with a context is written ascontextandmsgidjoined by U+0004):{ "Hello": {"de": "Hallo", "ru": "Привет"}, "%s item": {"de": ["%s Ding", "%s Dinge"], "ru": ["%s вещь", "%s вещи", "%s вещей"]} }vendor/bin/write-translations --core-owned=dev/core-owned.pot dev/translations.json resources/lang/messages.pot resources/lang/*/messages.poOnly an entry that has no translation yet is written. An entry that is fuzzy, that is already translated, that webtrees core translates itself (the optional
--core-ownedtemplate) or that the file holds nothing for is left alone. A plural entry whose strings do not match the form count of the locale is refused and named, and the run exits with 1. The locale is the name of the directory of the catalogue and the number of forms comes from the plural rule of that locale. A catalogue whose header declares another number of plural forms than the locale has is refused and named before anything is written. So is a catalogue whose header declares none, when a plural entry would be written to it. The run exits with 1 when anything was refused or a file it needs is unusable, with 2 when the arguments are too few, and otherwise with 0. The catalogues have to be what a merge with the template writes, as forresolve-fuzzy, and a second run changes nothing.
src/Testing/
AbstractCatalogueStructureTestCase, a PHPUnit test case that checks the translation catalogues a module ships. Extend it in the module'stests/and return the catalogue directory fromlanguageDirectory():
use MagicSunday\Webtrees\ModuleBase\Testing\AbstractCatalogueStructureTestCase; use PHPUnit\Framework\Attributes\CoversNothing; #[CoversNothing] final class CatalogueStructureTest extends AbstractCatalogueStructureTestCase { protected static function languageDirectory(): string { return __DIR__ . '/../resources/lang'; } }
webtrees merges a module's catalogue over the catalogue of webtrees itself, so a plural entry with the wrong number of forms or an empty form damages the translation of the same text everywhere in the installation. The checks guard against that. PHPUnit is a dev dependency of the consuming module. The subclass carries #[CoversNothing] because it covers no class. A phpunit.xml with requireCoverageMetadata="true" demands the attribute. A module whose catalogues carry no plural string overrides shipsPluralEntries() and returns false. The case then checks that no plural entry exists, so one added later fails until the override is removed.
Development
See AGENTS.md for the full development workflow, including the
make link-base sibling-clone pattern and the tooling-parity policy with
consumer modules.
Quick reference — this library has no module-local container; PHP runs through the webtrees-docker buildbox:
cd /path/to/webtrees-docker && docker compose run --rm buildbox bash -c \ 'cd /var/webtrees/app/vendor/magicsunday/webtrees-module-base && composer ci:test'
Substitute composer install, composer update, composer ci:cgl or
composer ci:rector for the last command as needed.
License
GPL-3.0-or-later — see LICENSE.