Pantrix UI

Bir dilin alfabesine göre sıralama

Compose Multiplatform'un ortak kodunda bir ad listesinin neden dilin alfabesine göre sıralanmadığı, bunun için kütüphanenin ne verdiği ve nasıl sınandığı. Sorun Pantrix'e özgü değildir: ortak kodunda sorted() ya da sortedBy { it.name } ile ad, şehir, başlık sıralayan her Kotlin Multiplatform projesinde aynı şekilde görülür.

Burada söylenenlerin hangisinin bu depoda koşan bir testte görüldüğü ve hangisinin doğrulanmadığı 7. bölümde ayrı ayrı yazılıdır.

1. Sorun

Belirti

listOf("zeytin", "şeker", "öğle", "iğne", "ılık", "çilek", "Çay", "cam").sorted()
// [cam, iğne, zeytin, Çay, çilek, öğle, ılık, şeker]

Neden

Kotlin'de iki yazının karşılaştırılması (String.compareTo) harflerin Unicode'daki sayılarına bakar. Bu, her platformda aynı sonucu veren, hızlı ve hiçbir dilin alfabesi olmayan bir sıradır: büyük harflerin sayıları küçük harflerinkinden küçüktür, ç (231), ö (246), ş (351) ve ı (305) de z'den (122) büyüktür.

Bir dilin alfabesine göre sıralamak "collation" denen ayrı bir iştir ve Kotlin'in ortak kütüphanesinde karşılığı yoktur. Her platformun kendi aracı vardır:

PlatformAraçOrtak koddan erişilir mi
Androidjava.text.Collator (arkasında ICU)Hayır, androidMain
Masaüstü (JVM)java.text.CollatorHayır, jvmMain
iOSNSString.compare(_:options:range:locale:)Hayır, iosMain
Web (js, wasmJs)Intl.CollatorHayır, webMain

Kısa yoldan gidilen iki çözüm de yetmez:

  1. lowercase() ile karşılaştırmak. Büyük küçük harf sorununu çözer, alfabeyi çözmez: ç yine z'den sonradır.

  2. String.CASE_INSENSITIVE_ORDER. JVM'de İ ile i'yi eşit sayar, JS'te, Wasm'da ve iOS'ta saymaz: aynı liste masaüstünde ve tarayıcıda başka sıralanır.

2. Çözüm

Her platformun kendi aracını tek bir Comparator<String> arkasına almak: ortak kodda bir expect, dört kaynak kümesinde karşılığı.

Üç karar her platformda aynı tutulur ki liste platformdan platforma değişmesin:

3. Pantrix ile

PantrixLocale.textOrder() o dilin karşılaştırıcısını verir; rememberTextOrder() bir bileşimde temanın dilininkini tutar.

// Ortak kodda, bileşimin dışında
val order = PantrixLocale.Turkish.textOrder()
val sorted = customers.sortedWith(compareBy(order) { it.name })

// Bir bileşimde: temanın diliyle, dil değişmedikçe aynı nesne
val order = rememberTextOrder()
val sorted = remember(customers, order) { customers.sortedWith(compareBy(order) { it.name }) }

PantrixDataTable'ın yazı kolonu onu doğrudan alır:

val order = rememberTextOrder()
val columns = remember(order) {
    listOf(
        DataTableColumn.text<Customer>(key = "name", header = name, sortable = true, textOrder = order) { it.name }
    )
}
PantrixDataTable(rows = remember(customers, sort) { customers.sortedWith(sort.comparator(columns)) }, columns = columns)

textOrder verilmeyen yazı kolonu eskisi gibi, harflerin sayılarıyla ve büyük küçük harf ayırmadan sıralar.

Karşılaştırıcıyı sakla. Bir karşılaştırıcı kurmak bedava değildir ve bir sıralama ona binlerce kez sorar. Bileşimin dışında bir alanda, içinde rememberTextOrder() ile tutulur; her karşılaştırmada yeniden kurulmaz.

4. Pantrix olmadan

Dört dosya. Ortak kodda:

// commonMain
expect fun textOrder(languageTag: String): Comparator<String>

Android'de ve masaüstünde (iki kaynak kümesinde aynı dosya):

