Preparing 0.5.2-2 release
[mussorgsky] / msgfmt.py
1 # -*- coding: utf-8 -*-
2 # Written by Martin v. Lwis <loewis@informatik.hu-berlin.de>
3 # Plural forms support added by alexander smishlajev <alex@tycobka.lv>
4 """
5 Generate binary message catalog from textual translation description.
6
7 This program converts a textual Uniforum-style message catalog (.po file) into
8 a binary GNU catalog (.mo file).  This is essentially the same function as the
9 GNU msgfmt program, however, it is a simpler implementation.
10
11 Usage: msgfmt.py [OPTIONS] filename.po
12
13 Options:
14     -o file
15     --output-file=file
16         Specify the output file to write to.  If omitted, output will go to a
17         file named filename.mo (based off the input file name).
18
19     -h
20     --help
21         Print this message and exit.
22
23     -V
24     --version
25         Display version information and exit.
26 """
27
28 import sys
29 import os
30 import getopt
31 import struct
32 import array
33
34 __version__ = "1.1"
35
36 MESSAGES = {}
37
38
39 def usage (ecode, msg=''):
40     """
41     Print usage and msg and exit with given code.
42     """
43     print >> sys.stderr, __doc__
44     if msg:
45         print >> sys.stderr, msg
46     sys.exit(ecode)
47
48
49 def add (msgid, transtr, fuzzy):
50     """
51     Add a non-fuzzy translation to the dictionary.
52     """
53     global MESSAGES
54     if not fuzzy and transtr and not transtr.startswith('\0'):
55         MESSAGES[msgid] = transtr
56
57
58 def generate ():
59     """
60     Return the generated output.
61     """
62     global MESSAGES
63     keys = MESSAGES.keys()
64     # the keys are sorted in the .mo file
65     keys.sort()
66     offsets = []
67     ids = strs = ''
68     for _id in keys:
69         # For each string, we need size and file offset.  Each string is NUL
70         # terminated; the NUL does not count into the size.
71         offsets.append((len(ids), len(_id), len(strs), len(MESSAGES[_id])))
72         ids += _id + '\0'
73         strs += MESSAGES[_id] + '\0'
74     output = ''
75     # The header is 7 32-bit unsigned integers.  We don't use hash tables, so
76     # the keys start right after the index tables.
77     # translated string.
78     keystart = 7*4+16*len(keys)
79     # and the values start after the keys
80     valuestart = keystart + len(ids)
81     koffsets = []
82     voffsets = []
83     # The string table first has the list of keys, then the list of values.
84     # Each entry has first the size of the string, then the file offset.
85     for o1, l1, o2, l2 in offsets:
86         koffsets += [l1, o1+keystart]
87         voffsets += [l2, o2+valuestart]
88     offsets = koffsets + voffsets
89     output = struct.pack("Iiiiiii",
90                          0x950412deL,       # Magic
91                          0,                 # Version
92                          len(keys),         # # of entries
93                          7*4,               # start of key index
94                          7*4+len(keys)*8,   # start of value index
95                          0, 0)              # size and offset of hash table
96     output += array.array("i", offsets).tostring()
97     output += ids
98     output += strs
99     return output
100
101
102 def make (filename, outfile):
103     ID = 1
104     STR = 2
105     global MESSAGES
106     MESSAGES = {}
107
108     # Compute .mo name from .po name and arguments
109     if filename.endswith('.po'):
110         infile = filename
111     else:
112         infile = filename + '.po'
113     if outfile is None:
114         outfile = os.path.splitext(infile)[0] + '.mo'
115
116     try:
117         lines = open(infile).readlines()
118     except IOError, msg:
119         print >> sys.stderr, msg
120         sys.exit(1)
121
122     section = None
123     fuzzy = 0
124
125     # Parse the catalog
126     msgid = msgstr = ''
127     lno = 0
128     for l in lines:
129         lno += 1
130         # If we get a comment line after a msgstr, this is a new entry
131         if l[0] == '#' and section == STR:
132             add(msgid, msgstr, fuzzy)
133             section = None
134             fuzzy = 0
135         # Record a fuzzy mark
136         if l[:2] == '#,' and (l.find('fuzzy') >= 0):
137             fuzzy = 1
138         # Skip comments
139         if l[0] == '#':
140             continue
141         # Start of msgid_plural section, separate from singular form with \0
142         if l.startswith('msgid_plural'):
143             msgid += '\0'
144             l = l[12:]
145         # Now we are in a msgid section, output previous section
146         elif l.startswith('msgid'):
147             if section == STR:
148                 add(msgid, msgstr, fuzzy)
149             section = ID
150             l = l[5:]
151             msgid = msgstr = ''
152         # Now we are in a msgstr section
153         elif l.startswith('msgstr'):
154             section = STR
155             l = l[6:]
156             # Check for plural forms
157             if l.startswith('['):
158                 # Separate plural forms with \0
159                 if not l.startswith('[0]'):
160                     msgstr += '\0'
161                 # Ignore the index - must come in sequence
162                 l = l[l.index(']') + 1:]
163         # Skip empty lines
164         l = l.strip()
165         if not l:
166             continue
167         # XXX: Does this always follow Python escape semantics?
168         l = eval(l)
169         if section == ID:
170             msgid += l
171         elif section == STR:
172             msgstr += l
173         else:
174             print >> sys.stderr, 'Syntax error on %s:%d' % (infile, lno), \
175                   'before:'
176             print >> sys.stderr, l
177             sys.exit(1)
178     # Add last entry
179     if section == STR:
180         add(msgid, msgstr, fuzzy)
181
182     # Compute output
183     output = generate()
184
185     try:
186         open(outfile,"wb").write(output)
187     except IOError,msg:
188         print >> sys.stderr, msg
189
190
191 def main ():
192     try:
193         opts, args = getopt.getopt(sys.argv[1:], 'hVo:',
194                                    ['help', 'version', 'output-file='])
195     except getopt.error, msg:
196         usage(1, msg)
197
198     outfile = None
199     # parse options
200     for opt, arg in opts:
201         if opt in ('-h', '--help'):
202             usage(0)
203         elif opt in ('-V', '--version'):
204             print >> sys.stderr, "msgfmt.py", __version__
205             sys.exit(0)
206         elif opt in ('-o', '--output-file'):
207             outfile = arg
208     # do it
209     if not args:
210         print >> sys.stderr, 'No input file given'
211         print >> sys.stderr, "Try `msgfmt --help' for more information."
212         return
213
214     for filename in args:
215         make(filename, outfile)
216
217
218 if __name__ == '__main__':
219     main()