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