Perform uiLanguages() likely-adjusted processing purely on IDs

The collection of translations available to us need not have anything
to do with whether CLDR has matching data, so preserve the system UI
language list's entries as they are, rather than forcing them through
the QLocale constructor's exercise of likely sub-tag rules.

Instead, simply parse the given locale tags to QLocaleId instances and
use these in the likely-subtag processing to determine what other
entries to add to the list in addition to those supplied by the
operating system. Since going via QLocale did usually supply a
territory, that was included in the BCP 47 name, it's now possible for
the given entry to lack the language_territory name, so be sure to add
that if missing.

This incidentally reduces heap traffic and saves a fair deal of hidden
likely-subtag processing in calls to the constructor and bcp47Name().

Expand testing of QLocale::uiLanguages(), both plain and system.  In
the process, cross-link the two closely-related tests, move a comment
on one's _data() to the other's, where it really belongs, and add
reporting of the actual lists on failure. Enable MySystemLocale to
remember the requested locale's ID, before likely sub-tag processing,
so that we can make query() report results for language, script and
territory as requested, to ensure the fake system locale really does
match what was requested. The new german-britain test failed without
it, because there is no de-GB locale in CLDR.

Task-number: QTBUG-99531
Change-Id: Ide041577772c442a4413e3b9a590e11140c48f49
Reviewed-by: Thiago Macieira <thiago.macieira@intel.com>
bb10
Edward Welbourne 2022-08-15 15:02:01 +02:00
parent ebe9aca900
commit 6a14ad4993
3 changed files with 112 additions and 33 deletions

View File

