7dc8f3e71c800291128f178b113058bb2a409756
[blender.git] / source / blender / blentranslation / msgfmt / msgfmt.c
1 /*
2  * ***** BEGIN GPL LICENSE BLOCK *****
3  *
4  * This program is free software; you can redistribute it and/or
5  * modify it under the terms of the GNU General Public License
6  * as published by the Free Software Foundation; either version 2
7  * of the License, or (at your option) any later version.
8  *
9  * This program is distributed in the hope that it will be useful,
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12  * GNU General Public License for more details.
13  *
14  * You should have received a copy of the GNU General Public License
15  * along with this program; if not, write to the Free Software Foundation,
16  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17  *
18  * The Original Code is Copyright (C) 2017 by Blender Foundation.
19  * All rights reserved.
20  *
21  * Contributor(s): Bastien Montagne
22  *
23  * ***** END GPL LICENSE BLOCK *****
24  */
25
26 /*
27  * Based on C++ version by Sergey Sharybin <sergey.vfx@gmail.com>.
28  * Based on Python script msgfmt.py from Python source code tree, which was written by
29  * Martin v. Löwis <loewis@informatik.hu-berlin.de>
30  *
31  * Generate binary message catalog from textual translation description.
32  *
33  * This program converts a textual Uniforum-style message catalog (.po file) into a binary GNU catalog (.mo file).
34  * This is essentially the same function as the GNU msgfmt program, however, it is a simpler implementation.
35  *
36  * Usage: msgfmt input.po output.po
37  */
38
39 #include <string.h>
40 #include <stdlib.h>
41
42 #include "BLI_utildefines.h"
43 #include "BLI_dynstr.h"
44 #include "BLI_fileops.h"
45 #include "BLI_ghash.h"
46 #include "BLI_linklist.h"
47 #include "BLI_memarena.h"
48
49 #include "MEM_guardedalloc.h"
50
51
52 /* Stupid stub necessary because some BLI files includes winstuff.h, which uses G a bit... */
53 #ifdef WIN32
54   typedef struct Global {
55         void *dummy;
56   } Global;
57
58   Global G;
59 #endif
60
61
62 /* We cannot use NULL char until ultimate step, would give nightmare to our C string processing...
63  * Using one of the UTF-8 invalid bytes (as per our BLI string_utf8.c) */
64 #define NULLSEP_STR "\xff"
65 #define NULLSEP_CHR '\xff'
66
67 typedef enum {
68         SECTION_NONE = 0,
69         SECTION_CTX  = 1,
70         SECTION_ID   = 2,
71         SECTION_STR  = 3,
72 } eSectionType;
73
74 typedef struct Message {
75         DynStr *ctxt;
76         DynStr *id;
77         DynStr *str;
78
79         bool is_fuzzy;
80 } Message;
81
82 static char *trim(char *str)
83 {
84         const size_t len = strlen(str);
85         size_t i;
86
87         if (len == 0) {
88                 return str;
89         }
90
91         for (i = 0; i < len && ELEM(str[0], ' ', '\t', '\n'); str++, i++);
92
93         char *end = &str[len - 1 - i];
94         for (i = len; i > 0 && ELEM(end[0], ' ', '\t', '\n'); end--, i--);
95         end[1] = '\0';
96
97         return str;
98 }
99
100 static char *unescape(char *str)
101 {
102         char *curr, *next;
103         for (curr = next = str; next[0] != '\0'; curr++, next++) {
104                 if (next[0] == '\\') {
105                         switch (next[1]) {
106                                 case '\0':
107                                         /* Get rid of trailing escape char... */
108                                         curr--;
109                                         break;
110                                 case '\\':
111                                         *curr = '\\';
112                                         next++;
113                                         break;
114                                 case 'n':
115                                         *curr = '\n';
116                                         next++;
117                                         break;
118                                 case 't':
119                                         *curr = '\t';
120                                         next++;
121                                         break;
122                                 default:
123                                         /* Get rid of useless escape char. */
124                                         next++;
125                                         *curr = *next;
126                         }
127                 }
128                 else if (curr != next) {
129                         *curr = *next;
130                 }
131         }
132         *curr = '\0';
133
134         if (str[0] == '"' && *(curr - 1) == '"') {
135                 *(curr - 1) = '\0';
136                 return str + 1;
137         }
138         return str;
139 }
140
141 static int qsort_str_cmp(const void *a, const void *b)
142 {
143         return strcmp(*(const char **)a, *(const char **)b);
144 }
145
146 static char **get_keys_sorted(GHash *messages, const uint32_t num_keys)
147 {
148         GHashIterator iter;
149
150         char **keys = MEM_mallocN(sizeof(*keys) * num_keys, __func__);
151         char **k = keys;
152
153         GHASH_ITER(iter, messages) {
154                 *k = BLI_ghashIterator_getKey(&iter);
155                 k++;
156         }
157
158         qsort(keys, num_keys, sizeof(*keys), qsort_str_cmp);
159
160         return keys;
161 }
162
163 BLI_INLINE size_t uint32_to_bytes(const int value, char *bytes) {
164         size_t i;
165         for (i = 0; i < sizeof(value); i++) {
166                 bytes[i] = (char) ((value >> ((int)i * 8)) & 0xff);
167         }
168         return i;
169 }
170
171 BLI_INLINE size_t msg_to_bytes(char *msg, char *bytes, uint32_t size) {
172         /* Note that we also perform replacing of our NULLSEP placeholder by real NULL char... */
173         size_t i;
174         for (i = 0; i < size; i++, msg++, bytes++) {
175                 *bytes = (*msg == NULLSEP_CHR) ? '\0' : *msg;
176         }
177         return i;
178 }
179
180 typedef struct Offset {
181         uint32_t key_offset, key_len, val_offset, val_len;
182 } Offset;
183
184 /* Return the generated binary output. */
185 static char *generate(GHash *messages, size_t *r_output_size) {
186         const uint32_t num_keys = BLI_ghash_size(messages);
187
188         /* Get list of sorted keys. */
189         char **keys = get_keys_sorted(messages, num_keys);
190         char **vals = MEM_mallocN(sizeof(*vals) * num_keys, __func__);
191         uint32_t tot_keys_len = 0;
192         uint32_t tot_vals_len = 0;
193
194         Offset *offsets = MEM_mallocN(sizeof(*offsets) * num_keys, __func__);
195
196         for (int i = 0; i < num_keys; i++) {
197                 Offset *off = &offsets[i];
198
199                 vals[i] = BLI_ghash_lookup(messages, keys[i]);
200
201                 /* For each string, we need size and file offset.
202                  * Each string is NULL terminated; the NULL does not count into the size. */
203                 off->key_offset = tot_keys_len;
204                 off->key_len = (uint32_t)strlen(keys[i]);
205                 tot_keys_len += off->key_len + 1;
206
207                 off->val_offset = tot_vals_len;
208                 off->val_len = (uint32_t)strlen(vals[i]);
209                 tot_vals_len += off->val_len + 1;
210         }
211
212         /* The header is 7 32-bit unsigned integers. then comes the keys index table, then the values index table. */
213         const uint32_t idx_keystart = 7 * 4;
214         const uint32_t idx_valstart = idx_keystart + 8 * num_keys;
215         /* We don't use hash tables, so the keys start right after the index tables. */
216         const uint32_t keystart = idx_valstart + 8 * num_keys;
217         /* and the values start after the keys */
218         const uint32_t valstart = keystart + tot_keys_len;
219
220         /* Final buffer representing the binary MO file. */
221         *r_output_size = valstart + tot_vals_len;
222         char *output = MEM_mallocN(*r_output_size, __func__);
223         char *h = output;
224         char *ik = output + idx_keystart;
225         char *iv = output + idx_valstart;
226         char *k = output + keystart;
227         char *v = output + valstart;
228
229         h += uint32_to_bytes(0x950412de, h);  /* Magic */
230         h += uint32_to_bytes(0x0, h);  /* Version */
231         h += uint32_to_bytes(num_keys, h);  /* Number of entries */
232         h += uint32_to_bytes(idx_keystart, h);  /* Start of key index */
233         h += uint32_to_bytes(idx_valstart, h);  /* Start of value index */
234         h += uint32_to_bytes(0, h);  /* Size of hash table */
235         h += uint32_to_bytes(0, h);  /* Offset of hash table */
236
237         BLI_assert(h == ik);
238
239         for (int i = 0; i < num_keys; i++) {
240                 Offset *off = &offsets[i];
241
242                 /* The index table first has the list of keys, then the list of values.
243                  * Each entry has first the size of the string, then the file offset. */
244                 ik += uint32_to_bytes(off->key_len, ik);
245                 ik += uint32_to_bytes(off->key_offset + keystart, ik);
246                 iv += uint32_to_bytes(off->val_len, iv);
247                 iv += uint32_to_bytes(off->val_offset + valstart, iv);
248
249                 k += msg_to_bytes(keys[i], k, off->key_len + 1);
250                 v += msg_to_bytes(vals[i], v, off->val_len + 1);
251         }
252
253         BLI_assert(ik == output + idx_valstart);
254         BLI_assert(iv == output + keystart);
255         BLI_assert(k == output + valstart);
256
257         MEM_freeN(keys);
258         MEM_freeN(vals);
259         MEM_freeN(offsets);
260
261         return output;
262 }
263
264 /* Add a non-fuzzy translation to the dictionary. */
265 static void add(GHash *messages, MemArena *memarena, const Message *msg)
266 {
267         const size_t msgctxt_len = (size_t)BLI_dynstr_get_len(msg->ctxt);
268         const size_t msgid_len = (size_t)BLI_dynstr_get_len(msg->id);
269         const size_t msgstr_len = (size_t)BLI_dynstr_get_len(msg->str);
270         const size_t msgkey_len = msgid_len  + ((msgctxt_len == 0) ? 0 : msgctxt_len + 1);
271
272         if (!msg->is_fuzzy && msgstr_len != 0) {
273                 char *msgkey = BLI_memarena_alloc(memarena, sizeof(*msgkey) * (msgkey_len + 1));
274                 char *msgstr = BLI_memarena_alloc(memarena, sizeof(*msgstr) * (msgstr_len + 1));
275
276                 if (msgctxt_len != 0) {
277                         BLI_dynstr_get_cstring_ex(msg->ctxt, msgkey);
278                         msgkey[msgctxt_len] = '\x04';  /* Context/msgid separator */
279                         BLI_dynstr_get_cstring_ex(msg->id, &msgkey[msgctxt_len + 1]);
280                 }
281                 else {
282                         BLI_dynstr_get_cstring_ex(msg->id, msgkey);
283                 }
284
285                 BLI_dynstr_get_cstring_ex(msg->str, msgstr);
286
287                 BLI_ghash_insert(messages, msgkey, msgstr);
288         }
289 }
290
291
292 static void clear(Message *msg)
293 {
294         BLI_dynstr_clear(msg->ctxt);
295         BLI_dynstr_clear(msg->id);
296         BLI_dynstr_clear(msg->str);
297         msg->is_fuzzy = false;
298 }
299
300 static int make(const char *input_file_name, const char *output_file_name)
301 {
302         GHash *messages = BLI_ghash_new(BLI_ghashutil_strhash_p_murmur, BLI_ghashutil_strcmp, __func__);
303         MemArena *msgs_memarena = BLI_memarena_new(BLI_MEMARENA_STD_BUFSIZE, __func__);
304
305         const char *msgctxt_kw = "msgctxt";
306         const char *msgid_kw = "msgid";
307         const char *msgid_plural_kw = "msgid_plural";
308         const char *msgstr_kw = "msgstr";
309         const size_t msgctxt_len = strlen(msgctxt_kw);
310         const size_t msgid_len = strlen(msgid_kw);
311         const size_t msgid_plural_len = strlen(msgid_plural_kw);
312         const size_t msgstr_len = strlen(msgstr_kw);
313
314         /* Note: For now, we assume file encoding is always utf-8. */
315
316         eSectionType section = SECTION_NONE;
317         bool is_plural = false;
318
319         Message msg = {
320             .ctxt = BLI_dynstr_new_memarena(),
321             .id = BLI_dynstr_new_memarena(),
322             .str = BLI_dynstr_new_memarena(),
323             .is_fuzzy = false,
324         };
325
326         LinkNode *input_file_lines = BLI_file_read_as_lines(input_file_name);
327         LinkNode *ifl = input_file_lines;
328
329         /* Parse the catalog. */
330         for (int lno = 1; ifl; ifl = ifl->next, lno++) {
331                 char *l = ifl->link;
332                 const bool is_comment = (l[0] == '#');
333                 /* If we get a comment line after a msgstr, this is a new entry. */
334                 if (is_comment) {
335                         if (section == SECTION_STR) {
336                                 add(messages, msgs_memarena, &msg);
337                                 clear(&msg);
338                                 section = SECTION_NONE;
339                         }
340                         /* Record a fuzzy mark. */
341                         if (l[1] == ',' && strstr(l, "fuzzy") != NULL) {
342                                 msg.is_fuzzy = true;
343                         }
344                         /* Skip comments */
345                         continue;
346                 }
347                 if (strstr(l, msgctxt_kw) == l) {
348                         if (section == SECTION_STR) {
349                                 /* New message, output previous section. */
350                                 add(messages, msgs_memarena, &msg);
351                         }
352                         if (!ELEM(section, SECTION_NONE, SECTION_STR)) {
353                                 printf("msgctxt not at start of new message on %s:%d\n", input_file_name, lno);
354                                 return EXIT_FAILURE;
355                         }
356                         section = SECTION_CTX;
357                         l = l + msgctxt_len;
358                         clear(&msg);
359                 }
360                 else if (strstr(l, msgid_plural_kw) == l) {
361                         /* This is a message with plural forms. */
362                         if (section != SECTION_ID) {
363                                 printf("msgid_plural not preceeded by msgid on %s:%d\n", input_file_name, lno);
364                                 return EXIT_FAILURE;
365                         }
366                         l = l + msgid_plural_len;
367                         BLI_dynstr_append(msg.id, NULLSEP_STR);  /* separator of singular and plural */
368                         is_plural = true;
369                 }
370                 else if (strstr(l, msgid_kw) == l) {
371                         if (section == SECTION_STR) {
372                                 add(messages, msgs_memarena, &msg);
373                         }
374                         if (section != SECTION_CTX) {
375                                 clear(&msg);
376                         }
377                         section = SECTION_ID;
378                         l = l + msgid_len;
379                         is_plural = false;
380                 }
381                 else if (strstr(l, msgstr_kw) == l) {
382                         l = l + msgstr_len;
383                         // Now we are in a msgstr section
384                         section = SECTION_STR;
385                         if (l[0] == '[') {
386                                 if (!is_plural) {
387                                         printf("plural without msgid_plural on %s:%d\n", input_file_name, lno);
388                                         return EXIT_FAILURE;
389                                 }
390                                 if ((l = strchr(l, ']')) == NULL) {
391                                         printf("Syntax error on %s:%d\n", input_file_name, lno);
392                                         return EXIT_FAILURE;
393                                 }
394                                 if (BLI_dynstr_get_len(msg.str) != 0) {
395                                         BLI_dynstr_append(msg.str, NULLSEP_STR);  /* Separator of the various plural forms. */
396                                 }
397                         }
398                         else {
399                                 if (is_plural) {
400                                         printf("indexed msgstr required for plural on %s:%d\n", input_file_name, lno);
401                                         return EXIT_FAILURE;
402                                 }
403                         }
404                 }
405                 /* Skip empty lines. */
406                 l = trim(l);
407                 if (l[0] == '\0') {
408                         if (section == SECTION_STR) {
409                                 add(messages, msgs_memarena, &msg);
410                                 clear(&msg);
411                         }
412                         section = SECTION_NONE;
413                         continue;
414                 }
415                 l = unescape(l);
416                 if (section == SECTION_CTX) {
417                         BLI_dynstr_append(msg.ctxt, l);
418                 }
419                 else if (section == SECTION_ID) {
420                         BLI_dynstr_append(msg.id, l);
421                 }
422                 else if (section == SECTION_STR) {
423                         BLI_dynstr_append(msg.str, l);
424                 }
425                 else {
426                         printf("Syntax error on %s:%d\n", input_file_name, lno);
427                         return EXIT_FAILURE;
428                 }
429         }
430         /* Add last entry */
431         if (section == SECTION_STR) {
432                 add(messages, msgs_memarena, &msg);
433         }
434
435         BLI_dynstr_free(msg.ctxt);
436         BLI_dynstr_free(msg.id);
437         BLI_dynstr_free(msg.str);
438         BLI_file_free_lines(input_file_lines);
439
440         /* Compute output */
441         size_t output_size;
442         char *output = generate(messages, &output_size);
443
444         FILE *fp = BLI_fopen(output_file_name, "wb");
445         fwrite(output, 1, output_size, fp);
446         fclose(fp);
447
448         MEM_freeN(output);
449         BLI_ghash_free(messages, NULL, NULL);
450         BLI_memarena_free(msgs_memarena);
451
452         return EXIT_SUCCESS;
453 }
454
455 int main(int argc, char **argv)
456 {
457         if (argc != 3) {
458                 printf("Usage: %s <input.po> <output.mo>\n", argv[0]);
459                 return EXIT_FAILURE;
460         }
461         const char *input_file = argv[1];
462         const char *output_file = argv[2];
463
464         return make(input_file, output_file);
465 }