CSV is what translators hand back. PO is what gettext-based runtimes consume — Django, Flask-Babel, PHP gettext, Symfony's Translation component, and a long tail of older codebases all read .po files at startup. The most reliable way to bridge the two is a converter that respects the conventions of both: first column equals key equals msgid, second column equals translation equals msgstr, every other column ignored unless explicitly mapped, and the result must be a parseable .po file with the canonical empty-msgid header that every gettext tool expects to see at the top.
i18n-convert performs exactly that mapping. It reads the CSV, treats the first column as the key (which becomes msgid) and the second column as the source value (which becomes msgstr), and writes a syntactically valid PO file with an empty header pseudo-entry at the top. Rows are emitted in input order to keep diffs stable across translation rounds, and each entry is separated by a blank line in keeping with PO convention. Special characters in values are escaped per gettext's grammar — embedded double quotes get backslash-escaped, embedded newlines become \n. If your gettext runtime needs the msgid to match a source-language string rather than a key (Django's common pattern), see the CSV-to-JSON page for a layout that keeps the keys instead.
Command
i18n-convert no_comments.csv --to po -o messages.po
Input
key,en
greeting,Hello
farewell,Goodbye
app.title,My App
Output
msgid ""
msgstr ""
msgid "greeting"
msgstr "Hello"
msgid "farewell"
msgstr "Goodbye"
msgid "app.title"
msgstr "My App"