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