chromium/third_party/blink/renderer/platform/text/locale_mac.mm

/*
 * Copyright (C) 2012 Google Inc. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are
 * met:
 *
 *     * Redistributions of source code must retain the above copyright
 * notice, this list of conditions and the following disclaimer.
 *     * Redistributions in binary form must reproduce the above
 * copyright notice, this list of conditions and the following disclaimer
 * in the documentation and/or other materials provided with the
 * distribution.
 *     * Neither the name of Google Inc. nor the names of its
 * contributors may be used to endorse or promote products derived from
 * this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

#include "third_party/blink/renderer/platform/text/locale_mac.h"

#import <Foundation/Foundation.h>

#include <iterator>
#include <memory>

#include "base/memory/ptr_util.h"
#include "base/ranges/algorithm.h"
#include "third_party/blink/renderer/platform/language.h"
#include "third_party/blink/renderer/platform/runtime_enabled_features.h"
#include "third_party/blink/renderer/platform/web_test_support.h"
#include "third_party/blink/renderer/platform/wtf/date_math.h"
#include "third_party/blink/renderer/platform/wtf/text/string_builder.h"
#include "ui/base/ui_base_features.h"

namespace blink {

static inline String LanguageFromLocale(const String& locale) {
  String normalized_locale = locale;
  normalized_locale.Replace('-', '_');
  wtf_size_t separator_position = normalized_locale.find('_');
  if (separator_position == kNotFound)
    return normalized_locale;
  return normalized_locale.Left(separator_position);
}

static NSLocale* DetermineLocale(const String& locale) {
  if (!WebTestSupport::IsRunningWebTest()) {
    NSLocale* current_locale = NSLocale.currentLocale;
    String current_locale_language =
        LanguageFromLocale(String(current_locale.localeIdentifier));
    String locale_language = LanguageFromLocale(locale);
    if (DeprecatedEqualIgnoringCase(current_locale_language, locale_language))
      return current_locale;
  }
  // It seems localeWithLocaleIdentifier accepts dash-separated locale
  // identifier.
  return [NSLocale localeWithLocaleIdentifier:locale];
}

std::unique_ptr<Locale> Locale::Create(const String& locale) {
  return LocaleMac::Create(DetermineLocale(locale));
}

static NSDateFormatter* CreateDateTimeFormatter(
    NSLocale* locale,
    NSCalendar* calendar,
    NSDateFormatterStyle date_style,
    NSDateFormatterStyle time_style) {
  NSDateFormatter* formatter = [[NSDateFormatter alloc] init];
  formatter.locale = locale;
  formatter.dateStyle = date_style;
  formatter.timeStyle = time_style;
  formatter.timeZone = [NSTimeZone timeZoneWithAbbreviation:@"UTC"];
  formatter.calendar = calendar;
  return formatter;
}

static inline String NormalizeWhitespace(const String& date_time_format) {
  String normalized_date_time_format = date_time_format;
  // Revert ICU 72 change that introduced U+202F instead of U+0020
  // to separate time from AM/PM.
  //
  // TODO(https://crbug.com/1453047): Move this normalization to
  // `//third_party/icu/` or `//third_party/icu/patches/`.
  return normalized_date_time_format.Replace(0x202f, 0x20);
}

LocaleMac::LocaleMac(NSLocale* locale)
    : locale_(locale),
      gregorian_calendar_([[NSCalendar alloc]
          initWithCalendarIdentifier:NSCalendarIdentifierGregorian]),
      did_initialize_number_data_(false) {
  NSArray* available_languages = NSLocale.ISOLanguageCodes;
  // NSLocale returns a lower case NSLocaleLanguageCode so we don't have care
  // about case.
  NSString* language = [locale_ objectForKey:NSLocaleLanguageCode];
  if ([available_languages indexOfObject:language] == NSNotFound) {
    locale_ = [[NSLocale alloc] initWithLocaleIdentifier:DefaultLanguage()];
  }
  [gregorian_calendar_ setLocale:locale_];
}

LocaleMac::~LocaleMac() = default;

std::unique_ptr<LocaleMac> LocaleMac::Create(const String& locale_identifier) {
  NSLocale* locale = [NSLocale localeWithLocaleIdentifier:locale_identifier];
  return LocaleMac::Create(locale);
}

std::unique_ptr<LocaleMac> LocaleMac::Create(NSLocale* locale) {
  return base::WrapUnique(new LocaleMac(locale));
}

NSDateFormatter* LocaleMac::ShortDateFormatter() {
  return CreateDateTimeFormatter(locale_, gregorian_calendar_,
                                 NSDateFormatterShortStyle,
                                 NSDateFormatterNoStyle);
}

const Vector<String>& LocaleMac::MonthLabels() {
  if (month_labels_.empty()) {
    month_labels_.reserve(12);
    NSArray* array = ShortDateFormatter().monthSymbols;
    if (array.count == 12) {
      for (unsigned i = 0; i < 12; ++i) {
        month_labels_.push_back(String(array[i]));
      }
    } else {
      base::ranges::copy(kFallbackMonthNames,
                         std::back_inserter(month_labels_));
    }
  }
  return month_labels_;
}

const Vector<String>& LocaleMac::WeekDayShortLabels() {
  if (week_day_short_labels_.empty()) {
    week_day_short_labels_.reserve(7);
    NSArray* array = ShortDateFormatter().veryShortWeekdaySymbols;
    if (array.count == 7) {
      for (unsigned i = 0; i < 7; ++i) {
        week_day_short_labels_.push_back(String(array[i]));
      }
    } else {
      base::ranges::copy(kFallbackWeekdayShortNames,
                         std::back_inserter(week_day_short_labels_));
    }
  }
  return week_day_short_labels_;
}

unsigned LocaleMac::FirstDayOfWeek() {
  // The document for NSCalendar - firstWeekday doesn't have an explanation of
  // firstWeekday value. We can guess it by the document of NSDateComponents -
  // weekDay, so it can be 1 through 7 and 1 is Sunday.
  return static_cast<unsigned>(gregorian_calendar_.firstWeekday - 1);
}

bool LocaleMac::IsRTL() {
  return NSLocaleLanguageDirectionRightToLeft ==
         [NSLocale characterDirectionForLanguage:
                       [NSLocale canonicalLanguageIdentifierFromString:
                                     locale_.localeIdentifier]];
}

NSDateFormatter* LocaleMac::TimeFormatter() {
  return CreateDateTimeFormatter(locale_, gregorian_calendar_,
                                 NSDateFormatterNoStyle,
                                 NSDateFormatterMediumStyle);
}

NSDateFormatter* LocaleMac::ShortTimeFormatter() {
  return CreateDateTimeFormatter(locale_, gregorian_calendar_,
                                 NSDateFormatterNoStyle,
                                 NSDateFormatterShortStyle);
}

NSDateFormatter* LocaleMac::DateTimeFormatterWithSeconds() {
  return CreateDateTimeFormatter(locale_, gregorian_calendar_,
                                 NSDateFormatterShortStyle,
                                 NSDateFormatterMediumStyle);
}

NSDateFormatter* LocaleMac::DateTimeFormatterWithoutSeconds() {
  return CreateDateTimeFormatter(locale_, gregorian_calendar_,
                                 NSDateFormatterShortStyle,
                                 NSDateFormatterShortStyle);
}

String LocaleMac::DateFormat() {
  if (!date_format_.IsNull())
    return date_format_;
  date_format_ = ShortDateFormatter().dateFormat;
  return date_format_;
}

String LocaleMac::MonthFormat() {
  if (!month_format_.IsNull())
    return month_format_;
  // Gets a format for "MMMM" because Windows API always provides formats for
  // "MMMM" in some locales.
  month_format_ = [NSDateFormatter dateFormatFromTemplate:@"yyyyMMMM"
                                                  options:0
                                                   locale:locale_];
  return month_format_;
}

String LocaleMac::ShortMonthFormat() {
  if (!short_month_format_.IsNull())
    return short_month_format_;
  short_month_format_ = [NSDateFormatter dateFormatFromTemplate:@"yyyyMMM"
                                                        options:0
                                                         locale:locale_];
  return short_month_format_;
}

String LocaleMac::TimeFormat() {
  if (!time_format_with_seconds_.IsNull())
    return time_format_with_seconds_;
  time_format_with_seconds_ = NormalizeWhitespace(TimeFormatter().dateFormat);
  return time_format_with_seconds_;
}

String LocaleMac::ShortTimeFormat() {
  if (!time_format_without_seconds_.IsNull())
    return time_format_without_seconds_;
  time_format_without_seconds_ =
      NormalizeWhitespace(ShortTimeFormatter().dateFormat);
  return time_format_without_seconds_;
}

String LocaleMac::DateTimeFormatWithSeconds() {
  if (!date_time_format_with_seconds_.IsNull())
    return date_time_format_with_seconds_;
  date_time_format_with_seconds_ =
      NormalizeWhitespace(DateTimeFormatterWithSeconds().dateFormat);
  return date_time_format_with_seconds_;
}

String LocaleMac::DateTimeFormatWithoutSeconds() {
  if (!date_time_format_without_seconds_.IsNull())
    return date_time_format_without_seconds_;
  date_time_format_without_seconds_ =
      NormalizeWhitespace(DateTimeFormatterWithoutSeconds().dateFormat);
  return date_time_format_without_seconds_;
}

const Vector<String>& LocaleMac::ShortMonthLabels() {
  if (short_month_labels_.empty()) {
    short_month_labels_.reserve(12);
    NSArray* array = ShortDateFormatter().shortMonthSymbols;
    if (array.count == 12) {
      for (unsigned i = 0; i < 12; ++i) {
        short_month_labels_.push_back(array[i]);
      }
    } else {
      base::ranges::copy(kFallbackMonthShortNames,
                         std::back_inserter(short_month_labels_));
    }
  }
  return short_month_labels_;
}

const Vector<String>& LocaleMac::StandAloneMonthLabels() {
  if (!stand_alone_month_labels_.empty())
    return stand_alone_month_labels_;
  NSArray* array = ShortDateFormatter().standaloneMonthSymbols;
  if (array.count == 12) {
    stand_alone_month_labels_.reserve(12);
    for (unsigned i = 0; i < 12; ++i)
      stand_alone_month_labels_.push_back(array[i]);
    return stand_alone_month_labels_;
  }
  stand_alone_month_labels_ = ShortMonthLabels();
  return stand_alone_month_labels_;
}

const Vector<String>& LocaleMac::ShortStandAloneMonthLabels() {
  if (!short_stand_alone_month_labels_.empty())
    return short_stand_alone_month_labels_;
  NSArray* array = ShortDateFormatter().shortStandaloneMonthSymbols;
  if (array.count == 12) {
    short_stand_alone_month_labels_.reserve(12);
    for (unsigned i = 0; i < 12; ++i)
      short_stand_alone_month_labels_.push_back(array[i]);
    return short_stand_alone_month_labels_;
  }
  short_stand_alone_month_labels_ = ShortMonthLabels();
  return short_stand_alone_month_labels_;
}

const Vector<String>& LocaleMac::TimeAMPMLabels() {
  if (!time_ampm_labels_.empty())
    return time_ampm_labels_;
  time_ampm_labels_.reserve(2);
  NSDateFormatter* formatter = ShortTimeFormatter();
  time_ampm_labels_.push_back(formatter.AMSymbol);
  time_ampm_labels_.push_back(formatter.PMSymbol);
  return time_ampm_labels_;
}

void LocaleMac::InitializeLocaleData() {
  if (did_initialize_number_data_)
    return;
  did_initialize_number_data_ = true;

  NSNumberFormatter* formatter = [[NSNumberFormatter alloc] init];
  formatter.locale = locale_;
  formatter.numberStyle = NSNumberFormatterDecimalStyle;
  formatter.usesGroupingSeparator = NO;

  NSNumber* sample_number = @9876543210;
  String nine_to_zero([formatter stringFromNumber:sample_number]);
  if (nine_to_zero.length() != 10)
    return;
  Vector<String, kDecimalSymbolsSize> symbols;
  for (unsigned i = 0; i < 10; ++i)
    symbols.push_back(nine_to_zero.Substring(9 - i, 1));
  DCHECK(symbols.size() == kDecimalSeparatorIndex);
  symbols.push_back([formatter decimalSeparator]);
  DCHECK(symbols.size() == kGroupSeparatorIndex);
  symbols.push_back([formatter groupingSeparator]);
  DCHECK(symbols.size() == kDecimalSymbolsSize);

  String positive_prefix(formatter.positivePrefix);
  String positive_suffix(formatter.positiveSuffix);
  String negative_prefix(formatter.negativePrefix);
  String negative_suffix(formatter.negativeSuffix);
  SetLocaleData(symbols, positive_prefix, positive_suffix, negative_prefix,
                negative_suffix);
}
}