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