diff options
Diffstat (limited to 'src/corelib/text/qlocale_mac.mm')
-rw-r--r-- | src/corelib/text/qlocale_mac.mm | 445 |
1 files changed, 288 insertions, 157 deletions
diff --git a/src/corelib/text/qlocale_mac.mm b/src/corelib/text/qlocale_mac.mm index 4293cdda27..89339be2eb 100644 --- a/src/corelib/text/qlocale_mac.mm +++ b/src/corelib/text/qlocale_mac.mm @@ -1,41 +1,5 @@ -/**************************************************************************** -** -** Copyright (C) 2020 The Qt Company Ltd. -** Contact: https://www.qt.io/licensing/ -** -** This file is part of the QtCore module of the Qt Toolkit. -** -** $QT_BEGIN_LICENSE:LGPL$ -** Commercial License Usage -** Licensees holding valid commercial Qt licenses may use this file in -** accordance with the commercial license agreement provided with the -** Software or, alternatively, in accordance with the terms contained in -** a written agreement between you and The Qt Company. For licensing terms -** and conditions see https://www.qt.io/terms-conditions. For further -** information use the contact form at https://www.qt.io/contact-us. -** -** GNU Lesser General Public License Usage -** Alternatively, this file may be used under the terms of the GNU Lesser -** General Public License version 3 as published by the Free Software -** Foundation and appearing in the file LICENSE.LGPL3 included in the -** packaging of this file. Please review the following information to -** ensure the GNU Lesser General Public License version 3 requirements -** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. -** -** GNU General Public License Usage -** Alternatively, this file may be used under the terms of the GNU -** General Public License version 2.0 or (at your option) the GNU General -** Public license version 3 or any later version approved by the KDE Free -** Qt Foundation. The licenses are as published by the Free Software -** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 -** included in the packaging of this file. Please review the following -** information to ensure the GNU General Public License requirements will -** be met: https://www.gnu.org/licenses/gpl-2.0.html and -** https://www.gnu.org/licenses/gpl-3.0.html. -** -** $QT_END_LICENSE$ -** -****************************************************************************/ +// 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" @@ -43,17 +7,59 @@ #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 <CoreFoundation/CoreFoundation.h> #endif +#include <QtCore/qloggingcategory.h> +#include <QtCore/qcoreapplication.h> + 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<CFLocaleRef> l = CFLocaleCopyCurrent(); @@ -148,16 +154,139 @@ static QVariant macDayName(int day, QSystemLocale::QueryType type) return {}; } -static QVariant macDateToString(QDate date, bool short_format) +static QString macZeroDigit() +{ + static QString cachedZeroDigit; + + if (cachedZeroDigit.isNull()) { + QCFType<CFLocaleRef> locale = CFLocaleCopyCurrent(); + QCFType<CFNumberFormatterRef> numberFormatter = + CFNumberFormatterCreate(nullptr, locale, kCFNumberFormatterNoStyle); + const int zeroDigit = 0; + QCFType<CFStringRef> 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) { - QCFType<CFDateRef> myDate = QDateTime(date, QTime()).toCFDate(); + // Return year formatted as an (at least) four digit number: + QCFType<CFLocaleRef> locale = CFLocaleCopyCurrent(); + QCFType<CFNumberFormatterRef> numberFormatter = + CFNumberFormatterCreate(nullptr, locale, kCFNumberFormatterNoStyle); + QCFType<CFStringRef> 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<CFDateRef> myDate = QDateTime(date, QTime(12, 0)).toCFDate(); QCFType<CFLocaleRef> mylocale = CFLocaleCopyCurrent(); - CFDateFormatterStyle style = short_format ? kCFDateFormatterShortStyle : kCFDateFormatterLongStyle; QCFType<CFDateFormatterRef> myFormatter - = CFDateFormatterCreate(kCFAllocatorDefault, - mylocale, style, + = CFDateFormatterCreate(kCFAllocatorDefault, mylocale, style, kCFDateFormatterNoStyle); - return QString::fromCFString(CFDateFormatterCreateStringWithDate(0, myFormatter, myDate)); + QCFType<CFStringRef> 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) @@ -169,7 +298,8 @@ static QVariant macTimeToString(QTime time, bool short_format) mylocale, kCFDateFormatterNoStyle, style); - return QString::fromCFString(CFDateFormatterCreateStringWithDate(0, myFormatter, myDate)); + QCFType<CFStringRef> text = CFDateFormatterCreateStringWithDate(0, myFormatter, myDate); + return QString::fromCFString(text); } // Mac uses the Unicode CLDR format codes @@ -180,114 +310,120 @@ static QVariant macTimeToString(QTime time, bool short_format) static QVariant macToQtFormat(QStringView sys_fmt) { QString result; - int i = 0; + qsizetype i = 0; while (i < sys_fmt.size()) { if (sys_fmt.at(i).unicode() == '\'') { QString text = qt_readEscapedFormatString(sys_fmt, &i); - if (text == QLatin1String("'")) - result += QLatin1String("''"); + if (text == "'"_L1) + result += "''"_L1; else - result += QLatin1Char('\'') + text + QLatin1Char('\''); + result += u'\'' + text + u'\''; continue; } QChar c = sys_fmt.at(i); - int repeat = qt_repeatCount(sys_fmt.mid(i)); + qsizetype repeat = qt_repeatCount(sys_fmt.sliced(i)); switch (c.unicode()) { // Qt does not support the following options - case 'G': // Era (1..5): 4 = long, 1..3 = short, 5 = narrow - case 'Y': // Year of Week (1..n): 1..n = padded number - case 'U': // Cyclic Year Name (1..5): 4 = long, 1..3 = short, 5 = narrow - case 'Q': // Quarter (1..4): 4 = long, 3 = short, 1..2 = padded number - case 'q': // Standalone Quarter (1..4): 4 = long, 3 = short, 1..2 = padded number - case 'w': // Week of Year (1..2): 1..2 = padded number - case 'W': // Week of Month (1): 1 = number - 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 'A': // Milliseconds in Day (1..n): 1..n = padded number - break; - - case 'y': // Year (1..n): 2 = short year, 1 & 3..n = padded number - case 'u': // Extended 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 += QLatin1String("yy"); - else - result += QLatin1String("yyyy"); - break; - case 'M': // Month (1..5): 4 = long, 3 = short, 1..2 = number, 5 = narrow - case 'L': // Standalone 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 += QLatin1String("MMM"); - else - result += QString(repeat, QLatin1Char('M')); - break; - case 'd': // Day of Month (1..2): 1..2 padded number - result += QString(repeat, c); - break; - case 'E': // Day of Week (1..6): 4 = long, 1..3 = short, 5..6 = narrow - // Qt only supports long, short and padded number, use short for narrow - if (repeat == 4) - result += QLatin1String("dddd"); - else - result += QLatin1String("ddd"); - break; - case 'e': // Local Day of Week (1..6): 4 = long, 3 = short, 5..6 = narrow, 1..2 padded number - case 'c': // Standalone Local Day of Week (1..6): 4 = long, 3 = short, 5..6 = narrow, 1..2 padded number - // Qt only supports long, short and padded number, use short for narrow - if (repeat >= 5) - result += QLatin1String("ddd"); - else - result += QString(repeat, QLatin1Char('d')); - break; - case 'a': // AM/PM (1): 1 = short - // Translate to Qt uppercase AM/PM - result += QLatin1String("AP"); - break; - case 'h': // Hour [1..12] (1..2): 1..2 = padded number - case 'K': // Hour [0..11] (1..2): 1..2 = padded number - case 'j': // Local Hour [12 or 24] (1..2): 1..2 = padded number - // Qt h is local hour - result += QString(repeat, QLatin1Char('h')); - 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, QLatin1Char('H')); - break; - case 'm': // Minutes (1..2): 1..2 = padded number - case 's': // Seconds (1..2): 1..2 = padded number + 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; - 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 += QLatin1Char('z'); - else - result += QLatin1String("zzz"); - break; - case 'z': // Time Zone (1..4) - case 'Z': // Time Zone (1..5) - case 'O': // Time Zone (1, 4) - case 'v': // Time Zone (1, 4) - case 'V': // Time Zone (1..4) - case 'X': // Time Zone (1..5) - case 'x': // Time Zone (1..5) - result += QLatin1Char('t'); - 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 < QLatin1Char('A') || c > QLatin1Char('z') || - (c > QLatin1Char('Z') && c < QLatin1Char('a'))) { - result += QString(repeat, c); - } - break; + break; } i += repeat; @@ -325,7 +461,7 @@ static QVariant macMeasurementSystem() { QCFType<CFLocaleRef> locale = CFLocaleCopyCurrent(); CFStringRef system = static_cast<CFStringRef>(CFLocaleGetValue(locale, kCFLocaleMeasurementSystem)); - if (QString::fromCFString(system) == QLatin1String("Metric")) { + if (QString::fromCFString(system) == "Metric"_L1) { return QLocale::MetricSystem; } else { return QLocale::ImperialSystem; @@ -361,17 +497,6 @@ static QVariant macCurrencySymbol(QLocale::CurrencySymbolFormat format) return {}; } -static QVariant macZeroDigit() -{ - QCFType<CFLocaleRef> locale = CFLocaleCopyCurrent(); - QCFType<CFNumberFormatterRef> numberFormatter = - CFNumberFormatterCreate(nullptr, locale, kCFNumberFormatterNoStyle); - static const int zeroDigit = 0; - QCFType<CFStringRef> value = CFNumberFormatterCreateStringWithValue(nullptr, numberFormatter, - kCFNumberIntType, &zeroDigit); - return QString::fromCFString(value); -} - #ifndef QT_NO_SYSTEMLOCALE static QVariant macFormatCurrency(const QSystemLocale::CurrencyToStringArgument &arg) { @@ -451,12 +576,18 @@ static QVariant getLocaleValue(CFStringRef key) return QVariant(); } -QVariant QSystemLocale::query(QueryType type, QVariant in) const +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<QLocalePrivate::codeToLanguage>(kCFLocaleLanguageCode); + return getLocaleValue<codeToLanguage>(kCFLocaleLanguageCode); case TerritoryId: return getLocaleValue<QLocalePrivate::codeToTerritory>(kCFLocaleCountryCode); case ScriptId: @@ -518,13 +649,13 @@ QVariant QSystemLocale::query(QueryType type, QVariant in) const case CurrencySymbol: return macCurrencySymbol(QLocale::CurrencySymbolFormat(in.toUInt())); case CurrencyToString: - return macFormatCurrency(in.value<QSystemLocale::CurrencyToStringArgument>()); + return macFormatCurrency(in.value<CurrencyToStringArgument>()); case UILanguages: { QStringList result; QCFType<CFArrayRef> languages = CFLocaleCopyPreferredLanguages(); - const int cnt = CFArrayGetCount(languages); + const CFIndex cnt = CFArrayGetCount(languages); result.reserve(cnt); - for (int i = 0; i < cnt; ++i) { + for (CFIndex i = 0; i < cnt; ++i) { const QString lang = QString::fromCFString( static_cast<CFStringRef>(CFArrayGetValueAtIndex(languages, i))); result.append(lang); |