Skip to content

Commit

Permalink
Merge branch 'issue-2008' into 3.2.x-stable
Browse files Browse the repository at this point in the history
  • Loading branch information
adriaandegroot committed Aug 24, 2022
2 parents befa677 + 3f5d656 commit f277cbb
Show file tree
Hide file tree
Showing 6 changed files with 328 additions and 121 deletions.
15 changes: 4 additions & 11 deletions src/modules/locale/CMakeLists.txt
Expand Up @@ -22,6 +22,7 @@ calamares_add_plugin(locale
Config.cpp
LCLocaleDialog.cpp
LocaleConfiguration.cpp
LocaleNames.cpp
LocalePage.cpp
LocaleViewStep.cpp
SetTimezoneJob.cpp
Expand All @@ -39,15 +40,7 @@ calamares_add_plugin(locale

calamares_add_test(
localetest
SOURCES
Tests.cpp
Config.cpp
LocaleConfiguration.cpp
SetTimezoneJob.cpp
timezonewidget/TimeZoneImage.cpp
DEFINITIONS
SOURCE_DIR="${CMAKE_CURRENT_LIST_DIR}/images"
DEBUG_TIMEZONES=1
LIBRARIES
Qt5::Gui
SOURCES Tests.cpp Config.cpp LocaleConfiguration.cpp LocaleNames.cpp SetTimezoneJob.cpp timezonewidget/TimeZoneImage.cpp
DEFINITIONS SOURCE_DIR="${CMAKE_CURRENT_LIST_DIR}/images" DEBUG_TIMEZONES=1
LIBRARIES Qt5::Gui
)
196 changes: 91 additions & 105 deletions src/modules/locale/LocaleConfiguration.cpp
Expand Up @@ -9,11 +9,13 @@
*/

#include "LocaleConfiguration.h"
#include "LocaleNames.h"

#include "utils/Logger.h"

#include <QLocale>
#include <QRegularExpression>
#include <QVector>

LocaleConfiguration::LocaleConfiguration()
: explicit_lang( false )
Expand All @@ -40,107 +42,114 @@ LocaleConfiguration::setLanguage( const QString& localeName )
m_lang = localeName;
}


LocaleConfiguration
LocaleConfiguration::fromLanguageAndLocation( const QString& languageLocale,
const QStringList& availableLocales,
const QString& countryCode )
static LocaleNameParts
updateCountry( LocaleNameParts p, const QString& country )
{
cDebug() << "Mapping" << languageLocale << "in" << countryCode << "to locale.";
QString language = languageLocale.split( '_' ).first();
QString region;
if ( language.contains( '@' ) )
{
auto r = language.split( '@' );
language = r.first();
region = r[ 1 ]; // second()
}

// Either an exact match, or the whole language part matches
// (followed by .<encoding> or _<country>
QStringList linesForLanguage = availableLocales.filter( QRegularExpression( language + "[._]" ) );
cDebug() << Logger::SubEntry << "Matching" << linesForLanguage;
p.country = country;
return p;
}

QString lang;
if ( linesForLanguage.isEmpty() || languageLocale.isEmpty() )
static QPair< int, LocaleNameParts >
identifyBestLanguageMatch( const LocaleNameParts& referenceLocale, QVector< LocaleNameParts >& others )
{
std::sort( others.begin(),
others.end(),
[ & ]( const LocaleNameParts& lhs, const LocaleNameParts& rhs )
{ return referenceLocale.similarity( lhs ) < referenceLocale.similarity( rhs ); } );
// The best match is at the end
LocaleNameParts best_match = others.last();
if ( !( referenceLocale.similarity( best_match ) > LocaleNameParts::no_match ) )
{
lang = "en_US.UTF-8";
cDebug() << Logger::SubEntry << "Got no good match for" << referenceLocale.name();
return { LocaleNameParts::no_match, LocaleNameParts {} };
}
else if ( linesForLanguage.length() == 1 )
else
{
lang = linesForLanguage.first();
cDebug() << Logger::SubEntry << "Got best match for" << referenceLocale.name() << "as" << best_match.name();
return { referenceLocale.similarity( best_match ), best_match };
}
}

// lang could still be empty if we found multiple locales that satisfy myLanguage
const QString combinedLanguageAndCountry = QString( "%1_%2" ).arg( language ).arg( countryCode );
if ( lang.isEmpty() && region.isEmpty() )
{
auto l = linesForLanguage.filter(
QRegularExpression( combinedLanguageAndCountry + "[._]" ) ); // no regional variants
if ( l.length() == 1 )
{
lang = l.first();
}
}
/** @brief Returns the QString from @p availableLocales that best-matches.
*/
static LocaleNameParts
identifyBestLanguageMatch( const QString& languageLocale,
const QStringList& availableLocales,
const QString& countryCode )
{
const QString default_lang = QStringLiteral( "en_US.UTF-8" );

// The following block was inspired by Ubiquity, scripts/localechooser-apply.
// No copyright statement found in file, assuming GPL v2 or later.
/* # In the special cases of Portuguese and Chinese, selecting a
# different location may imply a different dialect of the language.
# In such cases, make LANG reflect the selected language (for
# messages, character types, and collation) and make the other
# locale categories reflect the selected location. */
if ( language == "pt" || language == "zh" )
const LocaleNameParts self = LocaleNameParts::fromName( languageLocale );
if ( self.isValid() && !availableLocales.isEmpty() )
{
cDebug() << Logger::SubEntry << "Special-case Portuguese and Chinese";
QString proposedLocale = QString( "%1_%2" ).arg( language ).arg( countryCode );
for ( const QString& line : linesForLanguage )
QVector< LocaleNameParts > others;
others.resize( availableLocales.length() ); // Makes default structs
std::transform( availableLocales.begin(), availableLocales.end(), others.begin(), LocaleNameParts::fromName );

// Keep track of the best match in various attempts
int best_score = LocaleNameParts::no_match;
LocaleNameParts best_match;

// Check with the unmodified language setting
{
if ( line.contains( proposedLocale ) )
auto [ score, match ] = identifyBestLanguageMatch( self, others );
if ( score >= LocaleNameParts::complete_match )
{
return match;
}
else if ( score > best_score )
{
cDebug() << Logger::SubEntry << "Country-variant" << line << "chosen.";
lang = line;
break;
best_match = match;
}
}
}
if ( lang.isEmpty() && !region.isEmpty() )
{
cDebug() << Logger::SubEntry << "Special-case region @" << region;
QString proposedRegion = QString( "@%1" ).arg( region );
for ( const QString& line : linesForLanguage )


// .. but it might match **better** with the chosen location country Code
{
if ( line.startsWith( language ) && line.contains( proposedRegion ) )
auto [ score, match ] = identifyBestLanguageMatch( updateCountry( self, countryCode ), others );
if ( score >= LocaleNameParts::complete_match )
{
return match;
}
else if ( score > best_score )
{
cDebug() << Logger::SubEntry << "Region-variant" << line << "chosen.";
lang = line;
break;
best_match = match;
}
}
}


// If we found no good way to set a default lang, do a search with the whole
// language locale and pick the first result, if any.
if ( lang.isEmpty() )
{
for ( const QString& line : availableLocales )
// .. or better yet with the QLocale-derived country
{
if ( line.startsWith( languageLocale ) )
const QString localeCountry = LocaleNameParts::fromName( QLocale( languageLocale ).name() ).country;
auto [ score, match ] = identifyBestLanguageMatch( updateCountry( self, localeCountry ), others );
if ( score >= LocaleNameParts::complete_match )
{
return match;
}
else if ( score > best_score )
{
lang = line;
break;
best_match = match;
}
}

if ( best_match.isValid() )
{
cDebug() << Logger::SubEntry << "Matched best with" << best_match.name();
return best_match;
}
}

// Else we have an unrecognized or unsupported locale, all we can do is go with
// en_US.UTF-8 UTF-8. This completes all default language setting guesswork.
if ( lang.isEmpty() )
{
lang = "en_US.UTF-8";
}
return LocaleNameParts::fromName( default_lang );
}

LocaleConfiguration
LocaleConfiguration::fromLanguageAndLocation( const QString& languageLocale,
const QStringList& availableLocales,
const QString& countryCode )
{
cDebug() << "Mapping" << languageLocale << "in" << countryCode << "to locale.";
const auto bestLocale = identifyBestLanguageMatch( languageLocale, availableLocales, countryCode );

// The following block was inspired by Ubiquity, scripts/localechooser-apply.
// No copyright statement found in file, assuming GPL v2 or later.
Expand Down Expand Up @@ -188,34 +197,16 @@ LocaleConfiguration::fromLanguageAndLocation( const QString& languageLocale,
// We make a proposed locale based on the UI language and the timezone's country. There is no
// guarantee that this will be a valid, supported locale (often it won't).
QString lc_formats;
const QString combined = QString( "%1_%2" ).arg( language ).arg( countryCode );
if ( lang.isEmpty() )
const QString combined = QString( "%1_%2" ).arg( bestLocale.language ).arg( countryCode );
if ( availableLocales.contains( bestLocale.language ) )
{
cDebug() << Logger::SubEntry << "Looking up formats for" << combinedLanguageAndCountry;
// We look up if it's a supported locale.
for ( const QString& line : availableLocales )
{
if ( line.startsWith( combinedLanguageAndCountry ) )
{
lang = line;
lc_formats = line;
break;
}
}
cDebug() << Logger::SubEntry << "Exact formats match for language tag" << bestLocale.language;
lc_formats = bestLocale.language;
}
else
else if ( availableLocales.contains( combined ) )
{
if ( availableLocales.contains( lang ) )
{
cDebug() << Logger::SubEntry << "Exact formats match for language tag" << lang;
lc_formats = lang;
}
else if ( availableLocales.contains( combinedLanguageAndCountry ) )
{
cDebug() << Logger::SubEntry << "Exact formats match for combined" << combinedLanguageAndCountry;
lang = combinedLanguageAndCountry;
lc_formats = combinedLanguageAndCountry;
}
cDebug() << Logger::SubEntry << "Exact formats match for combined" << combined;
lc_formats = combined;
}

if ( lc_formats.isEmpty() )
Expand Down Expand Up @@ -303,12 +294,7 @@ LocaleConfiguration::fromLanguageAndLocation( const QString& languageLocale,

// If we cannot make a good choice for a given country we go with the LANG
// setting, which defaults to en_US.UTF-8 UTF-8 if all else fails.
if ( lc_formats.isEmpty() )
{
lc_formats = lang;
}

return LocaleConfiguration( lang, lc_formats );
return LocaleConfiguration( bestLocale.name(), lc_formats.isEmpty() ? bestLocale.name() : lc_formats );
}


Expand Down
90 changes: 90 additions & 0 deletions src/modules/locale/LocaleNames.cpp
@@ -0,0 +1,90 @@
/* === This file is part of Calamares - <https://calamares.io> ===
*
* SPDX-FileCopyrightText: 2022 Adriaan de Groot <groot@kde.org>
* SPDX-License-Identifier: GPL-3.0-or-later
*
* Calamares is Free Software: see the License-Identifier above.
*
*/

#include "LocaleNames.h"

#include "utils/Logger.h"

#include <QRegularExpression>

LocaleNameParts
LocaleNameParts::fromName( const QString& name )
{
auto requireAndRemoveLeadingChar = []( QChar c, QString s )
{
if ( s.startsWith( c ) )
{
return s.remove( 0, 1 );
}
else
{
return QString();
}
};

auto parts = QRegularExpression( "^([a-zA-Z]+)(_[a-zA-Z]+)?(\\.[-a-zA-Z0-9]+)?(@[a-zA-Z]+)?" ).match( name );
const QString calamaresLanguage = parts.captured( 1 );
const QString calamaresCountry = requireAndRemoveLeadingChar( '_', parts.captured( 2 ) );
const QString calamaresEncoding = requireAndRemoveLeadingChar( '.', parts.captured( 3 ) );
const QString calamaresRegion = requireAndRemoveLeadingChar( '@', parts.captured( 4 ) );

if ( calamaresLanguage.isEmpty() )
{
return LocaleNameParts {};
}
else
{
return LocaleNameParts { calamaresLanguage, calamaresCountry, calamaresRegion, calamaresEncoding };
}
}

QString
LocaleNameParts::name() const
{
// We don't want QStringView to a temporary; force conversion
auto insertLeadingChar = []( QChar c, QString s ) -> QString
{
if ( s.isEmpty() )
{
return QString();
}
else
{
return c + s;
}
};

if ( !isValid() )
{
return QString();
}
else
{
return language + insertLeadingChar( '_', country ) + insertLeadingChar( '.', encoding )
+ insertLeadingChar( '@', region );
}
}


int
LocaleNameParts::similarity( const LocaleNameParts& other ) const
{
if ( !isValid() || !other.isValid() )
{
return 0;
}
if ( language != other.language )
{
return 0;
}
const auto matched_region = ( region == other.region ? 30 : 0 );
const auto matched_country = ( country == other.country ? ( country.isEmpty() ? 10 : 20 ) : 0 );
const auto no_other_country_given = ( ( country != other.country && other.country.isEmpty() ) ? 10 : 0 );
return 50 + matched_region + matched_country + no_other_country_given;
}

0 comments on commit f277cbb

Please sign in to comment.