greek_romanizer
Configurable Greek → Latin romanization. One context-aware engine, three presets, every letter choice overridable.
import 'package:greek_romanizer/greek_romanizer.dart';
void main() {
final official = GreekRomanizer.official();
print(official.romanize('ευχαριστώ').text); // efcharisto
final english = GreekRomanizer.englishReader();
print(english.romanize('ευχαριστώ').text); // efcharistó
// "efharisto" reads easier for an English speaker than "efcharisto":
final easier = GreekRomanizer.englishReader(chiLetter: 'h');
print(easier.romanize('ευχαριστώ').text); // efharistó
}
Presets
| Preset | For | Notes |
|---|---|---|
GreekRomanizer.official() |
ASCII, official-style transliteration | close to ELOT 743 / ISO 843 (Greek passports, road signs), no accent marks |
GreekRomanizer.englishReader() |
an English speaker | phonetic respelling — collapses vowel digraphs, voices μπ/ντ/γκ at word start |
GreekRomanizer.germanReader() |
a German speaker | not a copy of englishReader — β, ζ, ξ, σ and γ's front/back split all diverge, because German reading habits differ from English ones |
Why official carries no accents by default. Real ELOT 743 output on a
passport or road sign doesn't show stress. Every romanizer still returns
RomanizedText.stressed — a list of StressSpan offsets into the plain
text — so a caller who wants to highlight the stressed syllable can, even
without a rendered accent mark. Pass showStress: true on a preset (via
copyWith, see below) to render the accent instead.
Why official is orthographic and the readers are phonetic. ELOT is
letter-for-letter: Πειραιάς transliterates to Peiraias, which an
unprimed reader says wrong — the word is pronounced pireas. The reader
presets respell for pronunciation instead (Pireás), at the cost of no
longer matching the passport spelling.
Overriding the rules
Every letter choice is a field on RomanizationSpec. Each preset factory
exposes the fields that are safe to tweak without breaking the scheme's own
identity:
final GreekRomanizer easier = GreekRomanizer.englishReader(chiLetter: 'h');
For full control, build a RomanizationSpec from scratch or copyWith a
preset's spec and hand it to GreekRomanizer.custom:
final spec = RomanizationSpec.englishReader.copyWith(
xLetter: 'x', // read ξ as x instead of ks
showStress: false, // drop accent marks, keep the phonetic letter choices
);
final GreekRomanizer custom = GreekRomanizer.custom(spec);
Whole-word overrides
For loanwords or names the rule table gets wrong, skip the engine for that word entirely:
final romanizer = GreekRomanizer.official(
wordOverrides: {'γιαούρτι': 'yaourti'},
);
romanizer.romanize('γιαούρτι').text; // yaourti
Matching is case-insensitive on the Greek key; the override's leading letter
is re-cased to match the input (Γιαούρτι → Yaourti).
Rule reference
Vowels
| Greek | official (literal) | reader (phonetic) |
|---|---|---|
| α ε η ι ο ω | a e i i o o | a e i i o o |
| υ (alone) | y | i |
| αι | ai | e |
| ει, οι, υι | ei, oi, yi | i |
| ου | ou | u |
αυ / ευ / ηυ are context-sensitive in every preset: voiced (av /
ev / iv, using vLetter) before a vowel or a voiced consonant (β γ δ ζ λ μ ν ρ), voiceless (af / ef / if) before a voiceless consonant (θ κ ξ π σ τ φ χ ψ) or at word end.
αύριο → ávrio αυτό → aftó
Ευρώπη → Evrópi ευχαριστώ → efcharistó
Consonants
| Greek | Field | official | englishReader | germanReader |
|---|---|---|---|---|
| β | vLetter |
v | v | w |
| ζ | zLetter |
z | z | s |
| ξ | xLetter |
x | ks | x |
| χ | chiLetter |
ch | ch | ch |
| θ | thetaLetter |
th | th | th |
| ψ | psiLetter |
ps | ps | ps |
| δ | deltaLetter |
d | dh | dh |
| γ (before ε ι η υ αι) | gammaFrontLetter |
g (never splits) | y | j |
| γ (elsewhere) | — | g | gh | gh |
| σ / ς | doubleIntervocalicSigma / collapseDoubleSigma |
s | s (σσ → s) | s / ss between vowels |
German doubles a single σ between two vowels (its own s would otherwise
read as /z/ there); English instead collapses an existing σσ pair down to
one s. The two flags are mutually exclusive.
Nasal + stop clusters — verified against the official ELOT 743 / BGN-PCGN
reference table (see Sources): μπ is the only one of the three that
collapses to a bare voiced stop at word start and word end, in every
preset (μπύρα → byra; a word-final μπ essentially never occurs in native
Modern Greek vocabulary, but the official table specs it as one row
alongside word-initial, so the engine honors it too). ντ and γκ stay
literal in official everywhere, initial or medial (ντομάτα → ntomata,
γκολ → gkol) — not symmetric with μπ, despite all three being the same
phonological pattern. The reader presets collapse all three the same way at
word start (not word end — that generalization isn't sourced, only the μπ
row is), reasoned from Modern Greek pronunciation rather than pulled from
the table:
| Greek | word-initial, official | word-initial, readers | medial, official | medial, readers |
|---|---|---|---|---|
| μπ | b | b | mp | mb |
| ντ | nt | d | nt | nd |
| γκ | gk | g | gk | ng |
No word-initial/medial split, same in every preset:
| Greek | official | readers |
|---|---|---|
| τζ | t + zLetter |
d + zLetter |
| τσ | ts | ts |
| γγ | ng | ng |
| γχ | n + chiLetter |
n + chiLetter |
| γξ | n + xLetter |
n + xLetter |
τζ, γχ and γξ route through the same zLetter / chiLetter / xLetter
fields as standalone ζ / χ / ξ, rather than hardcoding a letter — this
matters in practice: a German phrasebook spells τζ as ds, not dz (German
z reads as /ts/, so only a letter that reads as /z/ before a vowel — its
own respelling of ζ — produces the right sound), which is exactly what
germanReader's zLetter: 's' now produces automatically.
Stress
Modern Greek monotonic orthography carries the stress — the engine reads
it, never guesses it. A tonos vowel (ά έ ή ί ό ύ ώ) always produces a
StressSpan at that offset, whether or not the scheme renders the accent
mark itself. In a vowel digraph the tonos sits on the second character, and
the accent lands on the single resulting Latin vowel in a phonetic preset
(καλοί → kalí). A diaeresis breaks a digraph (παϊδάκια, not
*pedákia). πότε (when) and ποτέ (never) differ only by stress —
dropping it teaches the wrong word.
A diaeresis and a tonos can land on the same vowel (ΐ, ΰ) when a
stressed syllable is also the one broken out of a digraph, e.g. ταΐζω (to
feed) or Ταΰγετος (Taygetus). These still carry a StressSpan — official
ELOT 743 renders them as the base letter plus a combining acute (ḯ =
U+00EF + U+0301), which showStress: false strips down to a plain vowel
like every other accent.
Design notes
- Pure Dart, zero dependencies, no I/O — safe to call from an isolate.
- The engine returns stress offsets (
RomanizedText.stressed), never a marked-up string. Nothing has to parse the output looking for accents. - One
RomanizationEngine, parameterized byRomanizationSpec, drives all three presets — no scheme derives its output from another scheme's string, so none of them can lose information the others have.
Sources
- ELOT 743 / ISO 843, cross-checked letter-by-letter against the official UK Government BGN/PCGN reference table for Greek (ROMANIZATION_OF_GREEK.pdf, checked for validity and accuracy 2017), the Greek government's own converter at passport.gov.gr, and the UN romanization working group's Greek report. That cross-check is what caught the μπ word-final case and the ΐ/ΰ stress-and-diaeresis case.
- The reader presets are standard Modern Greek phonology, reasoned directly
rather than pulled from a converter (no official standard covers
"readable for an English/German speaker"). German-specific choices (β →
w, ζ/τζ →s/ds) are additionally corroborated by a native German-language phrasebook, Wikivoyage's Sprachführer Griechisch. DIN 31634, Germany's own Greek transliteration standard, was checked and ruled out as a source for this — it's a scholarly, accent-stripping system built for library cataloging of Ancient/Byzantine Greek (β →b, notvorw), not a Modern Greek pronunciation guide.
Libraries
- greek_romanizer
- Configurable Greek-to-Latin romanization.