tinusg / syllable-counter
Counts and hyphenates the syllables of Dutch (and Dutch-spelled international) names and words
Requires
- php: ^8.2
- ext-mbstring: *
- illuminate/support: ^11.0|^12.0|^13.0
Requires (Dev)
- laravel/pint: ^1.0
- orchestra/testbench: ^9.0|^10.0|^11.0
- pestphp/pest: ^3.0|^4.0
README
Counts and hyphenates the syllables of Dutch — and Dutch-spelled international — names and words, without a dictionary, a pattern file or an API call. Pure spelling rules, one small class, no runtime dependencies beyond illuminate/support for the service provider.
$counter->count('Sophia'); // 3 $counter->hyphenate('Sophia'); // "So-phi-a" $counter->split('Sophia'); // ['So', 'phi', 'a']
Installation
composer require tinusg/syllable-counter
The service provider is auto-discovered. Publishing the config is optional:
php artisan vendor:publish --tag=syllable-counter-config
Usage
Resolve it from the container (recommended — it picks up your config) or just new it up:
use TinusG\SyllableCounter\SyllableCounter; class NameController { public function show(SyllableCounter $syllables, string $name) { return view('names.show', [ 'syllableCount' => $syllables->count($name), 'hyphenated' => $syllables->hyphenate($name), ]); } }
| Method | Returns | Example |
|---|---|---|
count(string $value, ?bool $muteFinalE = null): int |
Number of syllables, never less than 1 | count('Olivia') → 4 |
split(string $value, ?bool $muteFinalE = null): array |
The syllables, casing and accents intact | split('Sophia') → ['So', 'phi', 'a'] |
hyphenate(string $value, string $separator = '-', ?bool $muteFinalE = null): string |
The hyphenated word | hyphenate('Olivia') → 'O-li-vi-a' |
count() runs through split(), so counting and hyphenating can never disagree.
Spaces and hyphens are word boundaries; every word contributes its own syllables:
$counter->count('Anne-Marie'); // 4 $counter->split('Anne-Marie'); // ['An', 'ne', 'Ma', 'rie']
How it works
The whole thing rests on one rule: a syllable has exactly one vowel nucleus. So finding the nuclei and dividing the consonants between them gives you both the count and the hyphenation.
1. Normalise, but remember the accents
The word is lower-cased and diacritics are reduced to their base letter (é → e, ç → c). Positions that carried a diaeresis or an accent are flagged, because those mark a vowel that is pronounced on its own. Without that flag Chloë and Chloé would both read as the digraph oe and come out as one syllable; with it they correctly split into Chlo-ë and Chlo-é.
2. Find the vowel nuclei
Adjacent vowels are collected into runs, and each run is cut into nuclei with a greedy match on known vowel groups:
- Trigraphs —
aai,ooi,oei,eeu,ieu,eau→ one nucleus. - Digraphs —
aa,ee,oo,uu,ie,ei,ui,ou,au,oe,eu,ae,ai,ay,ey,oy,uy,ij,oi→ one nucleus. SoThijs,FleurandLoesare one syllable, andMarieis two. - Anything else adjacent is a hiatus — two nuclei. That is what makes
So-phi-a,Le-o,Mat-te-oandAn-to-ni-ocome out right; naive vowel-group counters collapse these into one. - A flagged vowel (step 1) always breaks a group open, so
Zoënever becomes one nucleus.
Two letters get special treatment:
yis a glide, not a vowel, when a vowel follows it —Ya-raandMa-ya(2), butLy-di-a(3), where theyis the nucleus.uafterqbelongs to the /kw/ consonant —Quin-ten(2),Mo-nique(2).
3. Divide the consonants: maximal onset
For the consonant cluster between two nuclei, as much of it as possible moves to the following syllable, as long as the part that moves is a valid Dutch onset (br, chr, kn, zw, th, …). A single consonant is always a valid onset, so V-CV is the default:
So-phie ph is a valid onset, so it moves along
An-dre-a dr is valid → An-dre-a, not And-re-a
I-sa-bel-la ll is not an onset → the split falls in the middle
Chris-ti-aan st is not an onset → only t moves
4. The final e
Whether a final e is pronounced is not a spelling question — Céline (mute) and Eline (pronounced) share the same -ine ending — so it cannot be derived from the letters. The package handles this in two layers:
- The safe heuristic. After a
corquthe Dutch schwa-e simply does not occur, so there theeis mute:Flo-rence(2),A-lice(2),Grace(1). Wider rules (-ne,-lle) are deliberately not applied, because they would breakHan-ne,Jel-leandE-li-ne. - Your own knowledge wins. Pass
$muteFinalEwhen you know the answer for that specific name — from a database column, an editor, a lookup, an LLM:
$counter->count('Céline', muteFinalE: true); // 2 → Cé-line $counter->count('Eline', muteFinalE: false); // 3 → E-li-ne $counter->count('Hatice', muteFinalE: false); // 3 → Ha-ti-ce, overriding the -ce rule
null (the default) means "no knowledge, use the heuristic". The flag applies to the last word only, so hyphenate('Marie-Alice', '-', true) gives Ma-rie-A-lice.
5. Exceptions, last
A handful of names break every rule Dutch spelling has — James is one syllable, Ga-bri-el splits where the digraph rule says it should not, Turkish -ce names pronounce their final e. Those live in a small exception list that is checked before the rules run.
An exception stores the syllables in lower case; the original is then cut at those same lengths, so capitals and accents survive: GABRIEL → GA-BRI-EL. If the exception does not match the length of the input, it is ignored and the normal rules apply.
Add your own in config/syllable-counter.php (an entry overrides a built-in one with the same key):
return [ 'exceptions' => [ 'ilse' => ['il', 'se'], ], ];
Or pass them straight to the constructor:
new SyllableCounter(['ilse' => ['il', 'se']]);
What it does not do
It produces orthographic hyphenation (So-phi-a), not a phonetic transcription, and it is tuned for names. The rules are Dutch, so an English word with an English-only spelling pattern can come out wrong. It is a rule set, not an oracle — when you need a specific word to be exact, add an exception.
Testing
composer test
The suite is a large table of real names — Dutch, Turkish, Arabic, French, Latin, English — with their expected counts and hyphenations. If you find a name that comes out wrong, a pull request with that name added to the table is the most useful thing you can send.
Thanks
This package was extracted from the code that powers two Dutch sites, and it exists because they needed it:
- Naampedia — baby names, meanings, origins and popularity. The syllable rules here were written for its name pages and tested against tens of thousands of real first names.
- Puzzelpedia — a puzzle dictionary and solving aid, where counting syllables and splitting words is daily business.
If this package is useful to you, a link back to naampedia.nl and puzzelpedia.nl — in your README, your credits page or wherever you list what you build on — would be very much appreciated. That is the whole price.
License
MIT. See LICENSE.