diff --git a/src/modules/locale/LocaleNames.cpp b/src/modules/locale/LocaleNames.cpp
index 148d214725cf841c0ad56370134be94ba13a8d31..93e8444461d51a1ec8ecf291f2b36f14c3089dd8 100644
--- a/src/modules/locale/LocaleNames.cpp
+++ b/src/modules/locale/LocaleNames.cpp
@@ -70,3 +70,21 @@ LocaleNameParts::name() const
             + 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 ? 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;
+}
diff --git a/src/modules/locale/LocaleNames.h b/src/modules/locale/LocaleNames.h
index 976ee20b3a5fe0231e3629c327649c1ed07657fe..247a8496c2065fb587696cbe65097ffb47d4f93b 100644
--- a/src/modules/locale/LocaleNames.h
+++ b/src/modules/locale/LocaleNames.h
@@ -30,6 +30,14 @@ struct LocaleNameParts
     QString name() const;
 
     static LocaleNameParts fromName( const QString& name );
+
+    /** @brief Compute similarity-score with another locale-name.
+     *
+     * Similarity is driven by language and region, then country.
+     * Returns a number between 0 (no similarity, e.g. the
+     * language is different) and 100 (complete match).
+     */
+    int similarity( const LocaleNameParts& other ) const;
 };
 
 #endif
diff --git a/src/modules/locale/Tests.cpp b/src/modules/locale/Tests.cpp
index 85ec340262fdf87ae6a4b5145b9b323e14a9a29b..fda58059acd84700ebb8e798013f4fcce1fc2c64 100644
--- a/src/modules/locale/Tests.cpp
+++ b/src/modules/locale/Tests.cpp
@@ -59,6 +59,7 @@ private Q_SLOTS:
     void testLanguageMappingNeon();
     void testLanguageMappingFreeBSD_data();
     void testLanguageMappingFreeBSD();
+    void testLanguageSimilarity();
 
 private:
     QStringList m_KDEneonLocales;
@@ -536,6 +537,45 @@ LocaleTests::testLocaleNameParts()
     }
 }
 
+void
+LocaleTests::testLanguageSimilarity()
+{
+    // Empty
+    {
+        QCOMPARE( LocaleNameParts().similarity( LocaleNameParts() ), 0 );
+    }
+    // Some simple Dutch situations
+    {
+        auto nl_parts = LocaleNameParts::fromName( QStringLiteral( "nl_NL.UTF-8" ) );
+        auto be_parts = LocaleNameParts::fromName( QStringLiteral( "nl_BE.UTF-8" ) );
+        auto nl_short_parts = LocaleNameParts::fromName( QStringLiteral( "nl" ) );
+        QCOMPARE( nl_parts.similarity( nl_parts ), 100 );
+        QCOMPARE( nl_parts.similarity( LocaleNameParts() ), 0 );
+        QCOMPARE( nl_parts.similarity( be_parts ), 80 );  // Language + (empty) region match
+        QCOMPARE( nl_parts.similarity( nl_short_parts ), 90 );
+    }
+
+    // Everything matches itself
+    {
+        if ( m_KDEneonLocales.isEmpty() )
+        {
+            testKDENeonLanguageData();
+        }
+        QVERIFY( !m_FreeBSDLocales.isEmpty() );
+        QVERIFY( !m_KDEneonLocales.isEmpty() );
+        for ( const auto& l : m_KDEneonLocales )
+        {
+            auto locale_name = LocaleNameParts::fromName( l );
+            auto self_similarity = locale_name.similarity( locale_name );
+            if ( self_similarity != 100 )
+            {
+                cDebug() << "Locale" << l << "is unusual.";
+            }
+            QCOMPARE( self_similarity, 100 );
+        }
+    }
+}
+
 
 #include "utils/moc-warnings.h"