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