Twig adapter for PHP-Typography — smart quotes, dashes, ellipses, hyphenation, widow protection, fraction glyphs, ordinal suffixes, math symbols, CSS hooks for styling.
- PHP 8.3+
- Twig 3 or 4
- Symfony YAML 6, 7, or 8 (always installed as a hard dependency, parsing the bundled
typography.yml— memoised per path, so it costs at most one parse per file no matter how many times the filter runs — plus, when the constructor receives a project.ymlfile path, that file too. The package's own settings apply on every render either way; parsing is lazy, applying is not)
composer require parisek/twig-typographyuse Parisek\Twig\TypographyExtension;
use Twig\Environment;
use Twig\Loader\FilesystemLoader;
$twig = new Environment(new FilesystemLoader('/path/to/templates'));
// House policy only — no per-language typesetting.
$twig->addExtension(new TypographyExtension());
// — or — house policy + per-language typesetting, resolved fresh on every call:
$twig->addExtension(new TypographyExtension('', fn () => $currentLocale));
// — or — layer a project settings file on top:
$twig->addExtension(new TypographyExtension(__DIR__ . '/typography.yml', fn () => $currentLocale));
// — or — layer a PHP array on top instead (no filesystem):
$twig->addExtension(new TypographyExtension([
'set_smart_quotes' => true,
'set_smart_dashes' => true,
], fn () => $currentLocale));The second constructor argument is a locale resolver — a callable returning the
current locale as a string (cs_CZ, de-CH, a bare cs, …). It is invoked on
every |typography call, not cached, so a request that changes locale
mid-render (e.g. rendering two languages of the same page) always typesets each
call in the right one. Pass null (the default) to skip the language layer
entirely. A resolver that throws degrades to no language layer for that call
rather than breaking the render.
{{ title|typography }}
{{ "Lorem ipsum"|typography }}
{# Override constructor defaults for one call: #}
{{ title|typography({ set_smart_dashes: false }) }}The filter is is_safe: html — its output may contain <sup>, <span class="…">,
and similar markup, and is emitted unescaped.
The package ships one bundled settings file — typography.yml, at the
package root — beyond the Settings class defaults. It carries:
- A house policy — top-level keys that are a house decision rather than a property of any one language (e.g. unit spacing on, dewidowing off, language-neutral smart-quote/dash defaults). Applied on every render, regardless of what you pass in.
- Thirteen per-language tables, under its
languages:key — quote styles, dash conventions, single-character word spacing, and other settings that genuinely vary by language. Coverscs,sk,pl,de,de-CH,en,en-GB,fr,ru,sl,hr,hu,tr. Looked up from the locale resolver (see above) viaLocaleResolver::candidates(), which resolves the region/script-qualified tag and the bare language, then layers them — e.g.de_CHmergesde-CHoverdeover the global section, sode-CHonly needs to state the keys that genuinely differ fromde. An unrecognised language yields no language-specific overrides; the house policy's own neutral defaults still apply. Dutch and Portuguese are deliberately not included: their quote conventions are not settled enough to ship (mixed practice in Dutch; European vs. Brazilian Portuguese disagree) — see the CHANGELOG for the full rationale.
A project's own settings — passed as $config, either a YAML file path or a
PHP array — use the exact same shape: global keys at the top level, plus
an optional languages: map keyed by language tag. This is what makes a
single-language override possible without touching any other language:
# typography.yml — project override, layered on top of the house policy and
# the resolved language table
set_hyphenation: true # this project wants CSS-independent hyphenation, applies to every language
languages:
cs:
set_smart_quotes_primary: "doubleGuillemets" # this project prefers «…» for Czech specifically
# every other cs setting (secondary quotes, single-character word
# spacing, dashes, …) still comes from the package's own cs table —
# languages: is merged per key, not replaced wholesale
# every other language (en, de, pl, …) is completely unaffected by the cs
# override aboveEvery key — in your own file/array, and in the bundled table — becomes a
method call on
PHP-Typography's Settings class.
A key that doesn't match a Settings method (a typo, or a key from a newer
PHP-Typography version this package hasn't caught up to) is silently skipped
rather than fataling the render; languages itself is never passed through —
it's a document-structure key, not a setting.
Later layers win on a per-key basis; a layer that doesn't touch a key leaves
the earlier value in place. languages: overrides are themselves additive
per key — an entry only needs to state what departs from the global section
above it. That extends across the language/region boundary too: for a
regional locale like de_CH, step 3 below resolves as its own two-layer
merge — de first, de-CH layered on top — before the rest of the chain
continues, so a regional entry only needs the keys that differ from its
base language.
1. PHP-Typography's own Settings(true) defaults
2. typography.yml (bundled) — global section, house policy, every render
3. typography.yml (bundled) languages — resolved from the locale resolver, base language then region
4. $config — your constructor argument, global section
5. $config languages — your constructor argument, resolved the same base-then-region way
6. |typography({ ... }) — per-call arguments
You do not need to write a settings file just to typeset one of the thirteen covered languages — pass a locale resolver and steps 1–3 already produce a correct result. Write your own file (or array) only when your project departs from the house style: a different quote character, hyphenation switched on, a language the table doesn't cover, or a one-off override that should apply to every call rather than just one.
This extension exposes PHP-Typography as one Twig filter, |typography.
There's no {% typography %} block tag (despite earlier versions of this
README claiming one — the tag was never implemented in code). To apply
typography to a block of HTML, wrap it in an element and apply the filter
to the rendered string, or define a Twig macro
that encapsulates the pattern you want.
GPL-2.0-or-later, see LICENSE.txt.