ICU4CGlue.h (22091B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 #ifndef intl_components_ICUUtils_h 6 #define intl_components_ICUUtils_h 7 8 #include "unicode/uenum.h" 9 #include "unicode/utypes.h" 10 #include "mozilla/Buffer.h" 11 #include "mozilla/CheckedInt.h" 12 #include "mozilla/DebugOnly.h" 13 #include "mozilla/Likely.h" 14 #include "mozilla/Maybe.h" 15 #include "mozilla/Result.h" 16 #include "mozilla/Span.h" 17 #include "mozilla/Utf8.h" 18 #include "mozilla/Vector.h" 19 #include "mozilla/intl/ICUError.h" 20 21 // When building standalone js shell, it will include headers from 22 // intl/components if JS_HAS_INTL_API is true (the default value), but js shell 23 // won't include headers from XPCOM, so don't include nsTArray.h when building 24 // standalone js shell. 25 #ifndef JS_STANDALONE 26 # include "nsTArray.h" 27 #endif 28 29 #include <cstring> 30 #include <iterator> 31 #include <stddef.h> 32 #include <stdint.h> 33 #include <string> 34 #include <string_view> 35 36 struct UFormattedValue; 37 namespace mozilla::intl { 38 39 template <typename CharType> 40 static inline CharType* AssertNullTerminatedString(Span<CharType> aSpan) { 41 // Intentionally check one past the last character, because we expect that the 42 // NUL character isn't part of the string. 43 MOZ_ASSERT(*(aSpan.data() + aSpan.size()) == '\0'); 44 45 // Also ensure there aren't any other NUL characters within the string. 46 MOZ_ASSERT(std::char_traits<std::remove_const_t<CharType>>::length( 47 aSpan.data()) == aSpan.size()); 48 49 return aSpan.data(); 50 } 51 52 static inline const char* AssertNullTerminatedString(std::string_view aView) { 53 // Intentionally check one past the last character, because we expect that the 54 // NUL character isn't part of the string. 55 MOZ_ASSERT(*(aView.data() + aView.size()) == '\0'); 56 57 // Also ensure there aren't any other NUL characters within the string. 58 MOZ_ASSERT(std::strlen(aView.data()) == aView.size()); 59 60 return aView.data(); 61 } 62 63 /** 64 * Map the "und" locale to an empty string, which ICU uses internally. 65 */ 66 static inline const char* IcuLocale(const char* aLocale) { 67 // Return the empty string if the input is exactly equal to the string "und". 68 const char* locale = aLocale; 69 if (!std::strcmp(locale, "und")) { 70 locale = ""; // ICU root locale 71 } 72 return locale; 73 } 74 75 /** 76 * Ensure a locale is null-terminated, and map the "und" locale to an empty 77 * string, which ICU uses internally. 78 */ 79 static inline const char* IcuLocale(Span<const char> aLocale) { 80 return IcuLocale(AssertNullTerminatedString(aLocale)); 81 } 82 83 /** 84 * Ensure a locale in the buffer is null-terminated, and map the "und" locale to 85 * an empty string, which ICU uses internally. 86 */ 87 static inline const char* IcuLocale(const Buffer<char>& aLocale) { 88 return IcuLocale(Span(aLocale.begin(), aLocale.Length() - 1)); 89 } 90 91 using ICUResult = Result<Ok, ICUError>; 92 93 /** 94 * Convert a UErrorCode to ICUError. This will correctly apply the OutOfMemory 95 * case. 96 */ 97 ICUError ToICUError(UErrorCode status); 98 99 /** 100 * Convert a UErrorCode to ICUResult. This will correctly apply the OutOfMemory 101 * case. 102 */ 103 ICUResult ToICUResult(UErrorCode status); 104 105 /** 106 * The ICU status can complain about a string not being terminated, but this 107 * is fine for this API, as it deals with the mozilla::Span that has a pointer 108 * and a length. 109 */ 110 static inline bool ICUSuccessForStringSpan(UErrorCode status) { 111 return U_SUCCESS(status) || status == U_STRING_NOT_TERMINATED_WARNING; 112 } 113 114 /** 115 * This class enforces that the unified mozilla::intl methods match the 116 * const-ness of the underlying ICU4C API calls. const ICU4C APIs take a const 117 * pointer, while mutable ones take a non-const pointer. 118 * 119 * For const ICU4C calls use: 120 * ICUPointer::GetConst(). 121 * 122 * For non-const ICU4C calls use: 123 * ICUPointer::GetMut(). 124 * 125 * This will propagate the `const` specifier from the ICU4C API call to the 126 * unified method, and it will be enforced by the compiler. This helps ensures 127 * a consistence and correct implementation. 128 */ 129 template <typename T> 130 class ICUPointer { 131 public: 132 explicit ICUPointer(T* aPointer) : mPointer(aPointer) {} 133 134 // Only allow moves of ICUPointers, no copies. 135 ICUPointer(ICUPointer&& other) noexcept = default; 136 ICUPointer& operator=(ICUPointer&& other) noexcept = default; 137 138 // Implicitly take ownership of a raw pointer through copy assignment. 139 ICUPointer& operator=(T* aPointer) noexcept { 140 mPointer = aPointer; 141 return *this; 142 }; 143 144 const T* GetConst() const { return const_cast<const T*>(mPointer); } 145 T* GetMut() { return mPointer; } 146 147 explicit operator bool() const { return !!mPointer; } 148 149 private: 150 T* mPointer; 151 }; 152 153 /** 154 * Calling into ICU with the C-API can be a bit tricky. This function wraps up 155 * the relatively risky operations involving pointers, lengths, and buffers into 156 * a simpler call. This function accepts a lambda that performs the ICU call, 157 * and returns the length of characters in the buffer. When using a temporary 158 * stack-based buffer, the calls can often be done in one trip. However, if 159 * additional memory is needed, this function will call the C-API twice, in 160 * order to first get the size of the result, and then second to copy the result 161 * over to the buffer. 162 */ 163 template <typename ICUStringFunction, typename Buffer> 164 static ICUResult FillBufferWithICUCall(Buffer& buffer, 165 const ICUStringFunction& strFn) { 166 static_assert(std::is_same_v<typename Buffer::CharType, char16_t> || 167 std::is_same_v<typename Buffer::CharType, char> || 168 std::is_same_v<typename Buffer::CharType, uint8_t>); 169 170 UErrorCode status = U_ZERO_ERROR; 171 int32_t length = strFn(buffer.data(), buffer.capacity(), &status); 172 if (status == U_BUFFER_OVERFLOW_ERROR) { 173 MOZ_ASSERT(length >= 0); 174 175 if (!buffer.reserve(length)) { 176 return Err(ICUError::OutOfMemory); 177 } 178 179 status = U_ZERO_ERROR; 180 mozilla::DebugOnly<int32_t> length2 = strFn(buffer.data(), length, &status); 181 MOZ_ASSERT(length == length2); 182 } 183 if (!ICUSuccessForStringSpan(status)) { 184 return Err(ToICUError(status)); 185 } 186 187 buffer.written(length); 188 189 return Ok{}; 190 } 191 192 /** 193 * Adaptor for mozilla::Vector to implement the Buffer interface. 194 */ 195 template <typename T, size_t N> 196 class VectorToBufferAdaptor { 197 mozilla::Vector<T, N>& vector; 198 199 public: 200 using CharType = T; 201 202 explicit VectorToBufferAdaptor(mozilla::Vector<T, N>& vector) 203 : vector(vector) {} 204 205 T* data() { return vector.begin(); } 206 207 size_t capacity() const { return vector.capacity(); } 208 209 bool reserve(size_t length) { return vector.reserve(length); } 210 211 void written(size_t length) { 212 mozilla::DebugOnly<bool> result = vector.resizeUninitialized(length); 213 MOZ_ASSERT(result); 214 } 215 }; 216 217 /** 218 * An overload of FillBufferWithICUCall that accepts a mozilla::Vector rather 219 * than a Buffer. 220 */ 221 template <typename ICUStringFunction, size_t InlineSize, typename CharType> 222 static ICUResult FillBufferWithICUCall(Vector<CharType, InlineSize>& vector, 223 const ICUStringFunction& strFn) { 224 VectorToBufferAdaptor buffer(vector); 225 return FillBufferWithICUCall(buffer, strFn); 226 } 227 228 #ifndef JS_STANDALONE 229 /** 230 * mozilla::intl APIs require sizeable buffers. This class abstracts over 231 * the nsTArray. 232 */ 233 template <typename T> 234 class nsTArrayToBufferAdapter { 235 public: 236 using CharType = T; 237 238 // Do not allow copy or move. Move could be added in the future if needed. 239 nsTArrayToBufferAdapter(const nsTArrayToBufferAdapter&) = delete; 240 nsTArrayToBufferAdapter& operator=(const nsTArrayToBufferAdapter&) = delete; 241 242 explicit nsTArrayToBufferAdapter(nsTArray<CharType>& aArray) 243 : mArray(aArray) {} 244 245 /** 246 * Ensures the buffer has enough space to accommodate |size| elements. 247 */ 248 [[nodiscard]] bool reserve(size_t size) { 249 // Use fallible behavior here. 250 return mArray.SetCapacity(size, fallible); 251 } 252 253 /** 254 * Returns the raw data inside the buffer. 255 */ 256 CharType* data() { return mArray.Elements(); } 257 258 /** 259 * Returns the count of elements written into the buffer. 260 */ 261 size_t length() const { return mArray.Length(); } 262 263 /** 264 * Returns the buffer's overall capacity. 265 */ 266 size_t capacity() const { return mArray.Capacity(); } 267 268 /** 269 * Resizes the buffer to the given amount of written elements. 270 */ 271 void written(size_t amount) { 272 MOZ_ASSERT(amount <= mArray.Capacity()); 273 // This sets |mArray|'s internal size so that it matches how much was 274 // written. This is necessary because the write happens across FFI 275 // boundaries. 276 mArray.SetLengthAndRetainStorage(amount); 277 } 278 279 private: 280 nsTArray<CharType>& mArray; 281 }; 282 283 template <typename T, size_t N> 284 class AutoTArrayToBufferAdapter : public nsTArrayToBufferAdapter<T> { 285 using nsTArrayToBufferAdapter<T>::nsTArrayToBufferAdapter; 286 }; 287 288 /** 289 * An overload of FillBufferWithICUCall that accepts a nsTArray. 290 */ 291 template <typename ICUStringFunction, typename CharType> 292 static ICUResult FillBufferWithICUCall(nsTArray<CharType>& array, 293 const ICUStringFunction& strFn) { 294 nsTArrayToBufferAdapter<CharType> buffer(array); 295 return FillBufferWithICUCall(buffer, strFn); 296 } 297 298 template <typename ICUStringFunction, typename CharType, size_t N> 299 static ICUResult FillBufferWithICUCall(AutoTArray<CharType, N>& array, 300 const ICUStringFunction& strFn) { 301 AutoTArrayToBufferAdapter<CharType, N> buffer(array); 302 return FillBufferWithICUCall(buffer, strFn); 303 } 304 #endif 305 306 /** 307 * Fill a UTF-8 or a UTF-16 buffer with a UTF-16 span. ICU4C mostly uses UTF-16 308 * internally, but different consumers may have different situations with their 309 * buffers. 310 */ 311 template <typename Buffer> 312 [[nodiscard]] bool FillBuffer(Span<const char16_t> utf16Span, 313 Buffer& targetBuffer) { 314 static_assert(std::is_same_v<typename Buffer::CharType, char> || 315 std::is_same_v<typename Buffer::CharType, unsigned char> || 316 std::is_same_v<typename Buffer::CharType, char16_t>); 317 318 if constexpr (std::is_same_v<typename Buffer::CharType, char> || 319 std::is_same_v<typename Buffer::CharType, unsigned char>) { 320 auto targetSize = CheckedInt<size_t>(utf16Span.Length()) * 3; 321 if (MOZ_UNLIKELY(!targetSize.isValid() || 322 !targetBuffer.reserve(targetSize.value()))) { 323 return false; 324 } 325 326 size_t amount = ConvertUtf16toUtf8( 327 utf16Span, Span(reinterpret_cast<char*>(targetBuffer.data()), 328 targetBuffer.capacity())); 329 330 targetBuffer.written(amount); 331 } 332 if constexpr (std::is_same_v<typename Buffer::CharType, char16_t>) { 333 size_t amount = utf16Span.Length(); 334 if (!targetBuffer.reserve(amount)) { 335 return false; 336 } 337 for (size_t i = 0; i < amount; i++) { 338 targetBuffer.data()[i] = utf16Span[i]; 339 } 340 targetBuffer.written(amount); 341 } 342 343 return true; 344 } 345 346 /** 347 * Fill a UTF-8 or a UTF-16 buffer with a UTF-8 span. ICU4C mostly uses UTF-16 348 * internally, but different consumers may have different situations with their 349 * buffers. 350 */ 351 template <typename Buffer> 352 [[nodiscard]] bool FillBuffer(Span<const char> utf8Span, Buffer& targetBuffer) { 353 static_assert(std::is_same_v<typename Buffer::CharType, char> || 354 std::is_same_v<typename Buffer::CharType, unsigned char> || 355 std::is_same_v<typename Buffer::CharType, char16_t>); 356 357 if constexpr (std::is_same_v<typename Buffer::CharType, char> || 358 std::is_same_v<typename Buffer::CharType, unsigned char>) { 359 size_t amount = utf8Span.Length(); 360 if (!targetBuffer.reserve(amount)) { 361 return false; 362 } 363 for (size_t i = 0; i < amount; i++) { 364 targetBuffer.data()[i] = 365 // Static cast in case of a mismatch between `unsigned char` and 366 // `char` 367 static_cast<typename Buffer::CharType>(utf8Span[i]); 368 } 369 targetBuffer.written(amount); 370 } 371 if constexpr (std::is_same_v<typename Buffer::CharType, char16_t>) { 372 if (!targetBuffer.reserve(utf8Span.Length() + 1)) { 373 return false; 374 } 375 376 size_t amount = ConvertUtf8toUtf16( 377 utf8Span, Span(targetBuffer.data(), targetBuffer.capacity())); 378 379 targetBuffer.written(amount); 380 } 381 382 return true; 383 } 384 385 /** 386 * It is convenient for callers to be able to pass in UTF-8 strings to the API. 387 * This function can be used to convert that to a stack-allocated UTF-16 388 * mozilla::Vector that can then be passed into ICU calls. The string will be 389 * null terminated. 390 */ 391 template <size_t StackSize> 392 [[nodiscard]] static bool FillUTF16Vector( 393 Span<const char> utf8Span, 394 mozilla::Vector<char16_t, StackSize>& utf16TargetVec) { 395 // Per ConvertUtf8toUtf16: The length of aDest must be at least one greater 396 // than the length of aSource. This additional length will be used for null 397 // termination. 398 if (!utf16TargetVec.reserve(utf8Span.Length() + 1)) { 399 return false; 400 } 401 402 // ConvertUtf8toUtf16 fills the buffer with the data, but the length of the 403 // vector is unchanged. 404 size_t length = ConvertUtf8toUtf16( 405 utf8Span, Span(utf16TargetVec.begin(), utf16TargetVec.capacity())); 406 407 // Assert that the last element is free for writing a null terminator. 408 MOZ_ASSERT(length < utf16TargetVec.capacity()); 409 utf16TargetVec.begin()[length] = '\0'; 410 411 // The call to resizeUninitialized notifies the vector of how much was written 412 // exclusive of the null terminated character. 413 return utf16TargetVec.resizeUninitialized(length); 414 } 415 416 /** 417 * An iterable class that wraps calls to the ICU UEnumeration C API. 418 * 419 * Usage: 420 * 421 * // Make sure the range expression is non-temporary, otherwise there is a 422 * // risk of undefined behavior: 423 * auto result = Calendar::GetBcp47KeywordValuesForLocale("en-US"); 424 * 425 * for (auto name : result.unwrap()) { 426 * MOZ_ASSERT(name.unwrap(), "An iterable value exists".); 427 * } 428 */ 429 template <typename CharType, typename T, T(Mapper)(const CharType*, int32_t)> 430 class Enumeration { 431 public: 432 class Iterator; 433 friend class Iterator; 434 435 // Transfer ownership of the UEnumeration in the move constructor. 436 Enumeration(Enumeration&& other) noexcept 437 : mUEnumeration(other.mUEnumeration) { 438 other.mUEnumeration = nullptr; 439 } 440 441 // Transfer ownership of the UEnumeration in the move assignment operator. 442 Enumeration& operator=(Enumeration&& other) noexcept { 443 if (this == &other) { 444 return *this; 445 } 446 if (mUEnumeration) { 447 uenum_close(mUEnumeration); 448 } 449 mUEnumeration = other.mUEnumeration; 450 other.mUEnumeration = nullptr; 451 return *this; 452 } 453 454 class Iterator { 455 Enumeration& mEnumeration; 456 // `Nothing` signifies that no enumeration has been loaded through ICU yet. 457 Maybe<int32_t> mIteration = Nothing{}; 458 const CharType* mNext = nullptr; 459 int32_t mNextLength = 0; 460 461 public: 462 using value_type = const CharType*; 463 using reference = T; 464 using iterator_category = std::input_iterator_tag; 465 466 explicit Iterator(Enumeration& aEnumeration, bool aIsBegin) 467 : mEnumeration(aEnumeration) { 468 if (aIsBegin) { 469 AdvanceUEnum(); 470 } 471 } 472 473 Iterator& operator++() { 474 AdvanceUEnum(); 475 return *this; 476 } 477 478 Iterator operator++(int) { 479 Iterator retval = *this; 480 ++(*this); 481 return retval; 482 } 483 484 bool operator==(Iterator other) const { 485 return mIteration == other.mIteration; 486 } 487 488 bool operator!=(Iterator other) const { return !(*this == other); } 489 490 T operator*() const { 491 // Map the iterated value to something new. 492 return Mapper(mNext, mNextLength); 493 } 494 495 private: 496 void AdvanceUEnum() { 497 if (mIteration.isNothing()) { 498 mIteration = Some(-1); 499 } 500 UErrorCode status = U_ZERO_ERROR; 501 if constexpr (std::is_same_v<CharType, char16_t>) { 502 mNext = uenum_unext(mEnumeration.mUEnumeration, &mNextLength, &status); 503 } else { 504 static_assert(std::is_same_v<CharType, char>, 505 "Only char16_t and char are supported by " 506 "mozilla::intl::Enumeration."); 507 mNext = uenum_next(mEnumeration.mUEnumeration, &mNextLength, &status); 508 } 509 if (U_FAILURE(status)) { 510 mNext = nullptr; 511 } 512 513 if (mNext) { 514 (*mIteration)++; 515 } else { 516 // The iterator is complete. 517 mIteration = Nothing{}; 518 } 519 } 520 }; 521 522 Iterator begin() { return Iterator(*this, true); } 523 Iterator end() { return Iterator(*this, false); } 524 525 explicit Enumeration(UEnumeration* aUEnumeration) 526 : mUEnumeration(aUEnumeration) {} 527 528 ~Enumeration() { 529 if (mUEnumeration) { 530 // Only close when the object is being destructed, not moved. 531 uenum_close(mUEnumeration); 532 } 533 } 534 535 private: 536 UEnumeration* mUEnumeration = nullptr; 537 }; 538 539 template <typename CharType> 540 Result<Span<const CharType>, InternalError> SpanMapper(const CharType* string, 541 int32_t length) { 542 // Return the raw value from this Iterator. 543 if (string == nullptr) { 544 return Err(InternalError{}); 545 } 546 MOZ_ASSERT(length >= 0); 547 return Span<const CharType>(string, static_cast<size_t>(length)); 548 } 549 550 template <typename CharType> 551 using SpanResult = Result<Span<const CharType>, InternalError>; 552 553 template <typename CharType> 554 using SpanEnumeration = Enumeration<CharType, SpanResult<CharType>, SpanMapper>; 555 556 /** 557 * An iterable class that wraps calls to ICU's available locales API. 558 */ 559 template <int32_t(CountAvailable)(), const char*(GetAvailable)(int32_t)> 560 class AvailableLocalesEnumeration final { 561 // The overall count of available locales. 562 int32_t mLocalesCount = 0; 563 564 public: 565 AvailableLocalesEnumeration() { mLocalesCount = CountAvailable(); } 566 567 class Iterator { 568 public: 569 // std::iterator traits. 570 using iterator_category = std::input_iterator_tag; 571 using value_type = const char*; 572 using difference_type = ptrdiff_t; 573 using pointer = value_type*; 574 using reference = value_type&; 575 576 private: 577 // The current position in the list of available locales. 578 int32_t mLocalesPos = 0; 579 580 public: 581 explicit Iterator(int32_t aLocalesPos) : mLocalesPos(aLocalesPos) {} 582 583 Iterator& operator++() { 584 mLocalesPos++; 585 return *this; 586 } 587 588 Iterator operator++(int) { 589 Iterator result = *this; 590 ++(*this); 591 return result; 592 } 593 594 bool operator==(const Iterator& aOther) const { 595 return mLocalesPos == aOther.mLocalesPos; 596 } 597 598 bool operator!=(const Iterator& aOther) const { return !(*this == aOther); } 599 600 value_type operator*() const { return GetAvailable(mLocalesPos); } 601 }; 602 603 // std::iterator begin() and end() methods. 604 605 /** 606 * Return an iterator pointing to the first available locale. 607 */ 608 Iterator begin() const { return Iterator(0); } 609 610 /** 611 * Return an iterator pointing to one past the last available locale. 612 */ 613 Iterator end() const { return Iterator(mLocalesCount); } 614 }; 615 616 /** 617 * A helper class to wrap calling ICU function in cpp file so we don't have to 618 * include the ICU header here. 619 */ 620 class FormattedResult { 621 protected: 622 static Result<Span<const char16_t>, ICUError> ToSpanImpl( 623 const UFormattedValue* value); 624 }; 625 626 /** 627 * A RAII class to hold the formatted value of format result. 628 * 629 * The caller will need to create this AutoFormattedResult on the stack, with 630 * the following parameters: 631 * 1. Native ICU type. 632 * 2. An ICU function which opens the result. 633 * 3. An ICU function which can get the result as UFormattedValue. 634 * 4. An ICU function which closes the result. 635 * 636 * After the object is created, caller needs to call IsValid() method to check 637 * if the native object has been created properly, and then passes this 638 * object to other format interfaces. 639 * The format result will be stored in this object, the caller can use ToSpan() 640 * method to get the formatted string. 641 * 642 * The methods GetFormatted() and Value() are private methods since they expose 643 * native ICU types. If the caller wants to call these methods, the caller needs 644 * to register itself as a friend class in AutoFormattedResult. 645 * 646 * The formatted value and the native ICU object will be released once this 647 * class is destructed. 648 */ 649 template <typename T, T*(Open)(UErrorCode*), 650 const UFormattedValue*(GetValue)(const T*, UErrorCode*), 651 void(Close)(T*)> 652 class MOZ_RAII AutoFormattedResult : FormattedResult { 653 public: 654 AutoFormattedResult() { 655 mFormatted = Open(&mError); 656 if (U_FAILURE(mError)) { 657 mFormatted = nullptr; 658 } 659 } 660 ~AutoFormattedResult() { 661 if (mFormatted) { 662 Close(mFormatted); 663 } 664 } 665 666 AutoFormattedResult(const AutoFormattedResult& other) = delete; 667 AutoFormattedResult& operator=(const AutoFormattedResult& other) = delete; 668 669 AutoFormattedResult(AutoFormattedResult&& other) = delete; 670 AutoFormattedResult& operator=(AutoFormattedResult&& other) = delete; 671 672 /** 673 * Check if the native UFormattedDateInterval was created successfully. 674 */ 675 bool IsValid() const { return !!mFormatted; } 676 677 /** 678 * Get error code if IsValid() returns false. 679 */ 680 ICUError GetError() const { return ToICUError(mError); } 681 682 /** 683 * Get the formatted result. 684 */ 685 Result<Span<const char16_t>, ICUError> ToSpan() const { 686 if (!IsValid()) { 687 return Err(GetError()); 688 } 689 690 const UFormattedValue* value = Value(); 691 if (!value) { 692 return Err(ICUError::InternalError); 693 } 694 695 return ToSpanImpl(value); 696 } 697 698 private: 699 friend class DateIntervalFormat; 700 friend class ListFormat; 701 T* GetFormatted() const { return mFormatted; } 702 703 const UFormattedValue* Value() const { 704 if (!IsValid()) { 705 return nullptr; 706 } 707 708 UErrorCode status = U_ZERO_ERROR; 709 const UFormattedValue* value = GetValue(mFormatted, &status); 710 if (U_FAILURE(status)) { 711 return nullptr; 712 } 713 714 return value; 715 }; 716 717 T* mFormatted = nullptr; 718 UErrorCode mError = U_ZERO_ERROR; 719 }; 720 } // namespace mozilla::intl 721 722 #endif /* intl_components_ICUUtils_h */