1 /* Writing NeXTstep/GNUstep .strings files.
2 Copyright (C) 2003, 2006-2008, 2019 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 "write-stringtable.h"
24
25 #include <stdbool.h>
26 #include <stdlib.h>
27 #include <string.h>
28
29 #include <textstyle.h>
30
31 #include "message.h"
32 #include "msgl-ascii.h"
33 #include "msgl-iconv.h"
34 #include "po-charset.h"
35 #include "c-strstr.h"
36 #include "xvasprintf.h"
37 #include "write-po.h"
38
39 /* The format of NeXTstep/GNUstep .strings files is documented in
40 gnustep-base-1.8.0/Tools/make_strings/Using.txt
41 and in the comments of method propertyListFromStringsFileFormat in
42 gnustep-base-1.8.0/Source/NSString.m
43 In summary, it's a Objective-C like file with pseudo-assignments of the form
44 "key" = "value";
45 where the key is the msgid and the value is the msgstr.
46 */
47
48 /* Handling of comments: We copy all comments from the PO file to the
49 .strings file. This is not really needed; it's a service for translators
50 who don't like PO files and prefer to maintain the .strings file. */
51
52 /* Since the interpretation of text files in GNUstep depends on the locale's
53 encoding if they don't have a BOM, we choose one of three encodings with
54 a BOM: UCS-2BE, UCS-2LE, UTF-8. Since the first two of these don't cope
55 with all of Unicode and we don't know whether GNUstep will switch to
56 UTF-16 instead of UCS-2, we use UTF-8 with BOM. BOMs are bad because they
57 get in the way when concatenating files, but here we have no choice. */
58
59 /* Writes a key or value to the stream, without newline. */
60 static void
write_escaped_string(ostream_t stream,const char * str)61 write_escaped_string (ostream_t stream, const char *str)
62 {
63 const char *str_limit = str + strlen (str);
64
65 ostream_write_str (stream, "\"");
66 while (str < str_limit)
67 {
68 unsigned char c = (unsigned char) *str++;
69
70 if (c == '\t')
71 ostream_write_str (stream, "\\t");
72 else if (c == '\n')
73 ostream_write_str (stream, "\\n");
74 else if (c == '\r')
75 ostream_write_str (stream, "\\r");
76 else if (c == '\f')
77 ostream_write_str (stream, "\\f");
78 else if (c == '\\' || c == '"')
79 {
80 char seq[2];
81 seq[0] = '\\';
82 seq[1] = c;
83 ostream_write_mem (stream, seq, 2);
84 }
85 else
86 {
87 char seq[1];
88 seq[0] = c;
89 ostream_write_mem (stream, seq, 1);
90 }
91 }
92 ostream_write_str (stream, "\"");
93 }
94
95 /* Writes a message to the stream. */
96 static void
write_message(ostream_t stream,const message_ty * mp,size_t page_width,bool debug)97 write_message (ostream_t stream, const message_ty *mp,
98 size_t page_width, bool debug)
99 {
100 /* Print translator comment if available. */
101 if (mp->comment != NULL)
102 {
103 size_t j;
104
105 for (j = 0; j < mp->comment->nitems; ++j)
106 {
107 const char *s = mp->comment->item[j];
108
109 /* Test whether it is safe to output the comment in C style, or
110 whether we need C++ style for it. */
111 if (c_strstr (s, "*/") == NULL)
112 {
113 ostream_write_str (stream, "/*");
114 if (*s != '\0' && *s != '\n')
115 ostream_write_str (stream, " ");
116 ostream_write_str (stream, s);
117 ostream_write_str (stream, " */\n");
118 }
119 else
120 do
121 {
122 const char *e;
123 ostream_write_str (stream, "//");
124 if (*s != '\0' && *s != '\n')
125 ostream_write_str (stream, " ");
126 e = strchr (s, '\n');
127 if (e == NULL)
128 {
129 ostream_write_str (stream, s);
130 s = NULL;
131 }
132 else
133 {
134 ostream_write_mem (stream, s, e - s);
135 s = e + 1;
136 }
137 ostream_write_str (stream, "\n");
138 }
139 while (s != NULL);
140 }
141 }
142
143 /* Print xgettext extracted comments. */
144 if (mp->comment_dot != NULL)
145 {
146 size_t j;
147
148 for (j = 0; j < mp->comment_dot->nitems; ++j)
149 {
150 const char *s = mp->comment_dot->item[j];
151
152 /* Test whether it is safe to output the comment in C style, or
153 whether we need C++ style for it. */
154 if (c_strstr (s, "*/") == NULL)
155 {
156 ostream_write_str (stream, "/* Comment: ");
157 ostream_write_str (stream, s);
158 ostream_write_str (stream, " */\n");
159 }
160 else
161 {
162 bool first = true;
163 do
164 {
165 const char *e;
166 ostream_write_str (stream, "//");
167 if (first || (*s != '\0' && *s != '\n'))
168 ostream_write_str (stream, " ");
169 if (first)
170 ostream_write_str (stream, "Comment: ");
171 e = strchr (s, '\n');
172 if (e == NULL)
173 {
174 ostream_write_str (stream, s);
175 s = NULL;
176 }
177 else
178 {
179 ostream_write_mem (stream, s, e - s);
180 s = e + 1;
181 }
182 ostream_write_str (stream, "\n");
183 first = false;
184 }
185 while (s != NULL);
186 }
187 }
188 }
189
190 /* Print the file position comments. */
191 if (mp->filepos_count != 0)
192 {
193 size_t j;
194
195 for (j = 0; j < mp->filepos_count; ++j)
196 {
197 lex_pos_ty *pp = &mp->filepos[j];
198 const char *cp = pp->file_name;
199 char *str;
200
201 while (cp[0] == '.' && cp[1] == '/')
202 cp += 2;
203 str = xasprintf ("/* File: %s:%ld */\n", cp, (long) pp->line_number);
204 ostream_write_str (stream, str);
205 free (str);
206 }
207 }
208
209 /* Print flag information in special comment. */
210 if (mp->is_fuzzy || mp->msgstr[0] == '\0')
211 ostream_write_str (stream, "/* Flag: untranslated */\n");
212 if (mp->obsolete)
213 ostream_write_str (stream, "/* Flag: unmatched */\n");
214 {
215 size_t i;
216 for (i = 0; i < NFORMATS; i++)
217 if (significant_format_p (mp->is_format[i]))
218 {
219 ostream_write_str (stream, "/* Flag: ");
220 ostream_write_str (stream,
221 make_format_description_string (mp->is_format[i],
222 format_language[i],
223 debug));
224 ostream_write_str (stream, " */\n");
225 }
226 }
227 if (has_range_p (mp->range))
228 {
229 char *string;
230
231 ostream_write_str (stream, "/* Flag: ");
232 string = make_range_description_string (mp->range);
233 ostream_write_str (stream, string);
234 free (string);
235 ostream_write_str (stream, " */\n");
236 }
237
238 /* Now write the untranslated string and the translated string. */
239 write_escaped_string (stream, mp->msgid);
240 ostream_write_str (stream, " = ");
241 if (mp->msgstr[0] != '\0')
242 {
243 if (mp->is_fuzzy)
244 {
245 /* Output the msgid as value, so that at runtime the untranslated
246 string is returned. */
247 write_escaped_string (stream, mp->msgid);
248
249 /* Output the msgstr as a comment, so that at runtime
250 propertyListFromStringsFileFormat ignores it. */
251 if (c_strstr (mp->msgstr, "*/") == NULL)
252 {
253 ostream_write_str (stream, " /* = ");
254 write_escaped_string (stream, mp->msgstr);
255 ostream_write_str (stream, " */");
256 }
257 else
258 {
259 ostream_write_str (stream, "; // = ");
260 write_escaped_string (stream, mp->msgstr);
261 }
262 }
263 else
264 write_escaped_string (stream, mp->msgstr);
265 }
266 else
267 {
268 /* Output the msgid as value, so that at runtime the untranslated
269 string is returned. */
270 write_escaped_string (stream, mp->msgid);
271 }
272 ostream_write_str (stream, ";");
273
274 ostream_write_str (stream, "\n");
275 }
276
277 /* Writes an entire message list to the stream. */
278 static void
write_stringtable(ostream_t stream,message_list_ty * mlp,const char * canon_encoding,size_t page_width,bool debug)279 write_stringtable (ostream_t stream, message_list_ty *mlp,
280 const char *canon_encoding, size_t page_width, bool debug)
281 {
282 bool blank_line;
283 size_t j;
284
285 /* Convert the messages to Unicode. */
286 iconv_message_list (mlp, canon_encoding, po_charset_utf8, NULL);
287
288 /* Output the BOM. */
289 if (!is_ascii_message_list (mlp))
290 ostream_write_str (stream, "\xef\xbb\xbf");
291
292 /* Loop through the messages. */
293 blank_line = false;
294 for (j = 0; j < mlp->nitems; ++j)
295 {
296 const message_ty *mp = mlp->item[j];
297
298 if (mp->msgid_plural == NULL)
299 {
300 if (blank_line)
301 ostream_write_str (stream, "\n");
302
303 write_message (stream, mp, page_width, debug);
304
305 blank_line = true;
306 }
307 }
308 }
309
310 /* Output the contents of a PO file in .strings syntax. */
311 static void
msgdomain_list_print_stringtable(msgdomain_list_ty * mdlp,ostream_t stream,size_t page_width,bool debug)312 msgdomain_list_print_stringtable (msgdomain_list_ty *mdlp, ostream_t stream,
313 size_t page_width, bool debug)
314 {
315 message_list_ty *mlp;
316
317 if (mdlp->nitems == 1)
318 mlp = mdlp->item[0]->messages;
319 else
320 mlp = message_list_alloc (false);
321 write_stringtable (stream, mlp, mdlp->encoding, page_width, debug);
322 }
323
324 /* Describes a PO file in .strings syntax. */
325 const struct catalog_output_format output_format_stringtable =
326 {
327 msgdomain_list_print_stringtable, /* print */
328 true, /* requires_utf8 */
329 false, /* supports_color */
330 false, /* supports_multiple_domains */
331 false, /* supports_contexts */
332 false, /* supports_plurals */
333 false, /* sorts_obsoletes_to_end */
334 false, /* alternative_is_po */
335 false /* alternative_is_java_class */
336 };
337