Unified Messaging Subsystem
[supertux.git] / src / tinygettext / tinygettext.cpp
1 //  $Id$
2 // 
3 //  TinyGetText - A small flexible gettext() replacement
4 //  Copyright (C) 2004 Ingo Ruhnke <grumbel@gmx.de>
5 //
6 //  This program is free software; you can redistribute it and/or
7 //  modify it under the terms of the GNU General Public License
8 //  as published by the Free Software Foundation; either version 2
9 //  of the License, or (at your option) any later version.
10 //
11 //  This program is distributed in the hope that it will be useful,
12 //  but WITHOUT ANY WARRANTY; without even the implied warranty of
13 //  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14 //  GNU General Public License for more details.
15 // 
16 //  You should have received a copy of the GNU General Public License
17 //  along with this program; if not, write to the Free Software
18 //  Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
19 #include <config.h>
20
21 #include <sys/types.h>
22 #include <iconv.h>
23 #include <fstream>
24 #include <iostream>
25 #include <ctype.h>
26 #include <errno.h>
27
28 #include "tinygettext.hpp"
29 #include "msg.hpp"
30 #include "physfs/physfs_stream.hpp"
31 #include "msg.hpp"
32
33 //#define TRANSLATION_DEBUG
34
35 namespace TinyGetText {
36
37 /** Convert \a which is in \a from_charset to \a to_charset and return it */
38 std::string convert(const std::string& text,
39                     const std::string& from_charset,
40                     const std::string& to_charset)
41 {
42   if (from_charset == to_charset)
43     return text;
44
45   iconv_t cd = iconv_open(to_charset.c_str(), from_charset.c_str());
46   
47   size_t in_len = text.length();
48   size_t out_len = text.length()*3; // FIXME: cross fingers that this is enough
49
50   char*  out_orig = new char[out_len];
51   char*  in_orig  = new char[in_len+1];
52   strcpy(in_orig, text.c_str());
53
54   char* out = out_orig;
55   ICONV_CONST char* in  = in_orig;
56   size_t out_len_temp = out_len; // iconv is counting down the bytes it has
57                                  // written from this...
58
59   size_t retval = iconv(cd, &in, &in_len, &out, &out_len_temp);
60   out_len -= out_len_temp; // see above
61   if (retval == (size_t) -1)
62     {
63       msg_warning(strerror(errno));
64       msg_warning("Error: conversion from " << from_charset
65                 << " to " << to_charset << " went wrong: " << retval);
66       return "";
67     }
68   iconv_close(cd);
69
70   std::string ret(out_orig, out_len);
71   delete[] out_orig;
72   delete[] in_orig;
73   return ret;
74 }
75
76 bool has_suffix(const std::string& lhs, const std::string rhs)
77 {
78   if (lhs.length() < rhs.length())
79     return false;
80   else
81     return lhs.compare(lhs.length() - rhs.length(), rhs.length(), rhs) == 0;
82 }
83
84 bool has_prefix(const std::string& lhs, const std::string rhs)
85 {
86   if (lhs.length() < rhs.length())
87     return false;
88   else
89     return lhs.compare(0, rhs.length(), rhs) == 0;
90 }
91
92 int plural1(int )     { return 0; }
93 int plural2_1(int n)  { return (n != 1); }
94 int plural2_2(int n)  { return (n > 1); }
95 int plural3_lv(int n) { return (n%10==1 && n%100!=11 ? 0 : n != 0 ? 1 : 2); }
96 int plural3_ga(int n) { return n==1 ? 0 : n==2 ? 1 : 2; }
97 int plural3_lt(int n) { return (n%10==1 && n%100!=11 ? 0 : n%10>=2 && (n%100<10 || n%100>=20) ? 1 : 2); }
98 int plural3_1(int n)  { return (n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2); }
99 int plural3_sk(int n) { return (n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2; }
100 int plural3_pl(int n) { return (n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2); }
101 int plural3_sl(int n) { return (n%100==1 ? 0 : n%100==2 ? 1 : n%100==3 || n%100==4 ? 2 : 3); }
102
103 /** Language Definitions */
104 //*{
105 LanguageDef lang_hu("hu", "Hungarian",         1, plural1); // "nplurals=1; plural=0;"
106 LanguageDef lang_ja("ja", "Japanese",          1, plural1); // "nplurals=1; plural=0;"
107 LanguageDef lang_ko("ko", "Korean",            1, plural1); // "nplurals=1; plural=0;"
108 LanguageDef lang_tr("tr", "Turkish",           1, plural1); // "nplurals=1; plural=0;"
109 LanguageDef lang_da("da", "Danish",            2, plural2_1); // "nplurals=2; plural=(n != 1);"
110 LanguageDef lang_nl("nl", "Dutch",             2, plural2_1); // "nplurals=2; plural=(n != 1);"
111 LanguageDef lang_en("en", "English",           2, plural2_1); // "nplurals=2; plural=(n != 1);"
112 LanguageDef lang_fo("fo", "Faroese",           2, plural2_1); // "nplurals=2; plural=(n != 1);"
113 LanguageDef lang_de("de", "German",            2, plural2_1); // "nplurals=2; plural=(n != 1);"
114 LanguageDef lang_nb("nb", "Norwegian Bokmal",  2, plural2_1); // "nplurals=2; plural=(n != 1);"
115 LanguageDef lang_no("no", "Norwegian",         2, plural2_1); // "nplurals=2; plural=(n != 1);"
116 LanguageDef lang_nn("nn", "Norwegian Nynorsk", 2, plural2_1); // "nplurals=2; plural=(n != 1);"
117 LanguageDef lang_sv("sv", "Swedish",           2, plural2_1); // "nplurals=2; plural=(n != 1);"
118 LanguageDef lang_et("et", "Estonian",          2, plural2_1); // "nplurals=2; plural=(n != 1);"
119 LanguageDef lang_fi("fi", "Finnish",           2, plural2_1); // "nplurals=2; plural=(n != 1);"
120 LanguageDef lang_el("el", "Greek",             2, plural2_1); // "nplurals=2; plural=(n != 1);"
121 LanguageDef lang_he("he", "Hebrew",            2, plural2_1); // "nplurals=2; plural=(n != 1);"
122 LanguageDef lang_it("it", "Italian",           2, plural2_1); // "nplurals=2; plural=(n != 1);"
123 LanguageDef lang_pt("pt", "Portuguese",        2, plural2_1); // "nplurals=2; plural=(n != 1);"
124 LanguageDef lang_es("es", "Spanish",           2, plural2_1); // "nplurals=2; plural=(n != 1);"
125 LanguageDef lang_eo("eo", "Esperanto",         2, plural2_1); // "nplurals=2; plural=(n != 1);"
126 LanguageDef lang_fr("fr", "French",            2, plural2_2); // "nplurals=2; plural=(n > 1);"
127 LanguageDef lang_pt_BR("pt_BR", "Brazilian",   2, plural2_2); // "nplurals=2; plural=(n > 1);"
128 LanguageDef lang_lv("lv", "Latvian",           3, plural3_lv); // "nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n != 0 ? 1 : 2);"
129 LanguageDef lang_ga("ga", "Irish",             3, plural3_ga); // "nplurals=3; plural=n==1 ? 0 : n==2 ? 1 : 2;"
130 LanguageDef lang_lt("lt", "Lithuanian",        3, plural3_lt); // "nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && (n%100<10 || n%100>=20) ? 1 : 2);"
131 LanguageDef lang_hr("hr", "Croatian",          3, plural3_1); // "nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);"
132 LanguageDef lang_cs("cs", "Czech",             3, plural3_1); // "nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);"
133 LanguageDef lang_ru("ru", "Russian",           3, plural3_1); // "nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);"
134 LanguageDef lang_uk("uk", "Ukrainian",         3, plural3_1); // "nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);"
135 LanguageDef lang_sk("sk", "Slovak",            3, plural3_sk); // "nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;"
136 LanguageDef lang_pl("pl", "Polish",            3, plural3_pl); // "nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);
137 LanguageDef lang_sl("sl", "Slovenian",         3, plural3_sl); // "nplurals=4; plural=(n%100==1 ? 0 : n%100==2 ? 1 : n%100==3 || n%100==4 ? 2 : 3);"
138 //*}
139
140 LanguageDef&
141 get_language_def(const std::string& name)
142 {
143   if (name == "hu") return lang_hu;
144   else if (name == "ja") return lang_ja;
145   else if (name == "ko") return lang_ko;
146   else if (name == "tr") return lang_tr;
147   else if (name == "da") return lang_da;
148   else if (name == "nl") return lang_nl;
149   else if (name == "en") return lang_en;
150   else if (name == "fo") return lang_fo;
151   else if (name == "de") return lang_de;
152   else if (name == "nb") return lang_nb;
153   else if (name == "no") return lang_no;
154   else if (name == "nn") return lang_nn;
155   else if (name == "sv") return lang_sv;
156   else if (name == "et") return lang_et;
157   else if (name == "fi") return lang_fi;
158   else if (name == "el") return lang_el;
159   else if (name == "he") return lang_he;
160   else if (name == "it") return lang_it;
161   else if (name == "pt") return lang_pt;
162   else if (name == "es") return lang_es;
163   else if (name == "eo") return lang_eo;
164   else if (name == "fr") return lang_fr;
165   else if (name == "pt_BR") return lang_pt_BR;
166   else if (name == "lv") return lang_lv;
167   else if (name == "ga") return lang_ga;
168   else if (name == "lt") return lang_lt;
169   else if (name == "hr") return lang_hr;
170   else if (name == "cs") return lang_cs;
171   else if (name == "ru") return lang_ru;
172   else if (name == "uk") return lang_uk;
173   else if (name == "sk") return lang_sk;
174   else if (name == "pl") return lang_pl;
175   else if (name == "sl") return lang_sl;
176   else return lang_en; 
177 }
178
179 DictionaryManager::DictionaryManager()
180   : current_dict(&empty_dict)
181 {
182   parseLocaleAliases();
183   // setup language from environment vars
184   const char* lang = getenv("LC_ALL");
185   if(!lang)
186     lang = getenv("LC_MESSAGES");
187   if(!lang)
188     lang = getenv("LANG");
189   
190   if(lang)
191     set_language(lang);
192 }
193
194 void
195 DictionaryManager::parseLocaleAliases()
196 {
197   // try to parse language alias list
198   std::ifstream in("/usr/share/locale/locale.alias");
199   
200   char c = ' ';
201   while(in.good() && !in.eof()) {
202     while(isspace(c) && !in.eof())
203       in.get(c);
204     
205     if(c == '#') { // skip comments
206       while(c != '\n' && !in.eof())
207         in.get(c);
208       continue;
209     }
210     
211     std::string alias;
212     while(!isspace(c) && !in.eof()) {
213       alias += c;
214       in.get(c);
215     }
216     while(isspace(c) && !in.eof())
217       in.get(c);
218     std::string language;
219     while(!isspace(c) && !in.eof()) {
220       language += c;
221       in.get(c);
222     }
223
224     if(in.eof())
225       break;
226     set_language_alias(alias, language);
227   }
228 }
229   
230 Dictionary&
231 DictionaryManager::get_dictionary(const std::string& spec)
232 {
233   std::string lang = get_language_from_spec(spec);
234   Dictionaries::iterator i = dictionaries.find(get_language_from_spec(lang));
235   if (i != dictionaries.end())
236     {
237       return i->second;
238     }
239   else // Dictionary for languages lang isn't loaded, so we load it
240     {
241       //msg_debug("get_dictionary: " << lang);
242       Dictionary& dict = dictionaries[lang];
243
244       dict.set_language(get_language_def(lang));
245       if(charset != "")
246         dict.set_charset(charset);
247
248       for (SearchPath::iterator p = search_path.begin(); p != search_path.end(); ++p)
249         {
250           char** files = PHYSFS_enumerateFiles(p->c_str());
251           if(!files) 
252             {
253               msg_warning("Error: enumerateFiles() failed on " << *p);
254             }
255           else
256             {
257               for(const char* const* filename = files;
258                       *filename != 0; filename++) {
259                 if(std::string(*filename) == lang + ".po") {
260                   std::string pofile = *p + "/" + *filename;
261                   try {
262                       IFileStream in(pofile);
263                       read_po_file(dict, in);
264                   } catch(std::exception& e) {
265                       msg_warning("Error: Failure file opening: " << pofile);
266                       msg_warning(e.what() << "");
267                   }
268                 }
269               }
270               PHYSFS_freeList(files);
271             }
272         }
273
274       return dict;
275     }
276 }
277
278 std::set<std::string>
279 DictionaryManager::get_languages()
280 {
281   std::set<std::string> languages;
282
283   for (SearchPath::iterator p = search_path.begin(); p != search_path.end(); ++p)
284     {
285       char** files = PHYSFS_enumerateFiles(p->c_str());
286       if (!files)
287         {
288           msg_warning("Error: opendir() failed on " << *p);
289         }
290       else
291         {
292           for(const char* const* file = files; *file != 0; file++) {
293               if(has_suffix(*file, ".po")) {
294                   std::string filename = *file;
295                   languages.insert(filename.substr(0, filename.length()-3));
296               }
297           }
298           PHYSFS_freeList(files);
299         }
300     }  
301   return languages;
302 }
303
304 void
305 DictionaryManager::set_language(const std::string& lang)
306 {
307   language = get_language_from_spec(lang);
308   current_dict = & (get_dictionary(language));
309 }
310
311 const std::string&
312 DictionaryManager::get_language() const
313 {
314   return language;
315 }
316
317 void
318 DictionaryManager::set_charset(const std::string& charset)
319 {
320   dictionaries.clear(); // changing charset invalidates cache
321   this->charset = charset;
322   set_language(language);
323 }
324
325 void
326 DictionaryManager::set_language_alias(const std::string& alias,
327     const std::string& language)
328 {
329   language_aliases.insert(std::make_pair(alias, language));
330 }
331
332 std::string
333 DictionaryManager::get_language_from_spec(const std::string& spec)
334 {
335   std::string lang = spec;
336   Aliases::iterator i = language_aliases.find(lang);
337   if(i != language_aliases.end()) {
338     lang = i->second;
339   }
340   
341   std::string::size_type s = lang.find_first_of("_.");
342   if(s == std::string::npos)
343     return lang;
344
345   return std::string(lang, 0, s);  
346 }
347
348 void
349 DictionaryManager::add_directory(const std::string& pathname)
350 {
351   dictionaries.clear(); // adding directories invalidates cache
352   search_path.push_back(pathname);
353   set_language(language);
354 }
355
356 //---------------------------------------------------------------------------
357
358 Dictionary::Dictionary(const LanguageDef& language_, const std::string& charset_)
359   : language(language_), charset(charset_)
360 {
361 }
362
363 Dictionary::Dictionary()
364   : language(lang_en)
365 {
366 }
367
368 std::string
369 Dictionary::get_charset() const
370 {
371   return charset;
372 }
373
374 void
375 Dictionary::set_charset(const std::string& charset_)
376 {
377   charset = charset_;
378 }
379
380 void
381 Dictionary::set_language(const LanguageDef& lang)
382 {
383   language = lang;
384 }
385
386 std::string
387 Dictionary::translate(const std::string& msgid, const std::string& msgid2, int num) 
388 {
389   PluralEntries::iterator i = plural_entries.find(msgid);
390   std::map<int, std::string>& msgstrs = i->second;
391
392   if (i != plural_entries.end() && !msgstrs.empty())
393     {
394       int g = language.plural(num);
395       std::map<int, std::string>::iterator j = msgstrs.find(g);
396       if (j != msgstrs.end())
397         {
398           return j->second;
399         }
400       else
401         {
402           // Return the first translation, in case we can't translate the specific number
403           return msgstrs.begin()->second;
404         }
405     }
406   else
407     {
408 #ifdef TRANSLATION_DEBUG
409       msg_warning("Couldn't translate: " << msgid);
410       msg_warning("Candidates: ");
411       for (PluralEntries::iterator i = plural_entries.begin(); i != plural_entries.end(); ++i)
412         msg_debug("'" << i->first << "'");
413 #endif
414
415       if (plural2_1(num)) // default to english rules
416         return msgid2;
417       else
418         return msgid;
419     }
420 }
421
422 const char*
423 Dictionary::translate(const char* msgid)
424 {
425   Entries::iterator i = entries.find(msgid);
426   if (i != entries.end() && !i->second.empty())
427     {
428       return i->second.c_str();
429     }
430   else
431     {
432 #ifdef TRANSLATION_DBEUG
433       msg_warning("Couldn't translate: " << msgid);
434 #endif
435       return msgid;
436     }
437 }
438
439 std::string
440 Dictionary::translate(const std::string& msgid) 
441 {
442   Entries::iterator i = entries.find(msgid);
443   if (i != entries.end() && !i->second.empty())
444     {
445       return i->second;
446     }
447   else
448     {
449 #ifdef TRANSLATION_DBEUG
450       msg_warning("Couldn't translate: " << msgid);
451 #endif
452       return msgid;
453     }
454 }
455   
456 void
457 Dictionary::add_translation(const std::string& msgid, const std::string& ,
458                             const std::map<int, std::string>& msgstrs)
459 {
460   // Do we need msgid2 for anything? its after all supplied to the
461   // translate call, so we just throw it away
462   plural_entries[msgid] = msgstrs;
463 }
464
465 void 
466 Dictionary::add_translation(const std::string& msgid, const std::string& msgstr) 
467 {
468   entries[msgid] = msgstr;
469 }
470
471 class POFileReader
472 {
473 private:
474   struct Token
475   {
476     std::string keyword;
477     std::string content;
478   };
479
480   Dictionary& dict;
481
482   std::string from_charset;
483   std::string to_charset;
484
485   std::string current_msgid;
486   std::string current_msgid_plural;
487   std::map<int, std::string> msgstr_plural;
488
489   int line_num;
490
491   enum { WANT_MSGID, WANT_MSGSTR, WANT_MSGSTR_PLURAL, WANT_MSGID_PLURAL } state;
492
493 public:
494   POFileReader(std::istream& in, Dictionary& dict_)
495     : dict(dict_)
496   {
497     state = WANT_MSGID;
498     line_num = 0;
499     char c = in.get();
500     if(c == (char) 0xef) { // skip UTF-8 intro that some texteditors produce
501         in.get();
502         in.get();
503     } else {
504         in.unget();
505     }
506     tokenize_po(in);
507   }
508
509   void parse_header(const std::string& header)
510   {
511     // Seperate the header in lines
512     typedef std::vector<std::string> Lines;
513     Lines lines;
514     
515     std::string::size_type start = 0;
516     for(std::string::size_type i = 0; i < header.length(); ++i)
517       {
518         if (header[i] == '\n')
519           {
520             lines.push_back(header.substr(start, i - start));
521             start = i+1;
522           }
523       }
524
525     for(Lines::iterator i = lines.begin(); i != lines.end(); ++i)
526       {
527         if (has_prefix(*i, "Content-Type: text/plain; charset=")) {
528           from_charset = i->substr(strlen("Content-Type: text/plain; charset="));
529         }
530       }
531
532     if (from_charset.empty() || from_charset == "CHARSET")
533       {
534         msg_warning("Error: Charset not specified for .po, fallback to ISO-8859-1");
535         from_charset = "ISO-8859-1";
536       }
537
538     to_charset = dict.get_charset();
539     if (to_charset.empty())
540       { // No charset requested from the dict, use utf-8
541         to_charset = "utf-8";
542         dict.set_charset(from_charset);
543       }
544   }
545
546   void add_token(const Token& token)
547   {
548     switch(state) 
549       {
550       case WANT_MSGID:
551         if (token.keyword == "msgid") 
552           {
553             current_msgid = token.content;
554             state = WANT_MSGID_PLURAL;
555           }
556         else if (token.keyword.empty())
557           {
558             //msg_warning("Got EOF, everything looks ok.");
559           }
560         else
561           {
562             msg_warning("tinygettext: expected 'msgid' keyword, got " << token.keyword 
563                       << " at line " << line_num);
564           }
565         break;
566     
567       case WANT_MSGID_PLURAL:
568         if (token.keyword == "msgid_plural") 
569           {
570             current_msgid_plural = token.content;
571             state = WANT_MSGSTR_PLURAL;
572           } 
573         else
574           {
575             state = WANT_MSGSTR;
576             add_token(token);
577           }
578         break;
579
580       case WANT_MSGSTR:
581         if (token.keyword == "msgstr") 
582           {
583             if (current_msgid == "") 
584               { // .po Header is hidden in the msgid with the empty string
585                 parse_header(token.content);
586               }
587             else
588               {
589                 dict.add_translation(current_msgid, convert(token.content, from_charset, to_charset));
590               }
591             state = WANT_MSGID;
592           } 
593         else
594           {
595             msg_warning("tinygettext: expected 'msgstr' keyword, got " << token.keyword 
596                       << " at line " << line_num);
597           }
598         break;
599
600       case WANT_MSGSTR_PLURAL:
601         if (has_prefix(token.keyword, "msgstr[")) 
602           {
603             int num;
604             if (sscanf(token.keyword.c_str(), "msgstr[%d]", &num) != 1) 
605               {
606                 msg_warning("Error: Couldn't parse: " << token.keyword);
607               } 
608             else 
609               {
610                 msgstr_plural[num] = convert(token.content, from_charset, to_charset);
611               }
612           }
613         else 
614           {
615             dict.add_translation(current_msgid, current_msgid_plural, msgstr_plural);
616
617             state = WANT_MSGID;
618             add_token(token);
619           }
620         break;
621       }
622   }
623   
624   inline int getchar(std::istream& in) 
625   {
626     int c = in.get();
627     if (c == '\n')
628       line_num += 1;
629     return c;
630   }
631   
632   void tokenize_po(std::istream& in)
633   {
634     enum State { READ_KEYWORD, 
635                  READ_CONTENT,
636                  READ_CONTENT_IN_STRING,
637                  SKIP_COMMENT };
638
639     State state = READ_KEYWORD;
640     int c;
641     Token token;
642
643     while((c = getchar(in)) != EOF)
644       {
645         //msg_debug("Lexing char: " << char(c) << " " << state);
646         switch(state)
647           {
648           case READ_KEYWORD:
649             if (c == '#')
650               {
651                 state = SKIP_COMMENT;
652               }
653             else
654               {
655                 // Read a new token
656                 token = Token();
657                 
658                 do { // Read keyword 
659                   token.keyword += c;
660                 } while((c = getchar(in)) != EOF && !isspace(c));
661                 in.unget();
662
663                 state = READ_CONTENT;
664               }
665             break;
666
667           case READ_CONTENT:
668             while((c = getchar(in)) != EOF)
669               {
670                 if (c == '"') { 
671                   // Found start of content
672                   state = READ_CONTENT_IN_STRING;
673                   break;
674                 } else if (isspace(c)) {
675                   // skip
676                 } else { // Read something that may be a keyword
677                   in.unget();
678                   state = READ_KEYWORD;
679                   add_token(token);
680                   break;
681                 }
682               }
683             break;
684
685           case READ_CONTENT_IN_STRING:
686             if (c == '\\') {
687               c = getchar(in);
688               if (c != EOF)
689                 {
690                   if (c == 'n') token.content += '\n';
691                   else if (c == 't') token.content += '\t';
692                   else if (c == 'r') token.content += '\r';
693                   else if (c == '"') token.content += '"';
694                   else
695                     {
696                       msg_warning("Unhandled escape character: " << char(c));
697                     }
698                 }
699               else
700                 {
701                   msg_warning("Unterminated string");
702                 }
703             } else if (c == '"') { // Content string is terminated
704               state = READ_CONTENT;
705             } else {
706               token.content += c;
707             }
708             break;
709
710           case SKIP_COMMENT:
711             if (c == '\n')
712               state = READ_KEYWORD;
713             break;
714           }
715       }
716     add_token(token);
717   }
718 };
719
720 void read_po_file(Dictionary& dict_, std::istream& in) 
721 {
722   POFileReader reader(in, dict_);
723 }
724
725 } // namespace TinyGetText
726
727 /* EOF */