Add a writer function for derivepassphrase's .po template
Marco Ricci

Marco Ricci commited on 2024-12-31 16:03:01
Zeige 1 geänderte Dateien mit 130 Einfügungen und 1 Löschungen.


To be used together with the gettext toolset.

Many things are hard-coded.
... ...
@@ -6,12 +6,13 @@
6 6
 
7 7
 from __future__ import annotations
8 8
 
9
+import datetime
9 10
 import enum
10 11
 import gettext
11 12
 import inspect
12 13
 import textwrap
13 14
 import types
14
-from typing import TYPE_CHECKING, NamedTuple, cast
15
+from typing import TYPE_CHECKING, NamedTuple, TextIO, cast
15 16
 
16 17
 import derivepassphrase as dpp
17 18
 
... ...
@@ -1046,3 +1047,131 @@ class ErrMsgTemplate(enum.Enum):
1046 1047
         comments='',
1047 1048
         context='error message',
1048 1049
     )
1050
+
1051
+
1052
+def write_pot_file(fileobj: TextIO) -> None:
1053
+    r"""Write a .po template to the given file object.
1054
+
1055
+    Assumes the file object is opened for writing and accepts string
1056
+    inputs.  The file will *not* be closed when writing is complete.
1057
+    The file *must* be opened in UTF-8 encoding, lest the file will
1058
+    declare an incorrect encoding.
1059
+
1060
+    This function crucially depends on all translatable strings
1061
+    appearing in the enums of this module.  Certain parts of the
1062
+    .po header are hard-coded, as is the source filename.
1063
+
1064
+    """
1065
+    entries: dict[
1066
+        str,
1067
+        dict[
1068
+            str,
1069
+            Label | InfoMsgTemplate | WarnMsgTemplate | ErrMsgTemplate,
1070
+        ],
1071
+    ] = {}
1072
+    for enum_class in (
1073
+        Label,
1074
+        InfoMsgTemplate,
1075
+        WarnMsgTemplate,
1076
+        ErrMsgTemplate,
1077
+    ):
1078
+        for member in enum_class.__members__.values():
1079
+            ctx = member.value.l10n_context
1080
+            msg = member.value.singular
1081
+            if (
1082
+                msg in entries.setdefault(ctx, {})
1083
+                and entries[ctx][msg] != member
1084
+            ):
1085
+                raise AssertionError(  # noqa: DOC501,TRY003
1086
+                    f'Duplicate entry for ({ctx!r}, {msg!r}): '  # noqa: EM102
1087
+                    f'{entries[ctx][msg]!r} and {member!r}'
1088
+                )
1089
+            entries[ctx][msg] = member
1090
+    now = datetime.datetime.now().astimezone()
1091
+    header = (
1092
+        inspect.cleandoc(rf"""
1093
+        # English translation for {PROG_NAME!s}.
1094
+        # Copyright (C) {now.strftime('%Y')} AUTHOR
1095
+        # This file is distributed under the same license as {PROG_NAME!s}.
1096
+        # AUTHOR <someone@example.com>, {now.strftime('%Y')}.
1097
+        #
1098
+        msgid ""
1099
+        msgstr ""
1100
+        "Project-Id-Version: {PROG_NAME!s} {__version__!s}\n"
1101
+        "Report-Msgid-Bugs-To: software@the13thletter.info\n"
1102
+        "POT-Creation-Date: {now.strftime('%Y-%m-%d %H:%M%z')}\n"
1103
+        "PO-Revision-Date: {now.strftime('%Y-%m-%d %H:%M%z')}\n"
1104
+        "Last-Translator: AUTHOR <someone@example.com>\n"
1105
+        "Language: en\n"
1106
+        "MIME-Version: 1.0\n"
1107
+        "Content-Type: text/plain; charset=UTF-8\n"
1108
+        "Content-Transfer-Encoding: 8bit\n"
1109
+        "Plural-Forms: nplurals=2; plural=(n != 1);\n"
1110
+        """).removesuffix('\n')
1111
+        + '\n'
1112
+    )
1113
+    fileobj.write(header)
1114
+    for _ctx, subdict in sorted(entries.items()):
1115
+        for _msg, enum_value in sorted(
1116
+            subdict.items(),
1117
+            key=lambda kv: str(kv[1]),
1118
+        ):
1119
+            fileobj.writelines(_format_po_entry(enum_value))
1120
+
1121
+
1122
+def _format_po_entry(
1123
+    enum_value: Label | InfoMsgTemplate | WarnMsgTemplate | ErrMsgTemplate,
1124
+) -> tuple[str, ...]:
1125
+    ret: list[str] = ['\n']
1126
+    ts = enum_value.value
1127
+    if ts.translator_comments:
1128
+        ret.extend(
1129
+            f'#. {line}\n'
1130
+            for line in ts.translator_comments.splitlines(False)  # noqa: FBT003
1131
+        )
1132
+    ret.append(f'#: derivepassphrase/_cli_msg.py:{enum_value}\n')
1133
+    if ts.flags:
1134
+        ret.append(f'#, {", ".join(sorted(ts.flags))}\n')
1135
+    if ts.l10n_context:
1136
+        ret.append(f'msgctxt {_cstr(ts.l10n_context)}\n')
1137
+    ret.append(f'msgid {_cstr(ts.singular)}\n')
1138
+    if ts.plural:
1139
+        ret.append(f'msgid_plural {_cstr(ts.plural)}\n')
1140
+    ret.append('msgstr ""\n')
1141
+    return tuple(ret)
1142
+
1143
+
1144
+def _cstr(s: str) -> str:
1145
+    def escape(string: str) -> str:
1146
+        return string.translate({
1147
+            0: r'\000',
1148
+            1: r'\001',
1149
+            2: r'\002',
1150
+            3: r'\003',
1151
+            4: r'\004',
1152
+            5: r'\005',
1153
+            6: r'\006',
1154
+            7: r'\007',
1155
+            8: r'\b',
1156
+            9: r'\t',
1157
+            10: r'\n',
1158
+            11: r'\013',
1159
+            12: r'\f',
1160
+            13: r'\r',
1161
+            14: r'\016',
1162
+            15: r'\017',
1163
+            ord('"'): r'\"',
1164
+            ord('\\'): r'\\',
1165
+            127: r'\177',
1166
+        })
1167
+
1168
+    return '\n'.join(
1169
+        f'"{escape(line)}"'
1170
+        for line in s.splitlines(True)  # noqa: FBT003
1171
+    )
1172
+
1173
+
1174
+if __name__ == '__main__':
1175
+    import sys
1176
+
1177
+    write_pot_file(sys.stdout)
1049 1178