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