@ -4427,18 +4427,19 @@ QString QLocale::formattedDataSize(qint64 bytes, int precision, DataSizeFormats
QStringList QLocale::uiLanguages() const
{
QStringList uiLanguages;
QList<QLocale> locales;
QList<QLocaleId> localeIds;
#ifdef QT_NO_SYSTEMLOCALE
constexpr bool isSystem = false;
#else
const bool isSystem = d->m_data == &systemLocaleData;
if (isSystem) {
uiLanguages = systemLocale()->query(QSystemLocale::UILanguages).toStringList();
// ... but we need to include likely-adjusted forms of each of those, too:
// ... but we need to include likely-adjusted forms of each of those, too.
// For now, collect up locale Ids representing the entries, for later processing:
for (const auto &entry : std::as_const(uiLanguages))
locales.append(QLocale(entry));
if (locales.isEmpty())
locales.append(systemLocale()->fallbackLocale());
localeIds.append(QLocaleId::fromName(entry));
if (localeIds.isEmpty())
localeIds.append(systemLocale()->fallbackLocale().d->m_data->id());
// If the system locale (isn't C and) didn't include itself in the list,
// or as fallback, presume to know better than it and put its name
// first. (Known issue, QTBUG-104930, on some macOS versions when in
@ -4453,20 +4454,17 @@ QStringList QLocale::uiLanguages() const
return QLocaleId::fromName(entry).withLikelySubtagsRemoved() == mine;
};
if (std::none_of(uiLanguages.constBegin(), uiLanguages.constEnd(), isMine)) {
locales.prepend(*this);
localeIds.prepend(d->m_data->id());
uiLanguages.prepend(name);
}
}
} else
#endif
{
locales.append(*this);
localeIds.append(d->m_data->id());
}
for (qsizetype i = locales.size(); i-- > 0; ) {
const QLocale &locale = locales.at(i);
const auto data = locale.d->m_data;
QLocaleId id = data->id();
for (qsizetype i = localeIds.size(); i-- > 0; ) {
QLocaleId id = localeIds.at(i);
qsizetype j;
QByteArray prior;
if (isSystem && i < uiLanguages.size()) {
@ -4477,35 +4475,49 @@ QStringList QLocale::uiLanguages() const
j = i + 1;
} else if (id.language_id == C) {
// Attempt no likely sub-tag amendments to C:
uiLanguages.append(locale.name());
uiLanguages.append(QString::fromLatin1(id.name()));
continue;
} else {
// Plain locale or empty system uiLanguages; just append.
const QString name = locale.bcp47Name();
uiLanguages.append(name);
prior = name.toLatin1();
prior = id.name();
uiLanguages.append(QString::fromLatin1(prior));
j = uiLanguages.size();
}
const QLocaleId max = id.withLikelySubtagsAdded();
const QLocaleId min = max.withLikelySubtagsRemoved();
id.script_id = 0; // For re-use as script-less variant.
// Include minimal version (last) unless it's what our locale is derived from:
if (min.name() != prior)
uiLanguages.insert(j, QString::fromLatin1(min.name()));
if (auto name = min.name(); name != prior)
uiLanguages.insert(j, QString::fromLatin1(name));
else if (!isSystem)
--j; // bcp47Name() matches min(): put more specific forms *before* it.
// Include scriptless version if likely-equivalent and distinct:
if (data->m_script_id && id != min && id.name() != prior
&& id.withLikelySubtagsAdded() == max) {
uiLanguages.insert(j, QString::fromLatin1(id.name()));
if (id.script_id) {
// Include scriptless version if likely-equivalent and distinct:
id.script_id = 0;
if (id != min && id.withLikelySubtagsAdded() == max) {
if (auto name = id.name(); name != prior)
uiLanguages.insert(j, QString::fromLatin1(name));
}
}
if (!id.territory_id) {
Q_ASSERT(!min.territory_id);
Q_ASSERT(!id.script_id); // because we just cleared it.
// Include version with territory if it likely-equivalent and distinct:
id.territory_id = max.territory_id;
if (id != max && id.withLikelySubtagsAdded() == max) {
if (auto name = id.name(); name != prior)
uiLanguages.insert(j, QString::fromLatin1(name));
}
}
// Include version with all likely sub-tags (first) if distinct from the rest:
if (max != min && max != id && max.name() != prior)
uiLanguages.insert(j, QString::fromLatin1(max.name()));
if (max != min && max != id) {
if (auto name = max.name(); name != prior)
uiLanguages.insert(j, QString::fromLatin1(name));
}
}
return uiLanguages;
}

View File

@ -121,7 +121,7 @@ namespace QIcu {
struct QLocaleId
{
[[nodiscard]] static QLocaleId fromName(QStringView name);
[[nodiscard]] Q_AUTOTEST_EXPORT static QLocaleId fromName(QStringView name);
[[nodiscard]] inline bool operator==(QLocaleId other) const
{ return language_id == other.language_id && script_id == other.script_id && territory_id == other.territory_id; }
[[nodiscard]] inline bool operator!=(QLocaleId other) const

View File

@ -122,8 +122,10 @@ private slots:
void bcp47Name_data();
void bcp47Name();
#ifdef QT_BUILD_INTERNAL
void systemLocale_data();
void systemLocale();
#endif
#ifndef QT_NO_SYSTEMLOCALE
void systemLocaleDayAndMonthNames_data();
@ -2939,18 +2941,22 @@ void tst_QLocale::uiLanguages_data()
<< QLocale(QLocale::Chinese, QLocale::SimplifiedHanScript, QLocale::China)
<< QStringList{QString("zh-Hans-CN"), QString("zh-CN"), QString("zh")};
// TODO: test actual system backends correctly handle locales with
// script-specificity (script listed first is the default, in CLDR v40):
// az_{Latn,Cyrl}_AZ, bs_{Latn,Cyrl}_BA, sr_{Cyrl,Latn}_{BA,RS,XK,UZ},
// sr_{Latn,Cyrl}_ME, ff_{Latn,Adlm}_{BF,CM,GH,GM,GN,GW,LR,MR,NE,NG,SL,SN},
// shi_{Tfng,Latn}_MA, vai_{Vaii,Latn}_LR, zh_{Hant,Hans}_{MO,HK}
// We presently map und (or any other unrecognized language) to C, ignoring
// what a sub-tag lookup would surely find us.
QTest::newRow("und_US") << QLocale("und_US") << QStringList{QString("C")};
QTest::newRow("und_Latn") << QLocale("und_Latn") << QStringList{QString("C")};
}
void tst_QLocale::uiLanguages()
{
// Compare systemLocale(), which tests the same for a stub system locale.
QFETCH(const QLocale, locale);
QFETCH(const QStringList, all);
auto reporter = qScopeGuard([&locale]() {
qDebug("\n\t%s", qPrintable(locale.uiLanguages().join(u"\n\t")));
});
QCOMPARE(locale.uiLanguages(), all);
reporter.dismiss();
}
void tst_QLocale::weekendDays()
@ -3229,19 +3235,31 @@ void tst_QLocale::bcp47Name()
QCOMPARE(QLocale(QLatin1String(QTest::currentDataTag())).bcp47Name(), expect);
}
#ifdef QT_BUILD_INTERNAL
class MySystemLocale : public QSystemLocale
{
public:
MySystemLocale(const QString &locale) : m_name(locale), m_locale(locale)
MySystemLocale(const QString &locale)
: m_name(locale), m_id(QLocaleId::fromName(locale)), m_locale(locale)
{
}
QVariant query(QueryType type, QVariant /*in*/) const override
{
if (type == UILanguages) {
switch (type) {
case UILanguages:
if (m_name == u"en-DE") // QTBUG-104930: simulate macOS's list not including m_name.
return QVariant(QStringList{QStringLiteral("en-GB"), QStringLiteral("de-DE")});
return QVariant(QStringList{m_name});
case LanguageId:
return m_id.language_id;
case TerritoryId:
return m_id.territory_id;
case ScriptId:
return m_id.script_id;
default:
break;
}
return QVariant();
}
@ -3253,6 +3271,7 @@ public:
private:
const QString m_name;
const QLocaleId m_id;
const QLocale m_locale;
};
@ -3266,6 +3285,13 @@ void tst_QLocale::systemLocale_data()
QTest::addRow("catalan")
<< QString("ca") << QLocale::Catalan
<< QStringList{QStringLiteral("ca"), QStringLiteral("ca-Latn-ES"), QStringLiteral("ca-ES")};
QTest::addRow("catalan-spain")
<< QString("ca-ES") << QLocale::Catalan
<< QStringList{QStringLiteral("ca-ES"), QStringLiteral("ca-Latn-ES"), QStringLiteral("ca")};
QTest::addRow("catalan-latin")
<< QString("ca-Latn") << QLocale::Catalan
<< QStringList{QStringLiteral("ca-Latn"), QStringLiteral("ca-Latn-ES"),
QStringLiteral("ca-ES"), QStringLiteral("ca")};
QTest::addRow("ukrainian")
<< QString("uk") << QLocale::Ukrainian
<< QStringList{QStringLiteral("uk"), QStringLiteral("uk-Cyrl-UA"), QStringLiteral("uk-UA")};
@ -3278,16 +3304,55 @@ void tst_QLocale::systemLocale_data()
QTest::addRow("german")
<< QString("de") << QLocale::German
<< QStringList{QStringLiteral("de"), QStringLiteral("de-Latn-DE"), QStringLiteral("de-DE")};
QTest::addRow("german-britain")
<< QString("de-GB") << QLocale::German
<< QStringList{QStringLiteral("de-GB"), QStringLiteral("de-Latn-GB")};
QTest::addRow("chinese-min")
<< QString("zh") << QLocale::Chinese
<< QStringList{QStringLiteral("zh"), QStringLiteral("zh-Hans-CN"), QStringLiteral("zh-CN")};
QTest::addRow("chinese-full")
<< QString("zh-Hans-CN") << QLocale::Chinese
<< QStringList{QStringLiteral("zh-Hans-CN"), QStringLiteral("zh-CN"), QStringLiteral("zh")};
// For C, it should preserve what the system gave us but only add "C", never anything more:
QTest::addRow("C") << QString("C") << QLocale::C << QStringList{QStringLiteral("C")};
QTest::addRow("C-Latn")
<< QString("C-Latn") << QLocale::C
<< QStringList{QStringLiteral("C-Latn"), QStringLiteral("C")};
QTest::addRow("C-US")
<< QString("C-US") << QLocale::C
<< QStringList{QStringLiteral("C-US"), QStringLiteral("C")};
QTest::addRow("C-Latn-US")
<< QString("C-Latn-US") << QLocale::C
<< QStringList{QStringLiteral("C-Latn-US"), QStringLiteral("C")};
QTest::addRow("C-Hans")
<< QString("C-Hans") << QLocale::C
<< QStringList{QStringLiteral("C-Hans"), QStringLiteral("C")};
QTest::addRow("C-CN")
<< QString("C-CN") << QLocale::C
<< QStringList{QStringLiteral("C-CN"), QStringLiteral("C")};
QTest::addRow("C-Hans-CN")
<< QString("C-Hans-CN") << QLocale::C
<< QStringList{QStringLiteral("C-Hans-CN"), QStringLiteral("C")};
QTest::newRow("und-US")
<< QString("und-US") << QLocale::C
<< QStringList{QStringLiteral("und-US"), QStringLiteral("C")};
QTest::newRow("und-Latn")
<< QString("und-Latn") << QLocale::C
<< QStringList{QStringLiteral("und-Latn"), QStringLiteral("C")};
// TODO: test actual system backends correctly handle locales with
// script-specificity (script listed first is the default, in CLDR v40):
// az_{Latn,Cyrl}_AZ, bs_{Latn,Cyrl}_BA, sr_{Cyrl,Latn}_{BA,RS,XK,UZ},
// sr_{Latn,Cyrl}_ME, ff_{Latn,Adlm}_{BF,CM,GH,GM,GN,GW,LR,MR,NE,NG,SL,SN},
// shi_{Tfng,Latn}_MA, vai_{Vaii,Latn}_LR, zh_{Hant,Hans}_{MO,HK}
}
void tst_QLocale::systemLocale()
{
// Compare uiLanguages(), which tests this for CLDR-derived locales.
QLocale originalLocale;
QLocale originalSystemLocale = QLocale::system();
@ -3306,9 +3371,11 @@ void tst_QLocale::systemLocale()
reporter.dismiss();
}
// Verify MySystemLocale tidy-up restored prior state:
QCOMPARE(QLocale(), originalLocale);
QCOMPARE(QLocale::system(), originalSystemLocale);
}
#endif // QT_BUILD_INTERNAL
#ifndef QT_NO_SYSTEMLOCALE