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 |