Show how a word is pronounced, Classical Persian first
Wiktionary carries pronunciation for 45,000 of these headwords and the build was throwing it away. It is now kept: 101,306 entries of IPA, each tagged with the variety it belongs to, which matters here because a word in a 14th-century ghazal was not said the way Tehran says it now — عشق is /ˈʔiʃq/ in Classical Persian and [ʔeʃɢ̥] in Iran today, and the app leads with the former. The IPA's dots and stress marks are the syllable breakdown. Urdu Wiktionary adds its own, in Urdu script: the fully vowelled spelling عِشْق and the split عِش + قوں, both pulled out of its wikitext. Costs 7.7 MB of database, taking it to 94 MB. Verified on an API 36 emulator: tapping عشق shows the Classical Persian IPA, the Urdu syllable split, the vowelled spelling and two modern variants above the definitions. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
b16d020ce0
commit
0df736fd89
5 files changed
+92
-4
No files matched your search
Binary file not shown.
@@ -11,13 +11,19 @@ import java.text.Normalizer
|
||||
/** One definition, and where it came from, so the credit stays attached to the text. */
|
||||
data class Definition(val word: String, val gloss: String, val source: String)
|
||||
|
||||
/**
|
||||
* How a word sounds. [label] is the variety it belongs to — Classical Persian, Dari, Standard
|
||||
* Urdu — because a word in a 14th-century ghazal was not said the way Tehran says it now.
|
||||
*/
|
||||
data class Pronunciation(val text: String, val label: String)
|
||||
|
||||
/**
|
||||
* Word lookup over a bundled SQLite built from Wiktionary (CC BY-SA 3.0) and Daneshjoo.
|
||||
* See `tools/build_dictionary.py`; the asset ships gzipped and is unpacked once on first use.
|
||||
*/
|
||||
object Dictionary {
|
||||
/** Guards against a copy interrupted half-way leaving an unopenable file behind. */
|
||||
private const val ASSET_BYTES = 86663168L
|
||||
private const val ASSET_BYTES = 94384128L
|
||||
|
||||
// Not a .gz: the build packager silently gunzips those and drops the extension, which left
|
||||
// the asset under a different name than the code was opening.
|
||||
@@ -46,6 +52,37 @@ object Dictionary {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* How [raw] is pronounced, Classical Persian first: this is an app for poetry written long
|
||||
* before modern Tehrani vowels, and the IPA's dots and stress marks are the syllable
|
||||
* breakdown that goes with it.
|
||||
*/
|
||||
suspend fun pronunciations(raw: String, limit: Int = 5): List<Pronunciation> =
|
||||
withContext(Dispatchers.IO) {
|
||||
val database = open() ?: return@withContext emptyList()
|
||||
val word = normalise(raw).takeIf { it.isNotEmpty() } ?: return@withContext emptyList()
|
||||
database.rawQuery(
|
||||
"""
|
||||
SELECT text, label FROM pron WHERE word = ?
|
||||
ORDER BY CASE
|
||||
WHEN label LIKE 'Classical%' THEN 0
|
||||
WHEN source = 'urwiktionary' THEN 1
|
||||
WHEN source = 'wiktionary-fa' THEN 2
|
||||
WHEN source = 'wiktionary-ur' THEN 3
|
||||
ELSE 4
|
||||
END
|
||||
LIMIT ?
|
||||
""",
|
||||
arrayOf(word, limit.toString()),
|
||||
).use { cursor ->
|
||||
buildList {
|
||||
while (cursor.moveToNext()) {
|
||||
add(Pronunciation(cursor.getString(0), cursor.getString(1)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Looks a word up, widening the search until something matches:
|
||||
*
|
||||
|
||||
@@ -30,6 +30,7 @@ import androidx.compose.ui.unit.dp
|
||||
import com.ganjoor.android.R
|
||||
import com.ganjoor.android.data.Definition
|
||||
import com.ganjoor.android.data.Dictionary
|
||||
import com.ganjoor.android.data.Pronunciation
|
||||
import com.ganjoor.android.ui.theme.readingStyle
|
||||
|
||||
/** English prose inside an otherwise right-to-left sheet. */
|
||||
@@ -69,6 +70,9 @@ fun WordSheet(word: String, onDismiss: () -> Unit) {
|
||||
val definitions by produceState<List<Definition>?>(null, current) {
|
||||
value = Dictionary.lookup(current)
|
||||
}
|
||||
val sounds by produceState(emptyList<Pronunciation>(), current) {
|
||||
value = Dictionary.pronunciations(current)
|
||||
}
|
||||
val suggestions by produceState(emptyList<String>(), current, definitions) {
|
||||
value = if (definitions?.isEmpty() == true) Dictionary.suggest(current) else emptyList()
|
||||
}
|
||||
@@ -88,6 +92,25 @@ fun WordSheet(word: String, onDismiss: () -> Unit) {
|
||||
style = readingStyle(prefs.font, prefs.fontSize, prefs.fontWeight.weight),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
if (sounds.isNotEmpty()) {
|
||||
FlowRow(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
sounds.forEach { sound ->
|
||||
Column {
|
||||
// IPA is Latin-script, so it reads left to right whatever the UI does
|
||||
LeftToRight {
|
||||
Text(sound.text, style = MaterialTheme.typography.bodyMedium)
|
||||
}
|
||||
if (sound.label.isNotBlank()) {
|
||||
Text(
|
||||
text = sound.label,
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
HorizontalDivider()
|
||||
|
||||
when {
|
||||
|
||||
Reference in new issue
Block a user