1 /* Reading NeXTstep/GNUstep .strings files.
2 Copyright (C) 2003, 2005-2007, 2009, 2019-2020, 2023 Free Software Foundation, Inc.
3 Written by Bruno Haible <bruno@clisp.org>, 2003.
4
5 This program is free software: you can redistribute it and/or modify
6 it under the terms of the GNU General Public License as published by
7 the Free Software Foundation; either version 3 of the License, or
8 (at your option) any later version.
9
10 This program is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY; without even the implied warranty of
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 GNU General Public License for more details.
14
15 You should have received a copy of the GNU General Public License
16 along with this program. If not, see <https://www.gnu.org/licenses/>. */
17
18 #ifdef HAVE_CONFIG_H
19 # include <config.h>
20 #endif
21
22 /* Specification. */
23 #include "read-stringtable.h"
24
25 #include <assert.h>
26 #include <errno.h>
27 #include <stdbool.h>
28 #include <stdio.h>
29 #include <stdlib.h>
30 #include <string.h>
31
32 #include "attribute.h"
33 #include "error.h"
34 #include "error-progname.h"
35 #include "read-catalog-abstract.h"
36 #include "xalloc.h"
37 #include "xvasprintf.h"
38 #include "po-xerror.h"
39 #include "unistr.h"
40 #include "gettext.h"
41
42 #define _(str) gettext (str)
43
44 /* The format of NeXTstep/GNUstep .strings files is documented in
45 gnustep-base-1.8.0/Tools/make_strings/Using.txt
46 and in the comments of method propertyListFromStringsFileFormat in
47 gnustep-base-1.8.0/Source/NSString.m
48 In summary, it's a Objective-C like file with pseudo-assignments of the form
49 "key" = "value";
50 where the key is the msgid and the value is the msgstr.
51
52 The implementation of the parser of .strings files is in
53 gnustep-base-1.8.0/Source/NSString.m
54 function GSPropertyListFromStringsFormat
55 (indirectly called from NSBundle's method localizedStringForKey).
56
57 A test case is in
58 gnustep-base-1.8.0/Testing/English.lproj/NXStringTable.example
59 */
60
61 /* Handling of comments: We copy all comments from the .strings file to
62 the PO file. This is not really needed; it's a service for translators
63 who don't like PO files and prefer to maintain the .strings file. */
64
65
66 /* Real filename, used in error messages about the input file. */
67 static const char *real_file_name;
68
69 /* File name and line number. */
70 extern lex_pos_ty gram_pos;
71
72 /* The input file stream. */
73 static FILE *fp;
74
75
76 /* Phase 1: Read a byte.
77 Max. 4 pushback characters. */
78
79 static unsigned char phase1_pushback[4];
80 static int phase1_pushback_length;
81
82 static int
83 phase1_getc ()
84 {
85 int c;
86
87 if (phase1_pushback_length)
88 return phase1_pushback[--phase1_pushback_length];
89
90 c = getc (fp);
91
92 if (c == EOF)
93 {
94 if (ferror (fp))
95 {
96 const char *errno_description = strerror (errno);
97 po_xerror (PO_SEVERITY_FATAL_ERROR, NULL, NULL, 0, 0, false,
98 xasprintf ("%s: %s",
99 xasprintf (_("error while reading \"%s\""),
100 real_file_name),
101 errno_description));
102 }
103 return EOF;
104 }
105
106 return c;
107 }
108
109 static void
110 phase1_ungetc (int c)
111 {
112 if (c != EOF)
113 phase1_pushback[phase1_pushback_length++] = c;
114 }
115
116
117 /* Phase 2: Read an UCS-4 character.
118 Max. 2 pushback characters. */
119
120 /* End-of-file indicator for functions returning an UCS-4 character. */
121 #define UEOF -1
122
123 static int phase2_pushback[4];
124 static int phase2_pushback_length;
125
126 /* The input file can be in Unicode encoding (UCS-2BE, UCS-2LE, UTF-8, each
127 with a BOM!), or otherwise the locale-dependent default encoding is used.
128 Since we don't want to depend on the locale here, we use ISO-8859-1
129 instead. */
130 enum enc
131 {
132 enc_undetermined,
133 enc_ucs2be,
134 enc_ucs2le,
135 enc_utf8,
136 enc_iso8859_1
137 };
138 static enum enc encoding;
139
140 static int
141 phase2_getc ()
142 {
143 if (phase2_pushback_length)
144 return phase2_pushback[--phase2_pushback_length];
145
146 if (encoding == enc_undetermined)
147 {
148 /* Determine the input file's encoding. */
149 int c0, c1;
150
151 c0 = phase1_getc ();
152 if (c0 == EOF)
153 return UEOF;
154 c1 = phase1_getc ();
155 if (c1 == EOF)
156 {
157 phase1_ungetc (c0);
158 encoding = enc_iso8859_1;
159 }
160 else if (c0 == 0xfe && c1 == 0xff)
161 encoding = enc_ucs2be;
162 else if (c0 == 0xff && c1 == 0xfe)
163 encoding = enc_ucs2le;
164 else
165 {
166 int c2;
167
168 c2 = phase1_getc ();
169 if (c2 == EOF)
170 {
171 phase1_ungetc (c1);
172 phase1_ungetc (c0);
173 encoding = enc_iso8859_1;
174 }
175 else if (c0 == 0xef && c1 == 0xbb && c2 == 0xbf)
176 encoding = enc_utf8;
177 else
178 {
179 phase1_ungetc (c2);
180 phase1_ungetc (c1);
181 phase1_ungetc (c0);
182 encoding = enc_iso8859_1;
183 }
184 }
185 }
186
187 switch (encoding)
188 {
189 case enc_ucs2be:
190 /* Read an UCS-2BE encoded character. */
191 {
192 int c0, c1;
193
194 c0 = phase1_getc ();
195 if (c0 == EOF)
196 return UEOF;
197 c1 = phase1_getc ();
198 if (c1 == EOF)
199 return UEOF;
200 return (c0 << 8) + c1;
201 }
202
203 case enc_ucs2le:
204 /* Read an UCS-2LE encoded character. */
205 {
206 int c0, c1;
207
208 c0 = phase1_getc ();
209 if (c0 == EOF)
210 return UEOF;
211 c1 = phase1_getc ();
212 if (c1 == EOF)
213 return UEOF;
214 return c0 + (c1 << 8);
215 }
216
217 case enc_utf8:
218 /* Read an UTF-8 encoded character. */
219 {
220 unsigned char buf[6];
221 unsigned int count;
222 int c;
223 ucs4_t uc;
224
225 c = phase1_getc ();
226 if (c == EOF)
227 return UEOF;
228 buf[0] = c;
229 count = 1;
230
231 if (buf[0] >= 0xc0)
232 {
233 c = phase1_getc ();
234 if (c == EOF)
235 return UEOF;
236 buf[1] = c;
237 count = 2;
238
239 if (buf[0] >= 0xe0
240 && ((buf[1] ^ 0x80) < 0x40))
241 {
242 c = phase1_getc ();
243 if (c == EOF)
244 return UEOF;
245 buf[2] = c;
246 count = 3;
247
248 if (buf[0] >= 0xf0
249 && ((buf[2] ^ 0x80) < 0x40))
250 {
251 c = phase1_getc ();
252 if (c == EOF)
253 return UEOF;
254 buf[3] = c;
255 count = 4;
256
257 if (buf[0] >= 0xf8
258 && ((buf[3] ^ 0x80) < 0x40))
259 {
260 c = phase1_getc ();
261 if (c == EOF)
262 return UEOF;
263 buf[4] = c;
264 count = 5;
265
266 if (buf[0] >= 0xfc
267 && ((buf[4] ^ 0x80) < 0x40))
268 {
269 c = phase1_getc ();
270 if (c == EOF)
271 return UEOF;
272 buf[5] = c;
273 count = 6;
274 }
275 }
276 }
277 }
278 }
279
280 u8_mbtouc (&uc, buf, count);
281 return uc;
282 }
283
284 case enc_iso8859_1:
285 /* Read an ISO-8859-1 encoded character. */
286 {
287 int c = phase1_getc ();
288
289 if (c == EOF)
290 return UEOF;
291 return c;
292 }
293
294 default:
295 abort ();
296 }
297 }
298
299 static void
300 phase2_ungetc (int c)
301 {
302 if (c != UEOF)
303 phase2_pushback[phase2_pushback_length++] = c;
304 }
305
306
307 /* Phase 3: Read an UCS-4 character, with line number handling. */
308
309 static int
310 phase3_getc ()
311 {
312 int c = phase2_getc ();
313
314 if (c == '\n')
315 gram_pos.line_number++;
316
317 return c;
318 }
319
320 static void
321 phase3_ungetc (int c)
322 {
323 if (c == '\n')
324 --gram_pos.line_number;
325 phase2_ungetc (c);
326 }
327
328
329 /* Convert from UCS-4 to UTF-8. */
330 static char *
331 conv_from_ucs4 (const int *buffer, size_t buflen)
332 {
333 unsigned char *utf8_string;
334 size_t pos;
335 unsigned char *q;
336
337 /* Each UCS-4 word needs 6 bytes at worst. */
338 utf8_string = XNMALLOC (6 * buflen + 1, unsigned char);
339
340 for (pos = 0, q = utf8_string; pos < buflen; )
341 {
342 unsigned int uc;
343 int n;
344
345 uc = buffer[pos++];
346 n = u8_uctomb (q, uc, 6);
347 assert (n > 0);
348 q += n;
349 }
350 *q = '\0';
351 assert (q - utf8_string <= 6 * buflen);
352
353 return (char *) utf8_string;
354 }
355
356
357 /* Parse a string enclosed in double-quotes. Input is UCS-4 encoded.
358 Return the string in UTF-8 encoding, or NULL if the input doesn't represent
359 a valid string enclosed in double-quotes. */
360 static char *
361 parse_escaped_string (const int *string, size_t length)
362 {
363 static int *buffer;
364 static size_t bufmax;
365 static size_t buflen;
366 const int *string_limit = string + length;
367 int c;
368
369 if (string == string_limit)
370 return NULL;
371 c = *string++;
372 if (c != '"')
373 return NULL;
374 buflen = 0;
375 for (;;)
376 {
377 if (string == string_limit)
378 return NULL;
379 c = *string++;
380 if (c == '"')
381 break;
382 if (c == '\\')
383 {
384 if (string == string_limit)
385 return NULL;
386 c = *string++;
387 if (c >= '0' && c <= '7')
388 {
389 unsigned int n = 0;
390 int j = 0;
391 for (;;)
392 {
393 n = n * 8 + (c - '0');
394 if (++j == 3)
395 break;
396 if (string == string_limit)
397 break;
398 c = *string;
399 if (!(c >= '0' && c <= '7'))
400 break;
401 string++;
402 }
403 c = n;
404 }
405 else if (c == 'u' || c == 'U')
406 {
407 unsigned int n = 0;
408 int j;
409 for (j = 0; j < 4; j++)
410 {
411 if (string == string_limit)
412 break;
413 c = *string;
414 if (c >= '0' && c <= '9')
415 n = n * 16 + (c - '0');
416 else if (c >= 'A' && c <= 'F')
417 n = n * 16 + (c - 'A' + 10);
418 else if (c >= 'a' && c <= 'f')
419 n = n * 16 + (c - 'a' + 10);
420 else
421 break;
422 string++;
423 }
424 c = n;
425 }
426 else
427 switch (c)
428 {
429 case 'a': c = '\a'; break;
430 case 'b': c = '\b'; break;
431 case 't': c = '\t'; break;
432 case 'r': c = '\r'; break;
433 case 'n': c = '\n'; break;
434 case 'v': c = '\v'; break;
435 case 'f': c = '\f'; break;
436 }
437 }
438 if (buflen >= bufmax)
439 {
440 bufmax = 2 * bufmax + 10;
441 buffer = xrealloc (buffer, bufmax * sizeof (int));
442 }
443 buffer[buflen++] = c;
444 }
445
446 return conv_from_ucs4 (buffer, buflen);
447 }
448
449
450 /* Accumulating flag comments. */
451
452 static char *special_comment;
453
454 static inline void
455 special_comment_reset ()
456 {
457 if (special_comment != NULL)
458 free (special_comment);
459 special_comment = NULL;
460 }
461
462 static void
463 special_comment_add (const char *flag)
464 {
465 if (special_comment == NULL)
466 special_comment = xstrdup (flag);
467 else
468 {
469 size_t total_len = strlen (special_comment) + 2 + strlen (flag) + 1;
470 special_comment = xrealloc (special_comment, total_len);
471 strcat (special_comment, ", ");
472 strcat (special_comment, flag);
473 }
474 }
475
476 static inline void
477 special_comment_finish ()
478 {
479 if (special_comment != NULL)
480 {
481 po_callback_comment_special (special_comment);
482 free (special_comment);
483 special_comment = NULL;
484 }
485 }
486
487
488 /* Accumulating comments. */
489
490 static int *buffer;
491 static size_t bufmax;
492 static size_t buflen;
493 static bool next_is_obsolete;
494 static bool next_is_fuzzy;
495 static char *fuzzy_msgstr;
496 static bool expect_fuzzy_msgstr_as_c_comment;
497 static bool expect_fuzzy_msgstr_as_cxx_comment;
498
499 static inline void
500 comment_start ()
501 {
502 buflen = 0;
503 }
504
505 static inline void
506 comment_add (int c)
507 {
508 if (buflen >= bufmax)
509 {
510 bufmax = 2 * bufmax + 10;
511 buffer = xrealloc (buffer, bufmax * sizeof (int));
512 }
513 buffer[buflen++] = c;
514 }
515
516 static void
517 comment_line_end (size_t chars_to_remove, bool test_for_fuzzy_msgstr)
518 {
519 char *line;
520
521 buflen -= chars_to_remove;
522 /* Drop trailing white space, but not EOLs. */
523 while (buflen >= 1
524 && (buffer[buflen - 1] == ' ' || buffer[buflen - 1] == '\t'))
525 --buflen;
526
527 /* At special positions we interpret a comment of the form
528 = "escaped string"
529 with an optional trailing semicolon as being the fuzzy msgstr, not a
530 regular comment. */
531 if (test_for_fuzzy_msgstr
532 && buflen > 2 && buffer[0] == '=' && buffer[1] == ' '
533 && (fuzzy_msgstr =
534 parse_escaped_string (buffer + 2,
535 buflen - (buffer[buflen - 1] == ';') - 2)))
536 return;
537
538 line = conv_from_ucs4 (buffer, buflen);
539
540 if (strcmp (line, "Flag: untranslated") == 0)
541 {
542 special_comment_add ("fuzzy");
543 next_is_fuzzy = true;
544 }
545 else if (strcmp (line, "Flag: unmatched") == 0)
546 next_is_obsolete = true;
547 else if (strlen (line) >= 6 && memcmp (line, "Flag: ", 6) == 0)
548 special_comment_add (line + 6);
549 else if (strlen (line) >= 9 && memcmp (line, "Comment: ", 9) == 0)
550 /* A comment extracted from the source. */
551 po_callback_comment_dot (line + 9);
552 else
553 {
554 char *last_colon;
555 unsigned long number;
556 char *endp;
557
558 if (strlen (line) >= 6 && memcmp (line, "File: ", 6) == 0
559 && (last_colon = strrchr (line + 6, ':')) != NULL
560 && *(last_colon + 1) != '\0'
561 && (number = strtoul (last_colon + 1, &endp, 10), *endp == '\0'))
562 {
563 /* A "File: <filename>:<number>" type comment. */
564 *last_colon = '\0';
565 po_callback_comment_filepos (line + 6, number);
566 }
567 else
568 po_callback_comment (line);
569 }
570 }
571
572
573 /* Phase 4: Replace each comment that is not inside a string with a space
574 character. */
575
576 static int
577 phase4_getc ()
578 {
579 int c;
580
581 c = phase3_getc ();
582 if (c != '/')
583 return c;
584 c = phase3_getc ();
585 switch (c)
586 {
587 default:
588 phase3_ungetc (c);
589 return '/';
590
591 case '*':
592 /* C style comment. */
593 {
594 bool last_was_star;
595 size_t trailing_stars;
596 bool seen_newline;
597
598 comment_start ();
599 last_was_star = false;
600 trailing_stars = 0;
601 seen_newline = false;
602 /* Drop additional stars at the beginning of the comment. */
603 for (;;)
604 {
605 c = phase3_getc ();
606 if (c != '*')
607 break;
608 last_was_star = true;
609 }
610 phase3_ungetc (c);
611 for (;;)
612 {
613 c = phase3_getc ();
614 if (c == UEOF)
615 break;
616 /* We skip all leading white space, but not EOLs. */
617 if (!(buflen == 0 && (c == ' ' || c == '\t')))
618 comment_add (c);
619 switch (c)
620 {
621 case '\n':
622 seen_newline = true;
623 comment_line_end (1, false);
624 comment_start ();
625 last_was_star = false;
626 trailing_stars = 0;
627 continue;
628
629 case '*':
630 last_was_star = true;
631 trailing_stars++;
632 continue;
633
634 case '/':
635 if (last_was_star)
636 {
637 /* Drop additional stars at the end of the comment. */
638 comment_line_end (trailing_stars + 1,
639 expect_fuzzy_msgstr_as_c_comment
640 && !seen_newline);
641 break;
642 }
643 FALLTHROUGH;
644
645 default:
646 last_was_star = false;
647 trailing_stars = 0;
648 continue;
649 }
650 break;
651 }
652 return ' ';
653 }
654
655 case '/':
656 /* C++ style comment. */
657 comment_start ();
658 for (;;)
659 {
660 c = phase3_getc ();
661 if (c == '\n' || c == UEOF)
662 break;
663 /* We skip all leading white space, but not EOLs. */
664 if (!(buflen == 0 && (c == ' ' || c == '\t')))
665 comment_add (c);
666 }
667 comment_line_end (0, expect_fuzzy_msgstr_as_cxx_comment);
668 return '\n';
669 }
670 }
671
672 static inline void
673 phase4_ungetc (int c)
674 {
675 phase3_ungetc (c);
676 }
677
678
679 /* Return true if a character is considered as whitespace. */
680 static bool
681 is_whitespace (int c)
682 {
683 return (c == ' ' || c == '\t' || c == '\r' || c == '\n' || c == '\f'
684 || c == '\b');
685 }
686
687 /* Return true if a character needs quoting, i.e. cannot be used in unquoted
688 tokens. */
689 static bool
690 is_quotable (int c)
691 {
692 if ((c >= '0' && c <= '9')
693 || (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z'))
694 return false;
695 switch (c)
696 {
697 case '!': case '#': case '$': case '%': case '&': case '*':
698 case '+': case '-': case '.': case '/': case ':': case '?':
699 case '@': case '|': case '~': case '_': case '^':
700 return false;
701 default:
702 return true;
703 }
704 }
705
706
707 /* Read a key or value string.
708 Return the string in UTF-8 encoding, or NULL if no string is seen.
709 Return the start position of the string in *pos. */
710 static char *
711 read_string (lex_pos_ty *pos)
712 {
713 static int *buffer;
714 static size_t bufmax;
715 static size_t buflen;
716 int c;
717
718 /* Skip whitespace before the string. */
719 do
720 c = phase4_getc ();
721 while (is_whitespace (c));
722
723 if (c == UEOF)
724 /* No more string. */
725 return NULL;
726
727 *pos = gram_pos;
728 buflen = 0;
729 if (c == '"')
730 {
731 /* Read a string enclosed in double-quotes. */
732 for (;;)
733 {
734 c = phase3_getc ();
735 if (c == UEOF || c == '"')
736 break;
737 if (c == '\\')
738 {
739 c = phase3_getc ();
740 if (c == UEOF)
741 break;
742 if (c >= '0' && c <= '7')
743 {
744 unsigned int n = 0;
745 int j = 0;
746 for (;;)
747 {
748 n = n * 8 + (c - '0');
749 if (++j == 3)
750 break;
751 c = phase3_getc ();
752 if (!(c >= '0' && c <= '7'))
753 {
754 phase3_ungetc (c);
755 break;
756 }
757 }
758 c = n;
759 }
760 else if (c == 'u' || c == 'U')
761 {
762 unsigned int n = 0;
763 int j;
764 for (j = 0; j < 4; j++)
765 {
766 c = phase3_getc ();
767 if (c >= '0' && c <= '9')
768 n = n * 16 + (c - '0');
769 else if (c >= 'A' && c <= 'F')
770 n = n * 16 + (c - 'A' + 10);
771 else if (c >= 'a' && c <= 'f')
772 n = n * 16 + (c - 'a' + 10);
773 else
774 {
775 phase3_ungetc (c);
776 break;
777 }
778 }
779 c = n;
780 }
781 else
782 switch (c)
783 {
784 case 'a': c = '\a'; break;
785 case 'b': c = '\b'; break;
786 case 't': c = '\t'; break;
787 case 'r': c = '\r'; break;
788 case 'n': c = '\n'; break;
789 case 'v': c = '\v'; break;
790 case 'f': c = '\f'; break;
791 }
792 }
793 if (buflen >= bufmax)
794 {
795 bufmax = 2 * bufmax + 10;
796 buffer = xrealloc (buffer, bufmax * sizeof (int));
797 }
798 buffer[buflen++] = c;
799 }
800 if (c == UEOF)
801 po_xerror (PO_SEVERITY_ERROR, NULL,
802 real_file_name, gram_pos.line_number, (size_t)(-1), false,
803 _("warning: unterminated string"));
804 }
805 else
806 {
807 /* Read a token outside quotes. */
808 if (is_quotable (c))
809 po_xerror (PO_SEVERITY_ERROR, NULL,
810 real_file_name, gram_pos.line_number, (size_t)(-1), false,
811 _("warning: syntax error"));
812 for (; c != UEOF && !is_quotable (c); c = phase4_getc ())
813 {
814 if (buflen >= bufmax)
815 {
816 bufmax = 2 * bufmax + 10;
817 buffer = xrealloc (buffer, bufmax * sizeof (int));
818 }
819 buffer[buflen++] = c;
820 }
821 }
822
823 return conv_from_ucs4 (buffer, buflen);
824 }
825
826
827 /* Read a .strings file from a stream, and dispatch to the various
828 abstract_catalog_reader_class_ty methods. */
829 static void
830 stringtable_parse (abstract_catalog_reader_ty *pop, FILE *file,
831 const char *real_filename, const char *logical_filename)
832 {
833 fp = file;
834 real_file_name = real_filename;
835 gram_pos.file_name = xstrdup (real_file_name);
836 gram_pos.line_number = 1;
837 encoding = enc_undetermined;
838 expect_fuzzy_msgstr_as_c_comment = false;
839 expect_fuzzy_msgstr_as_cxx_comment = false;
840
841 for (;;)
842 {
843 char *msgid;
844 lex_pos_ty msgid_pos;
845 char *msgstr;
846 lex_pos_ty msgstr_pos;
847 int c;
848
849 /* Prepare for next msgid/msgstr pair. */
850 special_comment_reset ();
851 next_is_obsolete = false;
852 next_is_fuzzy = false;
853 fuzzy_msgstr = NULL;
854
855 /* Read the key and all the comments preceding it. */
856 msgid = read_string (&msgid_pos);
857 if (msgid == NULL)
858 break;
859
860 special_comment_finish ();
861
862 /* Skip whitespace. */
863 do
864 c = phase4_getc ();
865 while (is_whitespace (c));
866
867 /* Expect a '=' or ';'. */
868 if (c == UEOF)
869 {
870 po_xerror (PO_SEVERITY_ERROR, NULL,
871 real_file_name, gram_pos.line_number, (size_t)(-1), false,
872 _("warning: unterminated key/value pair"));
873 break;
874 }
875 if (c == ';')
876 {
877 /* "key"; is an abbreviation for "key"=""; and does not
878 necessarily designate an untranslated entry. */
879 msgstr = xstrdup ("");
880 msgstr_pos = msgid_pos;
881 po_callback_message (NULL, msgid, &msgid_pos, NULL,
882 msgstr, strlen (msgstr) + 1, &msgstr_pos,
883 NULL, NULL, NULL,
884 false, next_is_obsolete);
885 }
886 else if (c == '=')
887 {
888 /* Read the value. */
889 msgstr = read_string (&msgstr_pos);
890 if (msgstr == NULL)
891 {
892 po_xerror (PO_SEVERITY_ERROR, NULL,
893 real_file_name, gram_pos.line_number, (size_t)(-1),
894 false, _("warning: unterminated key/value pair"));
895 break;
896 }
897
898 /* Skip whitespace. But for fuzzy key/value pairs, look for the
899 tentative msgstr in the form of a C style comment. */
900 expect_fuzzy_msgstr_as_c_comment = next_is_fuzzy;
901 do
902 {
903 c = phase4_getc ();
904 if (fuzzy_msgstr != NULL)
905 expect_fuzzy_msgstr_as_c_comment = false;
906 }
907 while (is_whitespace (c));
908 expect_fuzzy_msgstr_as_c_comment = false;
909
910 /* Expect a ';'. */
911 if (c == ';')
912 {
913 /* But for fuzzy key/value pairs, look for the tentative msgstr
914 in the form of a C++ style comment. */
915 if (fuzzy_msgstr == NULL && next_is_fuzzy)
916 {
917 do
918 c = phase3_getc ();
919 while (c == ' ');
920 phase3_ungetc (c);
921
922 expect_fuzzy_msgstr_as_cxx_comment = true;
923 c = phase4_getc ();
924 phase4_ungetc (c);
925 expect_fuzzy_msgstr_as_cxx_comment = false;
926 }
927 if (fuzzy_msgstr != NULL && strcmp (msgstr, msgid) == 0)
928 msgstr = fuzzy_msgstr;
929
930 /* A key/value pair. */
931 po_callback_message (NULL, msgid, &msgid_pos, NULL,
932 msgstr, strlen (msgstr) + 1, &msgstr_pos,
933 NULL, NULL, NULL,
934 false, next_is_obsolete);
935 }
936 else
937 {
938 po_xerror (PO_SEVERITY_ERROR, NULL,
939 real_file_name, gram_pos.line_number, (size_t)(-1),
940 false,
941 _("warning: syntax error, expected ';' after string"));
942 break;
943 }
944 }
945 else
946 {
947 po_xerror (PO_SEVERITY_ERROR, NULL,
948 real_file_name, gram_pos.line_number, (size_t)(-1), false,
949 _("warning: syntax error, expected '=' or ';' after string"));
950 break;
951 }
952 }
953
954 fp = NULL;
955 real_file_name = NULL;
956 gram_pos.line_number = 0;
957 }
958
959 const struct catalog_input_format input_format_stringtable =
960 {
961 stringtable_parse, /* parse */
962 true /* produces_utf8 */
963 };