// androidMain, jvmMain
actual fun textOrder(languageTag: String): Comparator<String> {
    val collator = Collator.getInstance(Locale.forLanguageTag(languageTag))
    collator.strength = Collator.SECONDARY // aksanı ayırt eder, büyük küçük harfi etmez
    return Comparator { first, second -> collator.compare(first, second) }
}

iOS'ta:

// iosMain
@OptIn(ExperimentalForeignApi::class)
actual fun textOrder(languageTag: String): Comparator<String> {
    val locale = NSLocale(localeIdentifier = languageTag)
    return Comparator { first, second ->
        val whole = NSMakeRange(0u, first.length.toULong())
        @Suppress("CAST_NEVER_SUCCEEDS")
        (first as NSString).compare(second, options = NSCaseInsensitiveSearch, range = whole, locale = locale).toInt()
    }
}

Web'de, js ile wasmJs'in ortak kaynak kümesinde:

// webMain
actual fun textOrder(languageTag: String): Comparator<String> =
    Comparator { first, second -> compareInLanguage(first, second, languageTag) }

@OptIn(ExperimentalWasmJsInterop::class)
private fun compareInLanguage(first: String, second: String, languageTag: String): Int = js(
    """(function () {
        try {
            var kept = globalThis.__textOrders || (globalThis.__textOrders = {});
            var order = kept[languageTag] ||
                (kept[languageTag] = new Intl.Collator(languageTag, { sensitivity: 'accent' }));
            return order.compare(first, second);
        } catch (refused) {
            return first < second ? -1 : (first > second ? 1 : 0);
        }
    })()"""
)

Web'deki iki ayrıntı:

5. Yardımcının yapmadıkları

6. Test etmek

Sınanacak olan, platformun aracının gerçekten o dilin kurallarını bildiğidir; bu yüzden test her hedefte koşmalıdır. Türkçe iyi bir sınavdır: ç, ğ, ı, ö, ş, ü harflerinin altısı da harflerin sayılarıyla yanlış yere düşer.

@Test
fun turkish_texts_are_in_the_order_of_the_turkish_alphabet() {
    val words = listOf("zeytin", "şeker", "öğle", "iğne", "ılık", "çilek", "Çay", "cam")
    assertEquals(
        listOf("cam", "Çay", "çilek", "ılık", "iğne", "öğle", "şeker", "zeytin"),
        words.sortedWith(PantrixLocale.Turkish.textOrder())
    )
}

Büyük küçük harf eşitliğini Türkçe "I" ve "ı" ile sınamayın: platformların büyük küçük harf eşlemesi o çiftte ayrılabilir. "Çay" ile "çay" her platformda eşittir.

7. Doğrulama

Bu depoda koşan testlerde görülenler (2026-10-11, TextOrderTest, TextOrderOnAndroidTest):

NeJVMiOS simülatörüJS (başsız Chrome)Wasm (başsız Chrome)Android (ana makine)
Sekiz Türkçe sözcük alfabe sırasında✅✅✅✅✅
Türk alfabesinin 29 harfi sırayla✅✅✅✅
"ada" ile "Ada" eşit, liste kararlı✅✅✅✅✅
"resume", "résumé"den önce✅✅✅✅✅
Tanınmayan etiket hata vermiyor✅✅✅✅✅

Doğrulanmayanlar:

8. Bu depoda nerede

DosyaNe
ui/foundation/src/commonMain/kotlin/com/pantrixui/foundation/l10n/TextOrder.ktPantrixLocale.textOrder(), rememberTextOrder()
ui/foundation/src/{androidMain,jvmMain,iosMain,webMain}/kotlin/com/pantrixui/foundation/l10n/TextOrder.*.ktPlatformların karşılıkları
ui/foundation/src/commonTest/kotlin/com/pantrixui/foundation/l10n/TextOrderTest.ktDört hedefte koşan testler
ui/foundation/src/androidHostTest/kotlin/com/pantrixui/foundation/l10n/TextOrderOnAndroidTest.ktAndroid'in kaynak kümesi
ui/components/src/commonMain/kotlin/com/pantrixui/components/data/datatable/DataTableColumn.ktDataTableColumn.text(textOrder = …)