3 // TinyGetText - A small flexible gettext() replacement
4 // Copyright (C) 2004 Ingo Ruhnke <grumbel@gmx.de>
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.
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.
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.
20 #include <sys/types.h>
27 #include "tinygettext.h"
29 //#define TRANSLATION_DEBUG
31 namespace TinyGetText {
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)
38 if (from_charset == to_charset)
41 iconv_t cd = iconv_open(to_charset.c_str(), from_charset.c_str());
43 size_t in_len = text.length();
44 size_t out_len = text.length()*2;
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());
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;
59 std::cerr << strerror(errno) << std::endl;
60 std::cerr << "Error: conversion from " << from_charset
61 << " to " << to_charset << " went wrong: " << retval << std::endl;
65 std::string ret(out_orig, out_len);
71 bool has_suffix(const std::string& lhs, const std::string rhs)
73 if (lhs.length() < rhs.length())
76 return lhs.compare(lhs.length() - rhs.length(), rhs.length(), rhs) == 0;
79 bool has_prefix(const std::string& lhs, const std::string rhs)
81 if (lhs.length() < rhs.length())
84 return lhs.compare(0, rhs.length(), rhs) == 0;
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); }
98 /** Language Definitions */
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);"
136 get_language_def(const std::string& name)
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;
174 DictionaryManager::DictionaryManager()
175 : current_dict(&empty_dict)
177 parseLocaleAliases();
178 // setup language from environment vars
179 const char* lang = getenv("LC_ALL");
181 lang = getenv("LC_MESSAGES");
183 lang = getenv("LANG");
190 DictionaryManager::parseLocaleAliases()
192 // try to parse language alias list
193 std::ifstream in("/usr/share/locale/locale.alias");
196 while(in.good() && !in.eof()) {
197 while(isspace(c) && !in.eof())
200 if(c == '#') { // skip comments
201 while(c != '\n' && !in.eof())
207 while(!isspace(c) && !in.eof()) {
211 while(isspace(c) && !in.eof())
213 std::string language;
214 while(!isspace(c) && !in.eof()) {
221 set_language_alias(alias, language);
226 DictionaryManager::get_dictionary(const std::string& spec)
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())
234 else // Dictionary for languages lang isn't loaded, so we load it
236 //std::cout << "get_dictionary: " << lang << std::endl;
237 Dictionary& dict = dictionaries[lang];
239 dict.set_language(get_language_def(lang));
241 dict.set_charset(charset);
243 for (SearchPath::iterator p = search_path.begin(); p != search_path.end(); ++p)
245 DIR* dir = opendir(p->c_str());
248 std::cerr << "Error: opendir() failed on " << *p << std::endl;
253 while((ent = readdir(dir)))
255 if (std::string(ent->d_name) == lang + ".po")
257 std::string pofile = *p + "/" + ent->d_name;
258 std::ifstream in(pofile.c_str());
261 std::cerr << "Error: Failure file opening: " << pofile << std::endl;
265 read_po_file(dict, in);
277 std::set<std::string>
278 DictionaryManager::get_languages()
280 std::set<std::string> languages;
282 for (SearchPath::iterator p = search_path.begin(); p != search_path.end(); ++p)
284 DIR* dir = opendir(p->c_str());
287 std::cerr << "Error: opendir() failed on " << *p << std::endl;
292 while((ent = readdir(dir)))
294 if (has_suffix(ent->d_name, ".po"))
296 std::string filename = ent->d_name;
297 languages.insert(filename.substr(0, filename.length()-3));
307 DictionaryManager::set_language(const std::string& lang)
309 language = get_language_from_spec(lang);
310 current_dict = & (get_dictionary(language));
314 DictionaryManager::set_charset(const std::string& charset)
316 dictionaries.clear(); // changing charset invalidates cache
317 this->charset = charset;
318 set_language(language);
322 DictionaryManager::set_language_alias(const std::string& alias,
323 const std::string& language)
325 language_aliases.insert(std::make_pair(alias, language));
329 DictionaryManager::get_language_from_spec(const std::string& spec)
331 std::string lang = spec;
332 Aliases::iterator i = language_aliases.find(lang);
333 if(i != language_aliases.end()) {
337 std::string::size_type s = lang.find_first_of("_.");
338 if(s == std::string::npos)
341 return std::string(lang, 0, s);
345 DictionaryManager::add_directory(const std::string& pathname)
347 dictionaries.clear(); // adding directories invalidates cache
348 search_path.push_back(pathname);
349 set_language(language);
352 //---------------------------------------------------------------------------
354 Dictionary::Dictionary(const LanguageDef& language_, const std::string& charset_)
355 : language(language_), charset(charset_)
359 Dictionary::Dictionary()
365 Dictionary::get_charset() const
371 Dictionary::set_charset(const std::string& charset_)
377 Dictionary::set_language(const LanguageDef& lang)
383 Dictionary::translate(const std::string& msgid, const std::string& msgid2, int num)
385 PluralEntries::iterator i = plural_entries.find(msgid);
386 std::map<int, std::string>& msgstrs = i->second;
388 if (i != plural_entries.end() && !msgstrs.empty())
390 int g = language.plural(num);
391 std::map<int, std::string>::iterator j = msgstrs.find(g);
392 if (j != msgstrs.end())
398 // Return the first translation, in case we can't translate the specific number
399 return msgstrs.begin()->second;
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;
411 if (plural2_1(num)) // default to english rules
419 Dictionary::translate(const std::string& msgid)
421 Entries::iterator i = entries.find(msgid);
422 if (i != entries.end() && !i->second.empty())
428 #ifdef TRANSLATION_DBEUG
429 std::cout << "Error: Couldn't translate: " << msgid << std::endl;
436 Dictionary::add_translation(const std::string& msgid, const std::string& ,
437 const std::map<int, std::string>& msgstrs)
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;
445 Dictionary::add_translation(const std::string& msgid, const std::string& msgstr)
447 entries[msgid] = msgstr;
461 std::string from_charset;
462 std::string to_charset;
464 std::string current_msgid;
465 std::string current_msgid_plural;
466 std::map<int, std::string> msgstr_plural;
470 enum { WANT_MSGID, WANT_MSGSTR, WANT_MSGSTR_PLURAL, WANT_MSGID_PLURAL } state;
473 POFileReader(std::istream& in, Dictionary& dict_)
481 void parse_header(const std::string& header)
483 // Seperate the header in lines
484 typedef std::vector<std::string> Lines;
487 std::string::size_type start = 0;
488 for(std::string::size_type i = 0; i < header.length(); ++i)
490 if (header[i] == '\n')
492 lines.push_back(header.substr(start, i - start));
497 for(Lines::iterator i = lines.begin(); i != lines.end(); ++i)
499 if (has_prefix(*i, "Content-Type: text/plain; charset=")) {
500 from_charset = i->substr(strlen("Content-Type: text/plain; charset="));
504 if (from_charset.empty() || from_charset == "CHARSET")
506 std::cerr << "Error: Charset not specified for .po, fallback to ISO-8859-1" << std::endl;
507 from_charset = "ISO-8859-1";
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);
518 void add_token(const Token& token)
523 if (token.keyword == "msgid")
525 current_msgid = token.content;
526 state = WANT_MSGID_PLURAL;
528 else if (token.keyword.empty())
530 //std::cerr << "Got EOF, everything looks ok." << std::endl;
534 std::cerr << "tinygettext: expected 'msgid' keyword, got " << token.keyword
535 << " at line " << line_num << std::endl;
539 case WANT_MSGID_PLURAL:
540 if (token.keyword == "msgid_plural")
542 current_msgid_plural = token.content;
543 state = WANT_MSGSTR_PLURAL;
553 if (token.keyword == "msgstr")
555 if (current_msgid == "")
556 { // .po Header is hidden in the msgid with the empty string
557 parse_header(token.content);
561 dict.add_translation(current_msgid, convert(token.content, from_charset, to_charset));
567 std::cerr << "tinygettext: expected 'msgstr' keyword, got " << token.keyword
568 << " at line " << line_num << std::endl;
572 case WANT_MSGSTR_PLURAL:
573 if (has_prefix(token.keyword, "msgstr["))
576 if (sscanf(token.keyword.c_str(), "msgstr[%d]", &num) != 1)
578 std::cerr << "Error: Couldn't parse: " << token.keyword << std::endl;
582 msgstr_plural[num] = convert(token.content, from_charset, to_charset);
587 dict.add_translation(current_msgid, current_msgid_plural, msgstr_plural);
596 inline int getchar(std::istream& in)
604 void tokenize_po(std::istream& in)
606 enum State { READ_KEYWORD,
608 READ_CONTENT_IN_STRING,
611 State state = READ_KEYWORD;
615 while((c = getchar(in)) != EOF)
617 //std::cout << "Lexing char: " << char(c) << " " << state << std::endl;
623 state = SKIP_COMMENT;
632 } while((c = getchar(in)) != EOF && !isspace(c));
635 state = READ_CONTENT;
640 while((c = getchar(in)) != EOF)
643 // Found start of content
644 state = READ_CONTENT_IN_STRING;
646 } else if (isspace(c)) {
648 } else { // Read something that may be a keyword
650 state = READ_KEYWORD;
657 case READ_CONTENT_IN_STRING:
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 += '"';
668 std::cout << "Unhandled escape character: " << char(c) << std::endl;
673 std::cout << "Unterminated string" << std::endl;
675 } else if (c == '"') { // Content string is terminated
676 state = READ_CONTENT;
684 state = READ_KEYWORD;
692 void read_po_file(Dictionary& dict_, std::istream& in)
694 POFileReader reader(in, dict_);
697 } // namespace TinyGetText