1 // 2 // Copyright (c) 2009-2011 Artyom Beilis (Tonkikh) 3 // 4 // Distributed under the Boost Software License, Version 1.0. (See 5 // accompanying file LICENSE_1_0.txt or copy at 6 // http://www.boost.org/LICENSE_1_0.txt) 7 // 8 #define BOOST_LOCALE_SOURCE 9 #include <boost/config.hpp> 10 #include <boost/locale/message.hpp> 11 #include <boost/locale/gnu_gettext.hpp> 12 #include <boost/shared_ptr.hpp> 13 #include <boost/locale/encoding.hpp> 14 #ifdef BOOST_MSVC 15 # pragma warning(disable : 4996) 16 #endif 17 18 19 #if BOOST_VERSION >= 103600 20 #define BOOST_LOCALE_UNORDERED_CATALOG 21 #endif 22 23 #ifdef BOOST_LOCALE_UNORDERED_CATALOG 24 #include <boost/unordered_map.hpp> 25 #else 26 #include <map> 27 #endif 28 29 #include <iostream> 30 31 32 #include "mo_hash.hpp" 33 #include "mo_lambda.hpp" 34 35 #include <stdio.h> 36 37 #include <string.h> 38 39 namespace boost { 40 namespace locale { 41 namespace gnu_gettext { 42 43 class c_file { 44 c_file(c_file const &); 45 void operator=(c_file const &); 46 public: 47 48 FILE *file; 49 c_file()50 c_file() : 51 file(0) 52 { 53 } ~c_file()54 ~c_file() 55 { 56 close(); 57 } 58 close()59 void close() 60 { 61 if(file) { 62 fclose(file); 63 file=0; 64 } 65 } 66 67 #if defined(BOOST_WINDOWS) 68 open(std::string const & file_name,std::string const & encoding)69 bool open(std::string const &file_name,std::string const &encoding) 70 { 71 close(); 72 73 // 74 // Under windows we have to use "_wfopen" to get 75 // access to path's with Unicode in them 76 // 77 // As not all standard C++ libraries support nonstandard std::istream::open(wchar_t const *) 78 // we would use old and good stdio and _wfopen CRTL functions 79 // 80 81 std::wstring wfile_name = conv::to_utf<wchar_t>(file_name,encoding); 82 file = _wfopen(wfile_name.c_str(),L"rb"); 83 84 return file!=0; 85 } 86 87 #else // POSIX systems do not have all this Wide API crap, as native codepages are UTF-8 88 89 // We do not use encoding as we use native file name encoding 90 open(std::string const & file_name,std::string const &)91 bool open(std::string const &file_name,std::string const &/* encoding */) 92 { 93 close(); 94 95 file = fopen(file_name.c_str(),"rb"); 96 97 return file!=0; 98 } 99 100 #endif 101 102 }; 103 104 class mo_file { 105 public: 106 typedef std::pair<char const *,char const *> pair_type; 107 mo_file(std::vector<char> & file)108 mo_file(std::vector<char> &file) : 109 native_byteorder_(true), 110 size_(0) 111 { 112 load_file(file); 113 init(); 114 } 115 mo_file(FILE * file)116 mo_file(FILE *file) : 117 native_byteorder_(true), 118 size_(0) 119 { 120 load_file(file); 121 init(); 122 } 123 find(char const * context_in,char const * key_in) const124 pair_type find(char const *context_in,char const *key_in) const 125 { 126 pair_type null_pair((char const *)0,(char const *)0); 127 if(hash_size_==0) 128 return null_pair; 129 uint32_t hkey = 0; 130 if(context_in == 0) 131 hkey = pj_winberger_hash_function(key_in); 132 else { 133 pj_winberger_hash::state_type st = pj_winberger_hash::initial_state; 134 st = pj_winberger_hash::update_state(st,context_in); 135 st = pj_winberger_hash::update_state(st,'\4'); // EOT 136 st = pj_winberger_hash::update_state(st,key_in); 137 hkey = st; 138 } 139 uint32_t incr = 1 + hkey % (hash_size_-2); 140 hkey %= hash_size_; 141 uint32_t orig=hkey; 142 143 144 do { 145 uint32_t idx = get(hash_offset_ + 4*hkey); 146 /// Not found 147 if(idx == 0) 148 return null_pair; 149 /// If equal values return translation 150 if(key_equals(key(idx-1),context_in,key_in)) 151 return value(idx-1); 152 /// Rehash 153 hkey=(hkey + incr) % hash_size_; 154 } while(hkey!=orig); 155 return null_pair; 156 } 157 key_equals(char const * real_key,char const * cntx,char const * key)158 static bool key_equals(char const *real_key,char const *cntx,char const *key) 159 { 160 if(cntx == 0) 161 return strcmp(real_key,key) == 0; 162 else { 163 size_t real_len = strlen(real_key); 164 size_t cntx_len = strlen(cntx); 165 size_t key_len = strlen(key); 166 if(cntx_len + 1 + key_len != real_len) 167 return false; 168 return 169 memcmp(real_key,cntx,cntx_len) == 0 170 && real_key[cntx_len] == '\4' 171 && memcmp(real_key + cntx_len + 1 ,key,key_len) == 0; 172 } 173 } 174 key(int id) const175 char const *key(int id) const 176 { 177 uint32_t off = get(keys_offset_ + id*8 + 4); 178 return data_ + off; 179 } 180 value(int id) const181 pair_type value(int id) const 182 { 183 uint32_t len = get(translations_offset_ + id*8); 184 uint32_t off = get(translations_offset_ + id*8 + 4); 185 if(off >= file_size_ || off + len >= file_size_) 186 throw std::runtime_error("Bad mo-file format"); 187 return pair_type(&data_[off],&data_[off]+len); 188 } 189 has_hash() const190 bool has_hash() const 191 { 192 return hash_size_ != 0; 193 } 194 size() const195 size_t size() const 196 { 197 return size_; 198 } 199 empty()200 bool empty() 201 { 202 return size_ == 0; 203 } 204 205 private: init()206 void init() 207 { 208 // Read all format sizes 209 size_=get(8); 210 keys_offset_=get(12); 211 translations_offset_=get(16); 212 hash_size_=get(20); 213 hash_offset_=get(24); 214 } 215 load_file(std::vector<char> & data)216 void load_file(std::vector<char> &data) 217 { 218 vdata_.swap(data); 219 file_size_ = vdata_.size(); 220 data_ = &vdata_[0]; 221 if(file_size_ < 4 ) 222 throw std::runtime_error("invalid 'mo' file format - the file is too short"); 223 uint32_t magic=0; 224 memcpy(&magic,data_,4); 225 if(magic == 0x950412de) 226 native_byteorder_ = true; 227 else if(magic == 0xde120495) 228 native_byteorder_ = false; 229 else 230 throw std::runtime_error("Invalid file format - invalid magic number"); 231 } 232 load_file(FILE * file)233 void load_file(FILE *file) 234 { 235 uint32_t magic=0; 236 // if the size is wrong magic would be wrong 237 // ok to ingnore fread result 238 size_t four_bytes = fread(&magic,4,1,file); 239 (void)four_bytes; // shut GCC 240 241 if(magic == 0x950412de) 242 native_byteorder_ = true; 243 else if(magic == 0xde120495) 244 native_byteorder_ = false; 245 else 246 throw std::runtime_error("Invalid file format"); 247 248 fseek(file,0,SEEK_END); 249 long len=ftell(file); 250 if(len < 0) { 251 throw std::runtime_error("Wrong file object"); 252 } 253 fseek(file,0,SEEK_SET); 254 vdata_.resize(len+1,0); // +1 to make sure the vector is not empty 255 if(fread(&vdata_.front(),1,len,file)!=unsigned(len)) 256 throw std::runtime_error("Failed to read file"); 257 data_ = &vdata_[0]; 258 file_size_ = len; 259 } 260 get(unsigned offset) const261 uint32_t get(unsigned offset) const 262 { 263 uint32_t tmp; 264 if(offset > file_size_ - 4) { 265 throw std::runtime_error("Bad mo-file format"); 266 } 267 memcpy(&tmp,data_ + offset,4); 268 convert(tmp); 269 return tmp; 270 } 271 convert(uint32_t & v) const272 void convert(uint32_t &v) const 273 { 274 if(native_byteorder_) 275 return; 276 v = ((v & 0xFF) << 24) 277 | ((v & 0xFF00) << 8) 278 | ((v & 0xFF0000) >> 8) 279 | ((v & 0xFF000000) >> 24); 280 } 281 282 uint32_t keys_offset_; 283 uint32_t translations_offset_; 284 uint32_t hash_size_; 285 uint32_t hash_offset_; 286 287 char const *data_; 288 size_t file_size_; 289 std::vector<char> vdata_; 290 bool native_byteorder_; 291 size_t size_; 292 }; 293 294 template<typename CharType> 295 struct mo_file_use_traits { 296 static const bool in_use = false; 297 typedef CharType char_type; 298 typedef std::pair<char_type const *,char_type const *> pair_type; useboost::locale::gnu_gettext::mo_file_use_traits299 static pair_type use(mo_file const &/*mo*/,char_type const * /*context*/,char_type const * /*key*/) 300 { 301 return pair_type((char_type const *)(0),(char_type const *)(0)); 302 } 303 }; 304 305 template<> 306 struct mo_file_use_traits<char> { 307 static const bool in_use = true; 308 typedef char char_type; 309 typedef std::pair<char_type const *,char_type const *> pair_type; useboost::locale::gnu_gettext::mo_file_use_traits310 static pair_type use(mo_file const &mo,char const *context,char const *key) 311 { 312 return mo.find(context,key); 313 } 314 }; 315 316 template<typename CharType> 317 class converter { 318 public: converter(std::string,std::string in_enc)319 converter(std::string /*out_enc*/,std::string in_enc) : 320 in_(in_enc) 321 { 322 } 323 operator ()(char const * begin,char const * end)324 std::basic_string<CharType> operator()(char const *begin,char const *end) 325 { 326 return conv::to_utf<CharType>(begin,end,in_,conv::stop); 327 } 328 329 private: 330 std::string in_; 331 }; 332 333 template<> 334 class converter<char> { 335 public: converter(std::string out_enc,std::string in_enc)336 converter(std::string out_enc,std::string in_enc) : 337 out_(out_enc), 338 in_(in_enc) 339 { 340 } 341 operator ()(char const * begin,char const * end)342 std::string operator()(char const *begin,char const *end) 343 { 344 return conv::between(begin,end,out_,in_,conv::stop); 345 } 346 347 private: 348 std::string out_,in_; 349 }; 350 351 template<typename CharType> 352 struct message_key { 353 typedef CharType char_type; 354 typedef std::basic_string<char_type> string_type; 355 356 message_keyboost::locale::gnu_gettext::message_key357 message_key(string_type const &c = string_type()) : 358 c_context_(0), 359 c_key_(0) 360 { 361 size_t pos = c.find(char_type(4)); 362 if(pos == string_type::npos) { 363 key_ = c; 364 } 365 else { 366 context_ = c.substr(0,pos); 367 key_ = c.substr(pos+1); 368 } 369 } message_keyboost::locale::gnu_gettext::message_key370 message_key(char_type const *c,char_type const *k) : 371 c_key_(k) 372 { 373 static const char_type empty = 0; 374 if(c!=0) 375 c_context_ = c; 376 else 377 c_context_ = ∅ 378 } operator <boost::locale::gnu_gettext::message_key379 bool operator < (message_key const &other) const 380 { 381 int cc = compare(context(),other.context()); 382 if(cc != 0) 383 return cc < 0; 384 return compare(key(),other.key()) < 0; 385 } operator ==boost::locale::gnu_gettext::message_key386 bool operator==(message_key const &other) const 387 { 388 return compare(context(),other.context()) == 0 389 && compare(key(),other.key())==0; 390 } operator !=boost::locale::gnu_gettext::message_key391 bool operator!=(message_key const &other) const 392 { 393 return !(*this==other); 394 } contextboost::locale::gnu_gettext::message_key395 char_type const *context() const 396 { 397 if(c_context_) 398 return c_context_; 399 return context_.c_str(); 400 } keyboost::locale::gnu_gettext::message_key401 char_type const *key() const 402 { 403 if(c_key_) 404 return c_key_; 405 return key_.c_str(); 406 } 407 private: compareboost::locale::gnu_gettext::message_key408 static int compare(char_type const *l,char_type const *r) 409 { 410 typedef std::char_traits<char_type> traits_type; 411 for(;;) { 412 char_type cl = *l++; 413 char_type cr = *r++; 414 if(cl == 0 && cr == 0) 415 return 0; 416 if(traits_type::lt(cl,cr)) 417 return -1; 418 if(traits_type::lt(cr,cl)) 419 return 1; 420 } 421 } 422 string_type context_; 423 string_type key_; 424 char_type const *c_context_; 425 char_type const *c_key_; 426 }; 427 428 template<typename CharType> 429 struct hash_function { operator ()boost::locale::gnu_gettext::hash_function430 size_t operator()(message_key<CharType> const &msg) const 431 { 432 pj_winberger_hash::state_type state = pj_winberger_hash::initial_state; 433 CharType const *p = msg.context(); 434 if(*p != 0) { 435 CharType const *e = p; 436 while(*e) 437 e++; 438 state = pj_winberger_hash::update_state(state, 439 static_cast<char const *>(p), 440 static_cast<char const *>(e)); 441 state = pj_winberger_hash::update_state(state,'\4'); 442 } 443 p = msg.key(); 444 CharType const *e = p; 445 while(*e) 446 e++; 447 state = pj_winberger_hash::update_state(state, 448 static_cast<char const *>(p), 449 static_cast<char const *>(e)); 450 return state; 451 } 452 }; 453 454 455 // By default for wide types the conversion is not requiredyy 456 template<typename CharType> runtime_conversion(CharType const * msg,std::basic_string<CharType> &,bool,std::string const &,std::string const &)457 CharType const *runtime_conversion(CharType const *msg, 458 std::basic_string<CharType> &/*buffer*/, 459 bool /*do_conversion*/, 460 std::string const &/*locale_encoding*/, 461 std::string const &/*key_encoding*/) 462 { 463 return msg; 464 } 465 466 // But still need to specialize for char 467 template<> runtime_conversion(char const * msg,std::string & buffer,bool do_conversion,std::string const & locale_encoding,std::string const & key_encoding)468 char const *runtime_conversion( char const *msg, 469 std::string &buffer, 470 bool do_conversion, 471 std::string const &locale_encoding, 472 std::string const &key_encoding) 473 { 474 if(!do_conversion) 475 return msg; 476 if(details::is_us_ascii_string(msg)) 477 return msg; 478 std::string tmp = conv::between(msg,locale_encoding,key_encoding,conv::skip); 479 buffer.swap(tmp); 480 return buffer.c_str(); 481 } 482 483 template<typename CharType> 484 class mo_message : public message_format<CharType> { 485 486 typedef CharType char_type; 487 typedef std::basic_string<CharType> string_type; 488 typedef message_key<CharType> key_type; 489 #ifdef BOOST_LOCALE_UNORDERED_CATALOG 490 typedef boost::unordered_map<key_type,string_type,hash_function<CharType> > catalog_type; 491 #else 492 typedef std::map<key_type,string_type> catalog_type; 493 #endif 494 typedef std::vector<catalog_type> catalogs_set_type; 495 typedef std::map<std::string,int> domains_map_type; 496 public: 497 498 typedef std::pair<CharType const *,CharType const *> pair_type; 499 get(int domain_id,char_type const * context,char_type const * id) const500 virtual char_type const *get(int domain_id,char_type const *context,char_type const *id) const 501 { 502 return get_string(domain_id,context,id).first; 503 } 504 get(int domain_id,char_type const * context,char_type const * single_id,int n) const505 virtual char_type const *get(int domain_id,char_type const *context,char_type const *single_id,int n) const 506 { 507 pair_type ptr = get_string(domain_id,context,single_id); 508 if(!ptr.first) 509 return 0; 510 int form=0; 511 if(plural_forms_.at(domain_id)) 512 form = (*plural_forms_[domain_id])(n); 513 else 514 form = n == 1 ? 0 : 1; // Fallback to english plural form 515 516 CharType const *p=ptr.first; 517 for(int i=0;p < ptr.second && i<form;i++) { 518 p=std::find(p,ptr.second,0); 519 if(p==ptr.second) 520 return 0; 521 ++p; 522 } 523 if(p>=ptr.second) 524 return 0; 525 return p; 526 } 527 domain(std::string const & domain) const528 virtual int domain(std::string const &domain) const 529 { 530 domains_map_type::const_iterator p=domains_.find(domain); 531 if(p==domains_.end()) 532 return -1; 533 return p->second; 534 } 535 mo_message(messages_info const & inf)536 mo_message(messages_info const &inf) 537 { 538 std::string language = inf.language; 539 std::string variant = inf.variant; 540 std::string country = inf.country; 541 std::string encoding = inf.encoding; 542 std::string lc_cat = inf.locale_category; 543 std::vector<messages_info::domain> const &domains = inf.domains; 544 std::vector<std::string> const &search_paths = inf.paths; 545 546 // 547 // List of fallbacks: en_US@euro, en@euro, en_US, en. 548 // 549 std::vector<std::string> paths; 550 551 552 if(!variant.empty() && !country.empty()) 553 paths.push_back(language + "_" + country + "@" + variant); 554 555 if(!variant.empty()) 556 paths.push_back(language + "@" + variant); 557 558 if(!country.empty()) 559 paths.push_back(language + "_" + country); 560 561 paths.push_back(language); 562 563 catalogs_.resize(domains.size()); 564 mo_catalogs_.resize(domains.size()); 565 plural_forms_.resize(domains.size()); 566 567 568 for(unsigned id=0;id<domains.size();id++) { 569 std::string domain=domains[id].name; 570 std::string key_encoding = domains[id].encoding; 571 domains_[domain]=id; 572 573 574 bool found=false; 575 for(unsigned j=0;!found && j<paths.size();j++) { 576 for(unsigned i=0;!found && i<search_paths.size();i++) { 577 std::string full_path = search_paths[i]+"/"+paths[j]+"/" + lc_cat + "/"+domain+".mo"; 578 found = load_file(full_path,encoding,key_encoding,id,inf.callback); 579 } 580 } 581 } 582 } 583 convert(char_type const * msg,string_type & buffer) const584 char_type const *convert(char_type const *msg,string_type &buffer) const 585 { 586 return runtime_conversion<char_type>(msg,buffer,key_conversion_required_,locale_encoding_,key_encoding_); 587 } 588 ~mo_message()589 virtual ~mo_message() 590 { 591 } 592 593 private: compare_encodings(std::string const & left,std::string const & right)594 int compare_encodings(std::string const &left,std::string const &right) 595 { 596 return convert_encoding_name(left).compare(convert_encoding_name(right)); 597 } 598 convert_encoding_name(std::string const & in)599 std::string convert_encoding_name(std::string const &in) 600 { 601 std::string result; 602 for(unsigned i=0;i<in.size();i++) { 603 char c=in[i]; 604 if('A' <= c && c<='Z') 605 c=c-'A' + 'a'; 606 else if(('a' <= c && c<='z') || ('0' <= c && c<='9')) 607 ; 608 else 609 continue; 610 result+=c; 611 } 612 return result; 613 } 614 615 load_file(std::string const & file_name,std::string const & locale_encoding,std::string const & key_encoding,int id,messages_info::callback_type const & callback)616 bool load_file( std::string const &file_name, 617 std::string const &locale_encoding, 618 std::string const &key_encoding, 619 int id, 620 messages_info::callback_type const &callback) 621 { 622 locale_encoding_ = locale_encoding; 623 key_encoding_ = key_encoding; 624 625 key_conversion_required_ = sizeof(CharType) == 1 626 && compare_encodings(locale_encoding,key_encoding)!=0; 627 628 std::auto_ptr<mo_file> mo; 629 630 if(callback) { 631 std::vector<char> vfile = callback(file_name,locale_encoding); 632 if(vfile.empty()) 633 return false; 634 mo.reset(new mo_file(vfile)); 635 } 636 else { 637 c_file the_file; 638 the_file.open(file_name,locale_encoding); 639 if(!the_file.file) 640 return false; 641 mo.reset(new mo_file(the_file.file)); 642 } 643 644 std::string plural = extract(mo->value(0).first,"plural=","\r\n;"); 645 646 std::string mo_encoding = extract(mo->value(0).first,"charset="," \r\n;"); 647 648 if(mo_encoding.empty()) 649 throw std::runtime_error("Invalid mo-format, encoding is not specified"); 650 651 if(!plural.empty()) { 652 std::auto_ptr<lambda::plural> ptr=lambda::compile(plural.c_str()); 653 plural_forms_[id] = ptr; 654 } 655 656 if( mo_useable_directly(mo_encoding,*mo) ) 657 { 658 mo_catalogs_[id]=mo; 659 } 660 else { 661 converter<CharType> cvt_value(locale_encoding,mo_encoding); 662 converter<CharType> cvt_key(key_encoding,mo_encoding); 663 for(unsigned i=0;i<mo->size();i++) { 664 char const *ckey = mo->key(i); 665 string_type skey = cvt_key(ckey,ckey+strlen(ckey)); 666 key_type key(skey); 667 668 mo_file::pair_type tmp = mo->value(i); 669 string_type value = cvt_value(tmp.first,tmp.second); 670 catalogs_[id][key].swap(value); 671 } 672 } 673 return true; 674 675 } 676 677 // Check if the mo file as-is is useful 678 // 1. It is char and not wide character 679 // 2. The locale encoding and mo encoding is same 680 // 3. The source strings encoding and mo encoding is same or all 681 // mo key strings are US-ASCII mo_useable_directly(std::string const & mo_encoding,mo_file const & mo)682 bool mo_useable_directly( std::string const &mo_encoding, 683 mo_file const &mo) 684 { 685 if(sizeof(CharType) != 1) 686 return false; 687 if(!mo.has_hash()) 688 return false; 689 if(compare_encodings(mo_encoding.c_str(),locale_encoding_.c_str())!=0) 690 return false; 691 if(compare_encodings(mo_encoding.c_str(),key_encoding_.c_str())==0) { 692 return true; 693 } 694 for(unsigned i=0;i<mo.size();i++) { 695 if(!details::is_us_ascii_string(mo.key(i))) { 696 return false; 697 } 698 } 699 return true; 700 } 701 702 703 extract(std::string const & meta,std::string const & key,char const * separator)704 static std::string extract(std::string const &meta,std::string const &key,char const *separator) 705 { 706 size_t pos=meta.find(key); 707 if(pos == std::string::npos) 708 return ""; 709 pos+=key.size(); /// size of charset= 710 size_t end_pos = meta.find_first_of(separator,pos); 711 return meta.substr(pos,end_pos - pos); 712 } 713 714 715 716 get_string(int domain_id,char_type const * context,char_type const * in_id) const717 pair_type get_string(int domain_id,char_type const *context,char_type const *in_id) const 718 { 719 pair_type null_pair((CharType const *)0,(CharType const *)0); 720 if(domain_id < 0 || size_t(domain_id) >= catalogs_.size()) 721 return null_pair; 722 if(mo_file_use_traits<char_type>::in_use && mo_catalogs_[domain_id]) { 723 return mo_file_use_traits<char_type>::use(*mo_catalogs_[domain_id],context,in_id); 724 } 725 else { 726 key_type key(context,in_id); 727 catalog_type const &cat = catalogs_[domain_id]; 728 typename catalog_type::const_iterator p = cat.find(key); 729 if(p==cat.end()) { 730 return null_pair; 731 } 732 return pair_type(p->second.data(),p->second.data()+p->second.size()); 733 } 734 } 735 736 catalogs_set_type catalogs_; 737 std::vector<boost::shared_ptr<mo_file> > mo_catalogs_; 738 std::vector<boost::shared_ptr<lambda::plural> > plural_forms_; 739 domains_map_type domains_; 740 741 std::string locale_encoding_; 742 std::string key_encoding_; 743 bool key_conversion_required_; 744 }; 745 746 template<> create_messages_facet(messages_info const & info)747 message_format<char> *create_messages_facet(messages_info const &info) 748 { 749 return new mo_message<char>(info); 750 } 751 752 template<> create_messages_facet(messages_info const & info)753 message_format<wchar_t> *create_messages_facet(messages_info const &info) 754 { 755 return new mo_message<wchar_t>(info); 756 } 757 758 #ifdef BOOST_HAS_CHAR16_T 759 760 template<> create_messages_facet(messages_info const & info)761 message_format<char16_t> *create_messages_facet(messages_info const &info) 762 { 763 return new mo_message<char16_t>(info); 764 } 765 #endif 766 767 #ifdef BOOST_HAS_CHAR32_T 768 769 template<> create_messages_facet(messages_info const & info)770 message_format<char32_t> *create_messages_facet(messages_info const &info) 771 { 772 return new mo_message<char32_t>(info); 773 } 774 #endif 775 776 777 } /// gnu_gettext 778 779 } // locale 780 } // boost 781 // vim: tabstop=4 expandtab shiftwidth=4 softtabstop=4 782 783