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 |