// Copyright (C) 2021 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only #include "qlocale_p.h" #include "qstringlist.h" #include "qvariant.h" #include "qdatetime.h" #include "private/qstringiterator_p.h" #include "private/qgregoriancalendar_p.h" #ifdef Q_OS_DARWIN #include "private/qcore_mac_p.h" #include #endif #include #include QT_BEGIN_NAMESPACE using namespace Qt::StringLiterals; /****************************************************************************** ** Wrappers for Mac locale system functions */ Q_LOGGING_CATEGORY(lcLocale, "qt.core.locale") static void printLocalizationInformation() { if (!lcLocale().isDebugEnabled()) return; #if defined(Q_OS_MACOS) // Trigger initialization of standard user defaults, so that Foundation picks // up -AppleLanguages and -AppleLocale passed on the command line. Q_UNUSED(NSUserDefaults.standardUserDefaults); #endif auto singleLineDescription = [](NSArray *array) { NSString *str = [array description]; str = [str stringByReplacingOccurrencesOfString:@"\n" withString:@""]; return [str stringByReplacingOccurrencesOfString:@" " withString:@""]; }; bool allowMixedLocalizations = [NSBundle.mainBundle.infoDictionary[@"CFBundleAllowMixedLocalizations"] boolValue]; NSBundle *foundation = [NSBundle bundleForClass:NSBundle.class]; qCDebug(lcLocale).nospace() << "Launched with locale \"" << NSLocale.currentLocale.localeIdentifier << "\" based on user's preferred languages " << singleLineDescription(NSLocale.preferredLanguages) << ", main bundle localizations " << singleLineDescription(NSBundle.mainBundle.localizations) << ", and allowing mixed localizations " << allowMixedLocalizations << "; resulting in main bundle preferred localizations " << singleLineDescription(NSBundle.mainBundle.preferredLocalizations) << " and Foundation preferred localizations " << singleLineDescription(foundation.preferredLocalizations); qCDebug(lcLocale) << "Reflected by Qt as system locale" << QLocale::system() << "with UI languges " << QLocale::system().uiLanguages(); } Q_COREAPP_STARTUP_FUNCTION(printLocalizationInformation); static QString getMacLocaleName() { QCFType l = CFLocaleCopyCurrent(); CFStringRef locale = CFLocaleGetIdentifier(l); return QString::fromCFString(locale); } static QVariant macMonthName(int month, QSystemLocale::QueryType type) { month -= 1; if (month < 0 || month > 11) return {}; QCFType formatter = CFDateFormatterCreate(0, QCFType(CFLocaleCopyCurrent()), kCFDateFormatterNoStyle, kCFDateFormatterNoStyle); CFDateFormatterKey formatterType; switch (type) { case QSystemLocale::MonthNameLong: formatterType = kCFDateFormatterMonthSymbols; break; case QSystemLocale::MonthNameShort: formatterType = kCFDateFormatterShortMonthSymbols; break; case QSystemLocale::MonthNameNarrow: formatterType = kCFDateFormatterVeryShortMonthSymbols; break; case QSystemLocale::StandaloneMonthNameLong: formatterType = kCFDateFormatterStandaloneMonthSymbols; break; case QSystemLocale::StandaloneMonthNameShort: formatterType = kCFDateFormatterShortStandaloneMonthSymbols; break; case QSystemLocale::StandaloneMonthNameNarrow: formatterType = kCFDateFormatterVeryShortStandaloneMonthSymbols; break; default: qWarning("macMonthName: Unsupported query type %d", type); return {}; } QCFType values = static_cast(CFDateFormatterCopyProperty(formatter, formatterType)); if (values != 0) { CFStringRef cfstring = static_cast(CFArrayGetValueAtIndex(values, month)); return QString::fromCFString(cfstring); } return {}; } static QVariant macDayName(int day, QSystemLocale::QueryType type) { if (day < 1 || day > 7) return {}; QCFType formatter = CFDateFormatterCreate(0, QCFType(CFLocaleCopyCurrent()), kCFDateFormatterNoStyle, kCFDateFormatterNoStyle); CFDateFormatterKey formatterType; switch (type) { case QSystemLocale::DayNameLong: formatterType = kCFDateFormatterWeekdaySymbols; break; case QSystemLocale::DayNameShort: formatterType = kCFDateFormatterShortWeekdaySymbols; break; case QSystemLocale::DayNameNarrow: formatterType = kCFDateFormatterVeryShortWeekdaySymbols; break; case QSystemLocale::StandaloneDayNameLong: formatterType = kCFDateFormatterStandaloneWeekdaySymbols; break; case QSystemLocale::StandaloneDayNameShort: formatterType = kCFDateFormatterShortStandaloneWeekdaySymbols; break; case QSystemLocale::StandaloneDayNameNarrow: formatterType = kCFDateFormatterVeryShortStandaloneWeekdaySymbols; break; default: qWarning("macDayName: Unsupported query type %d", type); return {}; } QCFType values = static_cast(CFDateFormatterCopyProperty(formatter, formatterType)); if (values != 0) { CFStringRef cfstring = static_cast(CFArrayGetValueAtIndex(values, day % 7)); return QString::fromCFString(cfstring); } return {}; } static QString macZeroDigit() { static QString cachedZeroDigit; if (cachedZeroDigit.isNull()) { QCFType locale = CFLocaleCopyCurrent(); QCFType numberFormatter = CFNumberFormatterCreate(nullptr, locale, kCFNumberFormatterNoStyle); const int zeroDigit = 0; QCFType value = CFNumberFormatterCreateStringWithValue(nullptr, numberFormatter, kCFNumberIntType, &zeroDigit); cachedZeroDigit = QString::fromCFString(value); } static QMacNotificationObserver localeChangeObserver = QMacNotificationObserver( nil, NSCurrentLocaleDidChangeNotification, [&] { qCDebug(lcLocale) << "System locale changed"; cachedZeroDigit = QString(); }); return cachedZeroDigit; } static QString zeroPad(QString &&number, qsizetype minDigits, const QString &zero) { // Need to pad with zeros, possibly after a sign. qsizetype insert = -1, digits = 0; auto it = QStringIterator(number); while (it.hasNext()) { qsizetype here = it.index(); if (QChar::isDigit(it.next())) { if (insert < 0) insert = here; ++digits; } // else: assume we're stepping over a sign (or maybe grouping separator) } Q_ASSERT(digits > 0); Q_ASSERT(insert >= 0); while (digits++ < minDigits) number.insert(insert, zero); return std::move(number); } static QString trimTwoDigits(QString &&number) { // Retain any sign, but remove all but the last two digits. // We know number has at least four digits - it came from fourDigitYear(). // Note that each digit might be a surrogate pair. qsizetype first = -1, prev = -1, last = -1; auto it = QStringIterator(number); while (it.hasNext()) { qsizetype here = it.index(); if (QChar::isDigit(it.next())) { if (first == -1) last = first = here; else if (last != -1) prev = std::exchange(last, here); } } Q_ASSERT(first >= 0); Q_ASSERT(prev > first); Q_ASSERT(last > prev); number.remove(first, prev - first); return std::move(number); } static QString fourDigitYear(int year, const QString &zero) { // Return year formatted as an (at least) four digit number: QCFType locale = CFLocaleCopyCurrent(); QCFType numberFormatter = CFNumberFormatterCreate(nullptr, locale, kCFNumberFormatterNoStyle); QCFType value = CFNumberFormatterCreateStringWithValue(nullptr, numberFormatter, kCFNumberIntType, &year); auto text = QString::fromCFString(value); if (year > -1000 && year < 1000) text = zeroPad(std::move(text), 4, zero); return text; } static QString macDateToStringImpl(QDate date, CFDateFormatterStyle style) { // Use noon on the given date, to avoid complications that can arise for // dates before 1900 (see QTBUG-54955) using different UTC offset than // QDateTime extrapolates backwards from time_t functions that only work // back to 1900. (Alaska and Phillipines may still be borked, though.) QCFType myDate = QDateTime(date, QTime(12, 0)).toCFDate(); QCFType mylocale = CFLocaleCopyCurrent(); QCFType myFormatter = CFDateFormatterCreate(kCFAllocatorDefault, mylocale, style, kCFDateFormatterNoStyle); QCFType text = CFDateFormatterCreateStringWithDate(nullptr, myFormatter, myDate); return QString::fromCFString(text); } static QVariant macDateToString(QDate date, bool short_format) { const int year = date.year(); QString fakeYear, trueYear; if (year < 1583) { // System API (in macOS 11.0, at least) discards sign :-( // Simply negating the year won't do as the resulting year typically has // a different pattern of week-days. // Furthermore (see QTBUG-54955), Darwin uses the Julian calendar for // dates before 1582-10-15, leading to discrepancies. int matcher = QGregorianCalendar::yearSharingWeekDays(date); Q_ASSERT(matcher >= 1583); Q_ASSERT(matcher % 100 != date.month()); Q_ASSERT(matcher % 100 != date.day()); // i.e. there can't be any confusion between the two-digit year and // month or day-of-month in the formatted date. QString zero = macZeroDigit(); fakeYear = fourDigitYear(matcher, zero); trueYear = fourDigitYear(year, zero); date = QDate(matcher, date.month(), date.day()); } QString text = macDateToStringImpl(date, short_format ? kCFDateFormatterShortStyle : kCFDateFormatterLongStyle); if (year < 1583) { if (text.contains(fakeYear)) return std::move(text).replace(fakeYear, trueYear); // Cope with two-digit year: fakeYear = trimTwoDigits(std::move(fakeYear)); trueYear = trimTwoDigits(std::move(trueYear)); if (text.contains(fakeYear)) return std::move(text).replace(fakeYear, trueYear); // That should have worked. qWarning("Failed to fix up year when formatting a date in year %d", year); } return text; } static QVariant macTimeToString(QTime time, bool short_format) { QCFType myDate = QDateTime(QDate::currentDate(), time).toCFDate(); QCFType mylocale = CFLocaleCopyCurrent(); CFDateFormatterStyle style = short_format ? kCFDateFormatterShortStyle : kCFDateFormatterLongStyle; QCFType myFormatter = CFDateFormatterCreate(kCFAllocatorDefault, mylocale, kCFDateFormatterNoStyle, style); QCFType text = CFDateFormatterCreateStringWithDate(0, myFormatter, myDate); return QString::fromCFString(text); } // Mac uses the Unicode CLDR format codes // http://www.unicode.org/reports/tr35/tr35-dates.html#Date_Field_Symbol_Table // See also qtbase/util/locale_database/dateconverter.py // Makes the assumption that input formats are always well formed and consecutive letters // never exceed the maximum for the format code. static QVariant macToQtFormat(QStringView sys_fmt) { QString result; qsizetype i = 0; while (i < sys_fmt.size()) { if (sys_fmt.at(i).unicode() == '\'') { QString text = qt_readEscapedFormatString(sys_fmt, &i); if (text == "'"_L1) result += "''"_L1; else result += u'\'' + text + u'\''; continue; } QChar c = sys_fmt.at(i); qsizetype repeat = qt_repeatCount(sys_fmt.sliced(i)); switch (c.unicode()) { // Qt does not support the following options case 'A': // Milliseconds in Day (1..n): 1..n = padded number case 'C': // Input skeleton symbol. case 'D': // Day of Year (1..3): 1..3 = padded number case 'F': // Day of Week in Month (1): 1 = number case 'g': // Modified Julian Day (1..n): 1..n = padded number case 'G': // Era (1..5): 4 = long, 1..3 = short, 5 = narrow case 'j': // Input skeleton symbol. case 'J': // Input skeleton symbol. case 'l': // Deprecated Chinese leap month indicator. case 'q': // Standalone Quarter (1..4): 4 = long, 3 = short, 1,2 = padded number case 'Q': // Quarter (1..4): 4 = long, 3 = short, 1,2 = padded number case 'U': // Cyclic Year Name (1..5): 4 = long, 1..3 = short, 5 = narrow case 'w': // Week of Year (1,2): 1,2 = padded number case 'W': // Week of Month (1): 1 = number case 'Y': // Year for Week-of-year calendars (1..n): 1..n = padded number break; case 'u': // Extended Year (1..n), padded number. // Explicitly has no special case for 'uu' as only the last two digits. result += "yyyy"_L1; break; case 'y': // Year (1..n): 2 = short year, 1 & 3..n = padded number // Qt only supports long (4) or short (2) year, use long for all others if (repeat == 2) result += "yy"_L1; else result += "yyyy"_L1; break; case 'L': // Standalone Month (1..5): 4 = long, 3 = short, 1,2 = number, 5 = narrow case 'M': // Month (1..5): 4 = long, 3 = short, 1,2 = number, 5 = narrow // Qt only supports long, short and number, use short for narrow if (repeat == 5) result += "MMM"_L1; else result += QString(repeat, u'M'); break; case 'd': // Day of Month (1,2): 1,2 padded number result += QString(repeat, c); break; case 'c': // Standalone version of 'e' case 'e': // Local Day of Week (1..6): 4 = long, 3 = short, 5,6 = narrow, 1,2 padded number // "Local" only affects numeric form: depends on locale's start-day of the week. case 'E': // Day of Week (1..6): 4 = long, 1..3 = short, 5,6 = narrow // Qt only supports long, short: use short for narrow and padded number. if (repeat == 4) result += "dddd"_L1; else result += "ddd"_L1; break; case 'a': // AM/PM (1..n): Qt supports no distinctions case 'b': // Like a, but also distinguishing noon, midnight (ignore difference). case 'B': // Flexible day period (at night, &c.) // Translate to Qt AM/PM, using locale-appropriate case: result += "Ap"_L1; break; case 'h': // Hour [1..12] (1,2): 1,2 = padded number case 'K': // Hour [0..11] (1,2): 1,2 = padded number result += QString(repeat, 'h'_L1); break; case 'H': // Hour [0..23] (1,2): 1,2 = padded number case 'k': // Hour [1..24] (1,2): 1,2 = padded number // Qt H is 0..23 hour result += QString(repeat, 'H'_L1); break; case 'm': // Minutes (1,2): 1,2 = padded number case 's': // Seconds (1,2): 1,2 = padded number result += QString(repeat, c); break; case 'S': // Fractional second (1..n): 1..n = truncates to decimal places // Qt uses msecs either unpadded or padded to 3 places if (repeat < 3) result += u'z'; else result += "zzz"_L1; break; case 'O': // Time Zone (1, 4) result += u't'; break; case 'v': // Time Zone (1, 4) case 'V': // Time Zone (1..4) result += "tttt"_L1; break; case 'x': // Time Zone (1..5) case 'X': // Time Zone (1..5) result += (repeat > 1 && (repeat & 1)) ? "ttt"_L1 : "tt"_L1; break; case 'z': // Time Zone (1..4) case 'Z': // Time Zone (1..5) result += repeat < 4 ? "tt"_L1 : repeat > 4 ? "ttt"_L1 : "t"_L1; break; default: // a..z and A..Z are reserved for format codes, so any occurrence of these not // already processed are not known and so unsupported formats to be ignored. // All other chars are allowed as literals. if (c < u'A' || c > u'z' || (c > u'Z' && c < u'a')) result += QString(repeat, c); break; } i += repeat; } return !result.isEmpty() ? QVariant::fromValue(result) : QVariant(); } static QVariant getMacDateFormat(CFDateFormatterStyle style) { QCFType l = CFLocaleCopyCurrent(); QCFType formatter = CFDateFormatterCreate(kCFAllocatorDefault, l, style, kCFDateFormatterNoStyle); return macToQtFormat(QString::fromCFString(CFDateFormatterGetFormat(formatter))); } static QVariant getMacTimeFormat(CFDateFormatterStyle style) { QCFType l = CFLocaleCopyCurrent(); QCFType formatter = CFDateFormatterCreate(kCFAllocatorDefault, l, kCFDateFormatterNoStyle, style); return macToQtFormat(QString::fromCFString(CFDateFormatterGetFormat(formatter))); } static QVariant getCFLocaleValue(CFStringRef key) { QCFType locale = CFLocaleCopyCurrent(); CFTypeRef value = CFLocaleGetValue(locale, key); if (!value) return QVariant(); return QString::fromCFString(CFStringRef(static_cast(value))); } static QVariant macMeasurementSystem() { QCFType locale = CFLocaleCopyCurrent(); CFStringRef system = static_cast(CFLocaleGetValue(locale, kCFLocaleMeasurementSystem)); if (QString::fromCFString(system) == "Metric"_L1) { return QLocale::MetricSystem; } else { return QLocale::ImperialSystem; } } static quint8 macFirstDayOfWeek() { QCFType calendar = CFCalendarCopyCurrent(); quint8 day = static_cast(CFCalendarGetFirstWeekday(calendar))-1; if (day == 0) day = 7; return day; } static QVariant macCurrencySymbol(QLocale::CurrencySymbolFormat format) { QCFType locale = CFLocaleCopyCurrent(); switch (format) { case QLocale::CurrencyIsoCode: return QString::fromCFString(static_cast(CFLocaleGetValue(locale, kCFLocaleCurrencyCode))); case QLocale::CurrencySymbol: return QString::fromCFString(static_cast(CFLocaleGetValue(locale, kCFLocaleCurrencySymbol))); case QLocale::CurrencyDisplayName: { CFStringRef code = static_cast(CFLocaleGetValue(locale, kCFLocaleCurrencyCode)); QCFType value = CFLocaleCopyDisplayNameForPropertyValue(locale, kCFLocaleCurrencyCode, code); return QString::fromCFString(value); } default: break; } return {}; } #ifndef QT_NO_SYSTEMLOCALE static QVariant macFormatCurrency(const QSystemLocale::CurrencyToStringArgument &arg) { QCFType value; switch (arg.value.metaType().id()) { case QMetaType::Int: case QMetaType::UInt: { int v = arg.value.toInt(); value = CFNumberCreate(NULL, kCFNumberIntType, &v); break; } case QMetaType::Double: { double v = arg.value.toDouble(); value = CFNumberCreate(NULL, kCFNumberDoubleType, &v); break; } case QMetaType::LongLong: case QMetaType::ULongLong: { qint64 v = arg.value.toLongLong(); value = CFNumberCreate(NULL, kCFNumberLongLongType, &v); break; } default: return {}; } QCFType locale = CFLocaleCopyCurrent(); QCFType currencyFormatter = CFNumberFormatterCreate(NULL, locale, kCFNumberFormatterCurrencyStyle); if (!arg.symbol.isEmpty()) { CFNumberFormatterSetProperty(currencyFormatter, kCFNumberFormatterCurrencySymbol, arg.symbol.toCFString()); } QCFType result = CFNumberFormatterCreateStringWithNumber(NULL, currencyFormatter, value); return QString::fromCFString(result); } static QVariant macQuoteString(QSystemLocale::QueryType type, QStringView str) { QString begin, end; QCFType locale = CFLocaleCopyCurrent(); switch (type) { case QSystemLocale::StringToStandardQuotation: begin = QString::fromCFString(static_cast(CFLocaleGetValue(locale, kCFLocaleQuotationBeginDelimiterKey))); end = QString::fromCFString(static_cast(CFLocaleGetValue(locale, kCFLocaleQuotationEndDelimiterKey))); return QString(begin % str % end); case QSystemLocale::StringToAlternateQuotation: begin = QString::fromCFString(static_cast(CFLocaleGetValue(locale, kCFLocaleAlternateQuotationBeginDelimiterKey))); end = QString::fromCFString(static_cast(CFLocaleGetValue(locale, kCFLocaleAlternateQuotationEndDelimiterKey))); return QString(begin % str % end); default: break; } return QVariant(); } #endif //QT_NO_SYSTEMLOCALE #ifndef QT_NO_SYSTEMLOCALE QLocale QSystemLocale::fallbackLocale() const { return QLocale(getMacLocaleName()); } template static QVariant getLocaleValue(CFStringRef key) { if (auto code = getCFLocaleValue(key); !code.isNull()) { // If an invalid locale is requested with -AppleLocale, the system APIs // will report invalid or empty locale values back to us, which codeToLanguage() // and friends will fail to parse, resulting in returning QLocale::Any{L/C/S}. // If this is the case, we fall down and return a null-variant, which // QLocale's updateSystemPrivate() will interpret to use fallback logic. if (auto value = CodeToValueFunction(code.toString())) return value; } return QVariant(); } static QLocale::Language codeToLanguage(QStringView s) { return QLocalePrivate::codeToLanguage(s); } QVariant QSystemLocale::query(QueryType type, QVariant &&in) const { QMacAutoReleasePool pool; switch(type) { case LanguageId: return getLocaleValue(kCFLocaleLanguageCode); case TerritoryId: return getLocaleValue(kCFLocaleCountryCode); case ScriptId: return getLocaleValue(kCFLocaleScriptCode); case DecimalPoint: return getCFLocaleValue(kCFLocaleDecimalSeparator); case GroupSeparator: return getCFLocaleValue(kCFLocaleGroupingSeparator); case DateFormatLong: case DateFormatShort: return getMacDateFormat(type == DateFormatShort ? kCFDateFormatterShortStyle : kCFDateFormatterLongStyle); case TimeFormatLong: case TimeFormatShort: return getMacTimeFormat(type == TimeFormatShort ? kCFDateFormatterShortStyle : kCFDateFormatterLongStyle); case DayNameLong: case DayNameShort: case DayNameNarrow: case StandaloneDayNameLong: case StandaloneDayNameShort: case StandaloneDayNameNarrow: return macDayName(in.toInt(), type); case MonthNameLong: case MonthNameShort: case MonthNameNarrow: case StandaloneMonthNameLong: case StandaloneMonthNameShort: case StandaloneMonthNameNarrow: return macMonthName(in.toInt(), type); case DateToStringShort: case DateToStringLong: return macDateToString(in.toDate(), (type == DateToStringShort)); case TimeToStringShort: case TimeToStringLong: return macTimeToString(in.toTime(), (type == TimeToStringShort)); case NegativeSign: case PositiveSign: break; case ZeroDigit: return macZeroDigit(); case MeasurementSystem: return macMeasurementSystem(); case AMText: case PMText: { QCFType locale = CFLocaleCopyCurrent(); QCFType formatter = CFDateFormatterCreate(NULL, locale, kCFDateFormatterLongStyle, kCFDateFormatterLongStyle); QCFType value = static_cast(CFDateFormatterCopyProperty(formatter, (type == AMText ? kCFDateFormatterAMSymbol : kCFDateFormatterPMSymbol))); return QString::fromCFString(value); } case FirstDayOfWeek: return QVariant(macFirstDayOfWeek()); case CurrencySymbol: return macCurrencySymbol(QLocale::CurrencySymbolFormat(in.toUInt())); case CurrencyToString: return macFormatCurrency(in.value()); case UILanguages: { QStringList result; QCFType languages = CFLocaleCopyPreferredLanguages(); const CFIndex cnt = CFArrayGetCount(languages); result.reserve(cnt); for (CFIndex i = 0; i < cnt; ++i) { const QString lang = QString::fromCFString( static_cast(CFArrayGetValueAtIndex(languages, i))); result.append(lang); } return QVariant(result); } case StringToStandardQuotation: case StringToAlternateQuotation: return macQuoteString(type, in.value()); default: break; } return QVariant(); } #endif // QT_NO_SYSTEMLOCALE QT_END_NAMESPACE