implement vocabulary packs exploration and request system

This commit is contained in:
jonasgaudian
2026-02-19 13:01:55 +01:00
parent 0f8d605df7
commit b75f5f32a0
17 changed files with 1784 additions and 298 deletions

View File

@@ -0,0 +1,24 @@
All vocabulary lists in this section were generated using AI. While I strive for accuracy and quality, please keep in mind that these are machine-generated collections.
I'm a single developer building and maintaining this app in my spare time. I'm passionate about creating tools that help people learn languages, but I have limited resources and time.
### Your Feedback Matters
I greatly appreciate any feedback, suggestions, or ideas you might have! If you:
- Find errors in any vocabulary pack
- Have ideas for new topics, languages, or categories
- Want to request a specific vocabulary pack
- Have suggestions for improving existing packs
Please don't hesitate to reach out through the Request feature or contact me directly. Your input helps make this app better for everyone!
### How Packs Work
- **Download** packs that interest you
- **Preview** the words before adding them
- **Import** them into your library with options to handle duplicates
- **Organize** them into categories which are created automatically
Thank you for using this app and your feedback!

View File

@@ -49,7 +49,7 @@ fun AppTopAppBar(
navigationIcon: @Composable (() -> Unit)? = null,
actions: @Composable RowScope.() -> Unit = {},
colors: TopAppBarColors = TopAppBarDefaults.topAppBarColors(),
hintContent: Hint? = null
hint: Hint? = null
) {
val sheetState = rememberModalBottomSheetState()
var showBottomSheet by remember { mutableStateOf(false) }
@@ -61,7 +61,7 @@ fun AppTopAppBar(
colors = colors,
title = {
val showHints = LocalShowHints.current
if (showHints && hintContent != null) {
if (showHints && hint != null) {
// Simplified row: keeps the title and hint icon neatly centered together
Row(
verticalAlignment = Alignment.CenterVertically,
@@ -114,7 +114,7 @@ fun AppTopAppBar(
)
if (showBottomSheet) {
hintContent?.let {
hint?.let {
HintBottomSheet(
onDismissRequest = {
@Suppress("AssignedValueIsNeverRead")

View File

@@ -0,0 +1,167 @@
package eu.gaudian.translator.view.dialogs
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.core.net.toUri
import eu.gaudian.translator.R
import eu.gaudian.translator.ui.theme.ThemePreviews
import eu.gaudian.translator.view.composable.AppDialog
import eu.gaudian.translator.view.composable.AppSlider
import kotlin.math.roundToInt
@Composable
fun RequestMorePackDialog(
onDismiss: () -> Unit,
) {
val context = LocalContext.current
var topic by remember { mutableStateOf("") }
var langFrom by remember { mutableStateOf("") }
var langTo by remember { mutableStateOf("") }
var amount by remember { mutableFloatStateOf(50f) }
AppDialog(
onDismissRequest = onDismiss,
title = { Text("Request a Pack", fontWeight = FontWeight.Bold) },
) {
Column(
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
Text(
stringResource(R.string.text_request_pack_desc),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Text(
stringResource(R.string.label_topic),
style = MaterialTheme.typography.labelLarge,
fontWeight = FontWeight.SemiBold
)
OutlinedTextField(
value = topic,
onValueChange = { topic = it },
placeholder = { Text("e.g. Travel, Business, Cooking…") },
singleLine = true,
modifier = Modifier.fillMaxWidth()
)
Row(
verticalAlignment = Alignment.Bottom,
horizontalArrangement = Arrangement.spacedBy(6.dp)
) {
Text(
stringResource(R.string.label_languages),
style = MaterialTheme.typography.labelLarge,
fontWeight = FontWeight.SemiBold
)
Text(
stringResource(R.string.label_optional),
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
OutlinedTextField(
value = langFrom,
onValueChange = { langFrom = it },
placeholder = { Text(stringResource(R.string.label_from)) },
label = { Text(stringResource(R.string.label_from)) },
singleLine = true,
modifier = Modifier.weight(1f)
)
OutlinedTextField(
value = langTo,
onValueChange = { langTo = it },
placeholder = { Text(stringResource(R.string.label_to)) },
label = { Text(stringResource(R.string.label_to)) },
singleLine = true,
modifier = Modifier.weight(1f)
)
}
Text(
"Approx. word count: ~${amount.roundToInt()} words",
style = MaterialTheme.typography.labelLarge,
fontWeight = FontWeight.SemiBold
)
AppSlider(
value = amount,
onValueChange = { amount = it },
valueRange = 10f..200f,
steps = 18,
modifier = Modifier.fillMaxWidth()
)
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.End,
verticalAlignment = Alignment.CenterVertically
) {
TextButton(onClick = onDismiss) { Text(stringResource(R.string.label_cancel)) }
TextButton(
enabled = topic.isNotBlank(),
onClick = {
val subject = "Polly Pack Request $topic"
val langPart = buildString {
val from = langFrom.trim()
val to = langTo.trim()
if (from.isNotBlank() || to.isNotBlank()) {
append("Languages: ${from.ifBlank { "?" }} → ${to.ifBlank { "?" }}\n")
}
}
val body = buildString {
appendLine("Hey Jonas,")
appendLine()
appendLine("Please add the following vocabulary pack to Polly:")
appendLine()
appendLine("Topic: $topic")
if (langPart.isNotBlank()) append(langPart)
appendLine("Word count: ~${amount.roundToInt()} words")
appendLine()
appendLine("Thank you!")
}
val intent = android.content.Intent(android.content.Intent.ACTION_SENDTO).apply {
data = "mailto:play@gaudian.eu".toUri()
putExtra(android.content.Intent.EXTRA_EMAIL, arrayOf("play@gaudian.eu"))
putExtra(android.content.Intent.EXTRA_SUBJECT, subject)
putExtra(android.content.Intent.EXTRA_TEXT, body)
}
context.startActivity(intent)
onDismiss()
}
) {
Text(
stringResource(R.string.label_send_request),
fontWeight = FontWeight.Bold
)
}
}
}
}
}
@ThemePreviews
@Composable
fun RequestMorePackDialogPreview() {
RequestMorePackDialog(
onDismiss = {}
)
}

View File

@@ -63,7 +63,7 @@ fun VocabularyReviewScreen(
topBar = {
AppTopAppBar(
title = stringResource(R.string.found_items),
hintContent = HintDefinition.REVIEW.hint()
hint = HintDefinition.REVIEW.hint()
)
},
) { paddingValues ->

View File

@@ -26,7 +26,8 @@ enum class HintDefinition(
REVIEW("review_hint", R.string.review_intro),
SORTING("sorting_hint", R.string.sorting_hint_title),
TRANSLATION("translation_hint", R.string.hint_translate_how_it_works),
VOCABULARY_PROGRESS("vocabulary_progress_hint", R.string.hint_vocabulary_progress_hint_title);
VOCABULARY_PROGRESS("vocabulary_progress_hint", R.string.hint_vocabulary_progress_hint_title),
EXPLORE_PACKS("explore_packs_hint", R.string.hint_explore_packs_title);
/** Creates the Hint data class for this hint definition. */
@Composable

View File

@@ -136,7 +136,7 @@ fun AddModelScreen(navController: NavController, providerKey: String) {
AppTopAppBar(
title = providerName,
onNavigateBack = { navController.popBackStack() },
hintContent = HintDefinition.ADD_MODEL_SCAN.hint()
hint = HintDefinition.ADD_MODEL_SCAN.hint()
)
},
) { paddingValues ->

View File

@@ -117,7 +117,7 @@ fun ApiKeyScreen(navController: NavController) {
AppTopAppBar(
title = stringResource(R.string.label_ai_configuration),
onNavigateBack = { navController.popBackStack() },
hintContent = HintDefinition.API_KEY.hint()
hint = HintDefinition.API_KEY.hint()
)
}
) { paddingValues ->

View File

@@ -53,7 +53,7 @@ fun CustomVocabularyPromptScreen(
AppTopAppBar(
title = stringResource(R.string.text_vocabulary_prompt),
onNavigateBack = { navController.popBackStack() },
hintContent = null //TODO: Add hint
hint = null //TODO: Add hint
)
}

View File

@@ -64,7 +64,7 @@ fun DictionaryOptionsScreen(
AppTopAppBar(
title = stringResource(R.string.label_dictionary_options),
onNavigateBack = { navController.popBackStack() },
hintContent = HintDefinition.DICTIONARY_OPTIONS.hint()
hint = HintDefinition.DICTIONARY_OPTIONS.hint()
)
}
) { paddingValues ->

View File

@@ -62,7 +62,7 @@ fun TranslationSettingsScreen(
AppTopAppBar(
title = stringResource(R.string.label_translation_settings),
onNavigateBack = { navController.popBackStack() },
hintContent = null //TODO add hint
hint = null //TODO add hint
)
}
) { paddingValues ->

View File

@@ -79,7 +79,7 @@ fun VocabularyProgressOptionsScreen(
AppTopAppBar(
title = stringResource(R.string.label_vocabulary_settings),
onNavigateBack = { navController.popBackStack() },
hintContent = HintDefinition.VOCABULARY_PROGRESS.hint()
hint = HintDefinition.VOCABULARY_PROGRESS.hint()
)
}
) { paddingValues ->

View File

@@ -1,5 +1,3 @@
@file:Suppress("HardCodedStringLiteral")
package eu.gaudian.translator.view.vocabulary
import androidx.compose.foundation.background
@@ -20,18 +18,17 @@ import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.GridItemSpan
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowForwardIos
import androidx.compose.material.icons.filled.CheckCircle
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.FilterList
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material.icons.filled.Search
import androidx.compose.material.icons.filled.Warning
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Card
@@ -40,6 +37,7 @@ import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FilterChip
import androidx.compose.material3.FilterChipDefaults
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.LinearProgressIndicator
@@ -52,6 +50,7 @@ import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
@@ -64,18 +63,25 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.navigation.NavHostController
import eu.gaudian.translator.R
import eu.gaudian.translator.model.ConflictStrategy
import eu.gaudian.translator.model.VocabularyItem
import eu.gaudian.translator.utils.Log
import eu.gaudian.translator.utils.StatusMessageId
import eu.gaudian.translator.utils.StatusMessageService
import eu.gaudian.translator.utils.findActivity
import eu.gaudian.translator.view.composable.AppAlertDialog
import eu.gaudian.translator.view.composable.AppDialog
import eu.gaudian.translator.view.composable.AppTopAppBar
import eu.gaudian.translator.view.dialogs.RequestMorePackDialog
import eu.gaudian.translator.view.hints.HintDefinition
import eu.gaudian.translator.view.translation.LanguageSelectorBar
import eu.gaudian.translator.viewmodel.ExportImportViewModel
import eu.gaudian.translator.viewmodel.ImportState
@@ -83,6 +89,7 @@ import eu.gaudian.translator.viewmodel.LanguageViewModel
import eu.gaudian.translator.viewmodel.PackDownloadState
import eu.gaudian.translator.viewmodel.PackUiState
import eu.gaudian.translator.viewmodel.VocabPacksViewModel
import kotlin.math.abs
private const val TAG = "ExplorePacksScreen"
@@ -94,17 +101,16 @@ enum class PackFilter {
All, Newest,
A1, A2, B1, B2, C1, C2;
val label: String
get() = when (this) {
All -> "All"
Newest -> "Newest"
A1 -> "Beginner · A1"
A2 -> "Elementary · A2"
B1 -> "Intermediate · B1"
B2 -> "Upper Int. · B2"
C1 -> "Advanced · C1"
C2 -> "Proficient · C2"
}
fun getLabel(context: android.content.Context): String = when (this) {
All -> context.getString(R.string.filter_all)
Newest -> context.getString(R.string.filter_newest)
A1 -> context.getString(R.string.filter_a1)
A2 -> context.getString(R.string.filter_a2)
B1 -> context.getString(R.string.filter_b1)
B2 -> context.getString(R.string.filter_b2)
C1 -> context.getString(R.string.filter_c1)
C2 -> context.getString(R.string.filter_c2)
}
val cefrCode: String?
get() = when (this) {
@@ -129,7 +135,7 @@ private val gradientPalette = listOf(
)
private fun gradientForId(id: String): List<Color> =
gradientPalette[Math.abs(id.hashCode()) % gradientPalette.size]
gradientPalette[abs(id.hashCode()) % gradientPalette.size]
// ---------------------------------------------------------------------------
// Screen
@@ -142,6 +148,7 @@ fun ExplorePacksScreen(
modifier: Modifier = Modifier,
) {
val activity = LocalContext.current.findActivity()
val context = LocalContext.current
val vocabPacksViewModel: VocabPacksViewModel = hiltViewModel()
val exportImportViewModel: ExportImportViewModel = hiltViewModel(viewModelStoreOwner = activity)
@@ -162,6 +169,41 @@ fun ExplorePacksScreen(
var showStrategyDialog by remember { mutableStateOf(false) }
var isImporting by remember { mutableStateOf(false) }
// Pack whose ID the user tapped "Get" on once DOWNLOADED, auto-open conflict dialog
var pendingImportPackId by remember { mutableStateOf<String?>(null) }
// Pack opened in the full-screen preview
var previewPack by remember { mutableStateOf<PackUiState?>(null) }
// Request More dialog
var showRequestMoreDialog by remember { mutableStateOf(false) }
// Auto-delete downloaded-but-not-imported files when the screen is closed
DisposableEffect(Unit) {
onDispose {
vocabPacksViewModel.cleanupDownloadedFiles()
}
}
// Auto-open conflict dialog once a queued download finishes
LaunchedEffect(packs, pendingImportPackId) {
val id = pendingImportPackId ?: return@LaunchedEffect
val ps = packs.find { it.info.id == id } ?: return@LaunchedEffect
when (ps.downloadState) {
PackDownloadState.DOWNLOADED -> {
pendingImportPackState = ps
showStrategyDialog = true
pendingImportPackId = null
}
PackDownloadState.ERROR -> { pendingImportPackId = null }
else -> {}
}
}
// Keep previewPack in sync with latest state from packs list
LaunchedEffect(packs) {
val id = previewPack?.info?.id ?: return@LaunchedEffect
previewPack = packs.find { it.info.id == id }
}
// Observe async import result
LaunchedEffect(importState) {
val pending = pendingImportPackState ?: return@LaunchedEffect
@@ -234,16 +276,17 @@ fun ExplorePacksScreen(
) {
// ── Top bar ───────────────────────────────────────────────────────
AppTopAppBar(
title = "Explore Packs",
title = stringResource(R.string.title_explore_packs),
onNavigateBack = { navController.popBackStack() },
actions = {
IconButton(onClick = { vocabPacksViewModel.loadManifest() }) {
Icon(Icons.Default.Refresh, contentDescription = "Reload")
Icon(
Icons.Default.Refresh,
contentDescription = stringResource(R.string.cd_reload)
)
}
IconButton(onClick = { /* TODO: advanced filter sheet */ }) {
Icon(Icons.Default.FilterList, contentDescription = "Filter")
}
}
},
hint = HintDefinition.EXPLORE_PACKS.hint()
)
Spacer(modifier = Modifier.height(12.dp))
@@ -254,7 +297,7 @@ fun ExplorePacksScreen(
onValueChange = { searchQuery = it },
placeholder = {
Text(
"Search topics, phrases",
stringResource(R.string.search_topics_phrases),
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f)
)
},
@@ -301,7 +344,7 @@ fun ExplorePacksScreen(
onClick = { selectedFilter = filter },
label = {
Text(
text = filter.label,
text = filter.getLabel(context),
style = MaterialTheme.typography.labelMedium,
fontWeight = if (selectedFilter == filter) FontWeight.Bold else FontWeight.Normal
)
@@ -323,13 +366,13 @@ fun ExplorePacksScreen(
verticalAlignment = Alignment.CenterVertically
) {
Text(
"Featured Collections",
stringResource(R.string.label_available_collections),
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold
)
if (!isLoadingManifest && packs.isNotEmpty()) {
Text(
"${filteredPacks.size} packs",
stringResource(R.string.label_d_packs, filteredPacks.size),
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
@@ -345,7 +388,7 @@ fun ExplorePacksScreen(
Column(horizontalAlignment = Alignment.CenterHorizontally) {
CircularProgressIndicator()
Spacer(modifier = Modifier.height(12.dp))
Text("Loading packs", style = MaterialTheme.typography.bodyMedium)
Text(stringResource(R.string.text_loading_packs), style = MaterialTheme.typography.bodyMedium)
}
}
}
@@ -357,7 +400,7 @@ fun ExplorePacksScreen(
modifier = Modifier.padding(24.dp)
) {
Text(
"Could not load packs",
stringResource(R.string.text_could_not_load_packs),
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold
)
@@ -370,7 +413,7 @@ fun ExplorePacksScreen(
)
Spacer(modifier = Modifier.height(16.dp))
Button(onClick = { vocabPacksViewModel.loadManifest() }) {
Text("Retry")
Text(stringResource(R.string.label_retry))
}
}
}
@@ -378,11 +421,21 @@ fun ExplorePacksScreen(
filteredPacks.isEmpty() -> {
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Text(
"No packs match your search.",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(16.dp),
modifier = Modifier.padding(24.dp)
) {
Text(
stringResource(R.string.text_no_packs_match_search),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
textAlign = TextAlign.Center
)
OutlinedButton(onClick = { showRequestMoreDialog = true }) {
Text(stringResource(R.string.label_request_a_pack), fontWeight = FontWeight.Bold)
}
}
}
}
@@ -394,27 +447,89 @@ fun ExplorePacksScreen(
contentPadding = PaddingValues(bottom = 100.dp),
modifier = Modifier.fillMaxSize()
) {
item(span = { GridItemSpan(maxLineSpan) }) {
// intentionally empty grid footer below
}
items(filteredPacks, key = { it.info.id }) { packState ->
PackCard(
packState = packState,
onGetClick = { vocabPacksViewModel.downloadPack(packState.info) },
onCardClick = {
previewPack = packState
when (packState.downloadState) {
// Not yet downloaded → download for preview (will auto-open items)
PackDownloadState.IDLE ->
vocabPacksViewModel.downloadPack(packState.info)
// Already in library → re-download briefly just for preview
PackDownloadState.IMPORTED ->
vocabPacksViewModel.downloadForPreview(packState.info)
else -> {}
}
},
onGetClick = {
// Combined flow: download → auto-open conflict dialog
pendingImportPackId = packState.info.id
vocabPacksViewModel.downloadPack(packState.info)
},
onAddToLibraryClick = {
pendingImportPackState = packState
showStrategyDialog = true
},
onDeleteClick = { vocabPacksViewModel.deletePack(packState.info) },
)
}
// ── Request More footer ────────────────────────────────
item(span = { GridItemSpan(maxLineSpan) }) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 24.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
Text(
stringResource(R.string.text_dont_see_what_looking_for),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
OutlinedButton(onClick = { showRequestMoreDialog = true }) {
Text(stringResource(R.string.label_request_a_pack), fontWeight = FontWeight.Bold)
}
}
}
}
}
}
}
}
// ── Request More dialog ───────────────────────────────────────────────────
if (showRequestMoreDialog) {
RequestMorePackDialog(onDismiss = { showRequestMoreDialog = false })
}
// ── Pack preview dialog ───────────────────────────────────────────────────
val preview = previewPack
if (preview != null) {
PackPreviewDialog(
packState = preview,
onDismiss = { previewPack = null },
onGetClick = {
pendingImportPackId = preview.info.id
vocabPacksViewModel.downloadPack(preview.info)
previewPack = null
},
onAddToLibraryClick = {
pendingImportPackState = preview
showStrategyDialog = true
previewPack = null
},
)
}
// ── Conflict strategy dialog ──────────────────────────────────────────────
if (showStrategyDialog && pendingImportPackState != null) {
val packState = pendingImportPackState!!
AlertDialog(
AppAlertDialog(
onDismissRequest = {
if (!isImporting) {
showStrategyDialog = false
@@ -422,7 +537,7 @@ fun ExplorePacksScreen(
}
},
icon = { Icon(Icons.Default.Warning, contentDescription = null) },
title = { Text("Import \"${packState.info.name}\"") },
title = { Text(stringResource(R.string.title_import_pack, packState.info.name)) },
text = {
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
if (isImporting) {
@@ -434,19 +549,19 @@ fun ExplorePacksScreen(
CircularProgressIndicator()
Spacer(modifier = Modifier.height(12.dp))
Text(
"Importing ${packState.info.itemCount} words…",
stringResource(R.string.text_importing_d_words, packState.info.itemCount),
style = MaterialTheme.typography.bodyMedium
)
}
}
} else {
Text(
"${packState.info.itemCount} words will be added to your library.",
stringResource(R.string.text_d_words_will_be_added, packState.info.itemCount),
style = MaterialTheme.typography.bodyMedium
)
Spacer(modifier = Modifier.height(4.dp))
Text(
"How should duplicates be handled?",
stringResource(R.string.text_how_handle_duplicates),
style = MaterialTheme.typography.labelLarge,
fontWeight = FontWeight.SemiBold
)
@@ -454,26 +569,26 @@ fun ExplorePacksScreen(
PackConflictStrategyOption(
selected = selectedConflictStrategy == ConflictStrategy.MERGE,
onSelected = { selectedConflictStrategy = ConflictStrategy.MERGE },
title = "Merge (Recommended)",
description = "Keep existing progress; merge categories intelligently."
title = stringResource(R.string.strategy_merge),
description = stringResource(R.string.strategy_merge_desc)
)
PackConflictStrategyOption(
selected = selectedConflictStrategy == ConflictStrategy.SKIP,
onSelected = { selectedConflictStrategy = ConflictStrategy.SKIP },
title = "Skip Duplicates",
description = "Only add words that don't already exist."
title = stringResource(R.string.strategy_skip),
description = stringResource(R.string.strategy_skip_desc)
)
PackConflictStrategyOption(
selected = selectedConflictStrategy == ConflictStrategy.REPLACE,
onSelected = { selectedConflictStrategy = ConflictStrategy.REPLACE },
title = "Replace Existing",
description = "Overwrite matching words with the pack version."
title = stringResource(R.string.strategy_replace),
description = stringResource(R.string.strategy_replace_desc)
)
PackConflictStrategyOption(
selected = selectedConflictStrategy == ConflictStrategy.RENAME,
onSelected = { selectedConflictStrategy = ConflictStrategy.RENAME },
title = "Keep Both",
description = "Add all words from the pack as new entries."
title = stringResource(R.string.strategy_keep_both),
description = stringResource(R.string.strategy_keep_both_desc)
)
}
}
@@ -496,7 +611,7 @@ fun ExplorePacksScreen(
showStrategyDialog = false
exportImportViewModel.importFromJson(json, selectedConflictStrategy)
}
) { Text("Add to Library") }
) { Text(stringResource(R.string.label_add_to_library)) }
},
dismissButton = {
TextButton(
@@ -505,7 +620,7 @@ fun ExplorePacksScreen(
showStrategyDialog = false
pendingImportPackState = null
}
) { Text("Cancel") }
) { Text(stringResource(R.string.label_cancel)) }
}
)
}
@@ -553,16 +668,18 @@ private fun PackConflictStrategyOption(
@Composable
private fun PackCard(
packState: PackUiState,
onCardClick: () -> Unit,
onGetClick: () -> Unit,
onAddToLibraryClick: () -> Unit,
onDeleteClick: () -> Unit,
modifier: Modifier = Modifier,
) {
val info = packState.info
val gradient = gradientForId(info.id)
Surface(
modifier = modifier.fillMaxWidth(),
modifier = modifier
.fillMaxWidth()
.clickable(onClick = onCardClick),
shape = RoundedCornerShape(16.dp),
color = MaterialTheme.colorScheme.surfaceContainer,
tonalElevation = 2.dp
@@ -623,8 +740,8 @@ private fun PackCard(
// Status badge (top-right)
val badgeData: Pair<Color, String>? = when (packState.downloadState) {
PackDownloadState.DOWNLOADED -> Color(0xFF1565C0) to "Ready"
PackDownloadState.IMPORTED -> Color(0xFF388E3C) to "In Library"
PackDownloadState.DOWNLOADED -> Color(0xFF1565C0) to stringResource(R.string.label_ready)
PackDownloadState.IMPORTED -> Color(0xFF388E3C) to stringResource(R.string.label_in_library)
else -> null
}
if (badgeData != null) {
@@ -678,7 +795,7 @@ private fun PackCard(
)
Spacer(modifier = Modifier.height(4.dp))
Text(
text = "${info.itemCount} cards",
text = stringResource(R.string.text_d_cards, info.itemCount),
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
@@ -693,7 +810,7 @@ private fun PackCard(
contentPadding = PaddingValues(0.dp),
shape = RoundedCornerShape(8.dp)
) {
Text("Get", style = MaterialTheme.typography.labelMedium,
Text(stringResource(R.string.label_get), style = MaterialTheme.typography.labelMedium,
fontWeight = FontWeight.Bold)
}
}
@@ -706,36 +823,23 @@ private fun PackCard(
contentPadding = PaddingValues(0.dp),
shape = RoundedCornerShape(8.dp)
) {
Text("Downloading", style = MaterialTheme.typography.labelSmall)
Text(stringResource(R.string.text_downloading), style = MaterialTheme.typography.labelSmall)
}
}
PackDownloadState.DOWNLOADED -> {
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
Button(
onClick = onAddToLibraryClick,
modifier = Modifier.fillMaxWidth().height(34.dp),
contentPadding = PaddingValues(0.dp),
shape = RoundedCornerShape(8.dp),
colors = ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.secondary
)
) {
Text("Add ${info.itemCount} words",
style = MaterialTheme.typography.labelSmall,
fontWeight = FontWeight.Bold)
}
OutlinedButton(
onClick = onDeleteClick,
modifier = Modifier.fillMaxWidth().height(30.dp),
contentPadding = PaddingValues(0.dp),
shape = RoundedCornerShape(8.dp),
) {
Icon(Icons.Default.Delete, contentDescription = null,
modifier = Modifier.size(14.dp))
Spacer(modifier = Modifier.width(4.dp))
Text("Remove", style = MaterialTheme.typography.labelSmall)
}
Button(
onClick = onAddToLibraryClick,
modifier = Modifier.fillMaxWidth().height(34.dp),
contentPadding = PaddingValues(0.dp),
shape = RoundedCornerShape(8.dp),
colors = ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.secondary
)
) {
Text(stringResource(R.string.label_add_d_words, info.itemCount),
style = MaterialTheme.typography.labelSmall,
fontWeight = FontWeight.Bold)
}
}
@@ -750,7 +854,7 @@ private fun PackCard(
modifier = Modifier.size(14.dp),
tint = Color(0xFF388E3C))
Spacer(modifier = Modifier.width(4.dp))
Text("In Library", style = MaterialTheme.typography.labelSmall)
Text(stringResource(R.string.label_in_library), style = MaterialTheme.typography.labelSmall)
}
}
@@ -764,7 +868,7 @@ private fun PackCard(
containerColor = MaterialTheme.colorScheme.error
)
) {
Text("Retry", style = MaterialTheme.typography.labelMedium,
Text(stringResource(R.string.label_retry), style = MaterialTheme.typography.labelMedium,
fontWeight = FontWeight.Bold)
}
}
@@ -773,3 +877,197 @@ private fun PackCard(
}
}
}
// ---------------------------------------------------------------------------
// Pack preview sheet (AppDialog = ModalBottomSheet)
// ---------------------------------------------------------------------------
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun PackPreviewDialog(
packState: PackUiState,
onDismiss: () -> Unit,
onGetClick: () -> Unit,
onAddToLibraryClick: () -> Unit,
modifier: Modifier = Modifier,
) {
val info = packState.info
val gradient = gradientForId(info.id)
var selectedItem by remember { mutableStateOf<VocabularyItem?>(null) }
AppDialog(
onDismissRequest = onDismiss,
title = { Text(info.name, fontWeight = FontWeight.Bold) },
) {
// ── Gradient banner ───────────────────────────────────────────
Box(
modifier = modifier
.fillMaxWidth()
.height(140.dp)
.clip(RoundedCornerShape(12.dp))
.background(Brush.verticalGradient(gradient))
) {
if (info.level.isNotBlank()) {
Surface(
modifier = Modifier.align(Alignment.TopStart).padding(10.dp),
shape = RoundedCornerShape(6.dp),
color = Color.Black.copy(alpha = 0.4f)
) {
Text(
text = info.level,
style = MaterialTheme.typography.labelSmall,
fontWeight = FontWeight.Bold,
color = Color.White,
modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp)
)
}
}
Column(modifier = Modifier.align(Alignment.BottomStart).padding(12.dp)) {
Text(info.emoji, fontSize = 32.sp)
Text(
"${info.category} · ${stringResource(R.string.text_d_cards, info.itemCount)}",
style = MaterialTheme.typography.labelMedium,
color = Color.White.copy(alpha = 0.85f)
)
}
}
// ── Description ───────────────────────────────────────────────
if (info.description.isNotBlank()) {
Spacer(modifier = Modifier.height(12.dp))
Text(
text = info.description,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Spacer(modifier = Modifier.height(8.dp))
HorizontalDivider()
}
// ── Item list (or loading indicator) ──────────────────────────
Spacer(modifier = Modifier.height(8.dp))
when {
packState.downloadState == PackDownloadState.DOWNLOADING ||
packState.previewItems.isEmpty() -> {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.fillMaxWidth().padding(vertical = 24.dp)
) {
CircularProgressIndicator()
Spacer(modifier = Modifier.height(12.dp))
Text(
stringResource(R.string.text_loading_preview),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
else -> {
val items = packState.previewItems
Text(
stringResource(R.string.label_words_in_this_pack),
style = MaterialTheme.typography.labelLarge,
fontWeight = FontWeight.SemiBold
)
Spacer(modifier = Modifier.height(4.dp))
// Regular Column (AppDialog is already inside verticalScroll)
Column {
items.forEachIndexed { index, item ->
Row(
modifier = Modifier
.fillMaxWidth()
.clickable { selectedItem = item }
.padding(vertical = 10.dp),
verticalAlignment = Alignment.CenterVertically
) {
Column(modifier = Modifier.weight(1f)) {
Text(item.wordFirst, style = MaterialTheme.typography.bodyLarge)
Text(
item.wordSecond,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
Icon(
Icons.Default.ArrowForwardIos,
contentDescription = null,
modifier = Modifier.size(14.dp),
tint = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f)
)
}
if (index < items.lastIndex) HorizontalDivider(thickness = 0.5.dp)
}
}
}
}
// ── Action button ─────────────────────────────────────────────
Spacer(modifier = Modifier.height(16.dp))
HorizontalDivider()
Spacer(modifier = Modifier.height(12.dp))
when (packState.downloadState) {
PackDownloadState.IDLE -> {
Button(
onClick = onGetClick,
modifier = Modifier.fillMaxWidth().height(52.dp),
shape = RoundedCornerShape(12.dp)
) {
Text(stringResource(R.string.label_get_d_words, info.itemCount), style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold)
}
}
PackDownloadState.DOWNLOADING -> {
Button(onClick = {}, enabled = false, modifier = Modifier.fillMaxWidth().height(52.dp), shape = RoundedCornerShape(12.dp)) {
Row(verticalAlignment = Alignment.CenterVertically) {
CircularProgressIndicator(modifier = Modifier.size(20.dp), color = MaterialTheme.colorScheme.onPrimary, strokeWidth = 2.dp)
Spacer(modifier = Modifier.width(8.dp))
Text(stringResource(R.string.text_downloading))
}
}
}
PackDownloadState.DOWNLOADED -> {
Button(
onClick = onAddToLibraryClick,
modifier = Modifier.fillMaxWidth().height(52.dp),
shape = RoundedCornerShape(12.dp),
colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.secondary)
) {
Text(stringResource(R.string.label_add_d_words_to_library, info.itemCount), style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold)
}
}
PackDownloadState.IMPORTED -> {
OutlinedButton(onClick = onDismiss, modifier = Modifier.fillMaxWidth().height(52.dp), shape = RoundedCornerShape(12.dp)) {
Icon(Icons.Default.CheckCircle, contentDescription = null, tint = Color(0xFF388E3C), modifier = Modifier.size(20.dp))
Spacer(modifier = Modifier.width(8.dp))
Text(stringResource(R.string.text_already_in_your_library), style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold)
}
}
PackDownloadState.ERROR -> {
Button(
onClick = onGetClick,
modifier = Modifier.fillMaxWidth().height(52.dp),
shape = RoundedCornerShape(12.dp),
colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.error)
) {
Text(stringResource(R.string.label_retry_download), style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold)
}
}
}
}
// ── Individual item detail ────────────────────────────────────────────────
val item = selectedItem
if (item != null) {
AppAlertDialog(
onDismissRequest = { selectedItem = null },
title = { Text(item.wordFirst, fontWeight = FontWeight.Bold) },
text = {
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
Text(item.wordSecond, style = MaterialTheme.typography.bodyLarge)
}
},
confirmButton = {
TextButton(onClick = { selectedItem = null }) { Text(stringResource(R.string.label_close)) }
}
)
}
}

View File

@@ -232,7 +232,7 @@ fun VocabularySortingScreen(
}
},
onNavigateBack = { navController.popBackStack() },
hintContent = HintDefinition.SORTING.hint()
hint = HintDefinition.SORTING.hint()
)
},

View File

@@ -7,13 +7,19 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
import eu.gaudian.translator.model.VocabularyItem
import eu.gaudian.translator.model.communication.FileDownloadManager
import eu.gaudian.translator.model.communication.files_download.VocabCollectionInfo
import eu.gaudian.translator.model.jsonParser
import eu.gaudian.translator.utils.Log
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import kotlinx.serialization.Serializable
import java.io.File
import javax.inject.Inject
@@ -27,8 +33,14 @@ data class PackUiState(
val info: VocabCollectionInfo,
val downloadState: PackDownloadState = PackDownloadState.IDLE,
val progress: Float = 0f,
/** Items available once the file has been downloaded and parsed (for preview). */
val previewItems: List<VocabularyItem> = emptyList(),
)
/** Internal wrapper for deserializing the items array from a pack file. */
@Serializable
private data class PackPreviewWrapper(val items: List<VocabularyItem> = emptyList())
// ---------------------------------------------------------------------------
// ViewModel
// ---------------------------------------------------------------------------
@@ -51,10 +63,30 @@ class VocabPacksViewModel @Inject constructor(
private val _manifestError = MutableStateFlow<String?>(null)
val manifestError: StateFlow<String?> = _manifestError.asStateFlow()
/**
* Emits a pack ID every time a pack has been fully downloaded AND its items parsed.
* The screen listens to this to auto-open the conflict dialog after "Get" is tapped.
*/
private val _downloadCompleteEvents = MutableSharedFlow<String>(extraBufferCapacity = 8)
val downloadCompleteEvents: SharedFlow<String> = _downloadCompleteEvents.asSharedFlow()
init {
loadManifest()
}
// ── Persistent import records ─────────────────────────────────────────────
private fun importedVersion(packId: String): Int? {
val prefs = context.getSharedPreferences(PREFS_NAME, android.content.Context.MODE_PRIVATE)
return if (prefs.contains(prefKey(packId))) prefs.getInt(prefKey(packId), -1) else null
}
private fun saveImportedVersion(packId: String, version: Int) {
context.getSharedPreferences(PREFS_NAME, android.content.Context.MODE_PRIVATE)
.edit().putInt(prefKey(packId), version).apply()
Log.d(TAG, "Saved imported version $version for $packId")
}
// ── Manifest ─────────────────────────────────────────────────────────────
fun loadManifest() {
@@ -65,10 +97,18 @@ class VocabPacksViewModel @Inject constructor(
val manifest = downloadManager.fetchVocabManifest()
Log.d(TAG, "Fetched ${manifest?.size ?: 0} packs from manifest")
_packs.value = manifest?.map { info ->
val savedVersion = importedVersion(info.id)
val isCurrentVersionImported = savedVersion != null && savedVersion >= info.version
val downloaded = downloadManager.isVocabCollectionDownloaded(info)
val items = if (downloaded) parsePreviewItems(info) else emptyList()
PackUiState(
info = info,
downloadState = if (downloaded) PackDownloadState.DOWNLOADED else PackDownloadState.IDLE,
downloadState = when {
isCurrentVersionImported -> PackDownloadState.IMPORTED
downloaded -> PackDownloadState.DOWNLOADED
else -> PackDownloadState.IDLE
},
previewItems = items,
)
} ?: emptyList()
} catch (e: Exception) {
@@ -83,6 +123,11 @@ class VocabPacksViewModel @Inject constructor(
// ── Download ─────────────────────────────────────────────────────────────
fun downloadPack(info: VocabCollectionInfo) {
// Avoid double-downloading
val current = _packs.value.find { it.info.id == info.id }
if (current?.downloadState == PackDownloadState.DOWNLOADING ||
current?.downloadState == PackDownloadState.DOWNLOADED) return
viewModelScope.launch {
Log.d(TAG, "Starting download of ${info.id}")
updatePack(info.id) { it.copy(downloadState = PackDownloadState.DOWNLOADING, progress = 0f) }
@@ -91,8 +136,12 @@ class VocabPacksViewModel @Inject constructor(
updatePack(info.id) { it.copy(progress = progress) }
}
if (success) {
Log.d(TAG, "Download complete for ${info.id} (${info.filename})")
updatePack(info.id) { it.copy(downloadState = PackDownloadState.DOWNLOADED, progress = 1f) }
val items = parsePreviewItems(info)
Log.d(TAG, "Download complete for ${info.id}: ${items.size} items parsed")
updatePack(info.id) {
it.copy(downloadState = PackDownloadState.DOWNLOADED, progress = 1f, previewItems = items)
}
_downloadCompleteEvents.emit(info.id)
} else {
Log.e(TAG, "Download returned false for ${info.id}")
updatePack(info.id) { it.copy(downloadState = PackDownloadState.ERROR) }
@@ -104,15 +153,9 @@ class VocabPacksViewModel @Inject constructor(
}
}
// ── Read file for import ──────────────────────────────────────────────────
// ── Read raw JSON for import ──────────────────────────────────────────────
/**
* Reads the downloaded pack file and returns its raw JSON string, or null if the
* file doesn't exist. The caller (screen) passes this to [ExportImportViewModel.importFromJson].
*
* The file is in the full Polly export format:
* { "type": "Category", "items": [...], "category": {...}, ... }
*/
/** Returns the raw JSON of the downloaded pack file, or null if not present. */
fun readPackRawJson(info: VocabCollectionInfo): String? {
val file = localFile(info)
if (!file.exists()) {
@@ -131,34 +174,89 @@ class VocabPacksViewModel @Inject constructor(
// ── Post-import cleanup ───────────────────────────────────────────────────
/**
* Called after the import has been confirmed successful.
* Deletes the local JSON file (items are now in the DB) and marks the
* card as [PackDownloadState.IMPORTED].
*/
fun markImportedAndCleanup(info: VocabCollectionInfo) {
val file = localFile(info)
if (file.exists()) {
file.delete()
Log.d(TAG, "Deleted local pack file: ${file.absolutePath}")
}
saveImportedVersion(info.id, info.version)
updatePack(info.id) { it.copy(downloadState = PackDownloadState.IMPORTED, progress = 1f) }
}
// ── Delete ────────────────────────────────────────────────────────────────
// ── Preview download for already-imported packs ───────────────────────────
fun deletePack(info: VocabCollectionInfo) {
val file = localFile(info)
if (file.exists()) file.delete()
Log.d(TAG, "Deleted pack (user-requested): ${info.id}")
updatePack(info.id) { it.copy(downloadState = PackDownloadState.IDLE, progress = 0f) }
/**
* Downloads the pack file solely to populate [PackUiState.previewItems] for IMPORTED packs.
* The file is deleted immediately after parsing; [downloadState] stays IMPORTED throughout.
*/
fun downloadForPreview(info: VocabCollectionInfo) {
val current = _packs.value.find { it.info.id == info.id }
if (current?.downloadState != PackDownloadState.IMPORTED) return
if (current.previewItems.isNotEmpty()) return // already cached in memory
viewModelScope.launch {
Log.d(TAG, "Downloading for preview (IMPORTED): ${info.id}")
try {
val success = downloadManager.downloadVocabCollection(info) { /* no progress UI */ }
if (success) {
val items = parsePreviewItems(info)
Log.d(TAG, "Preview items loaded for ${info.id}: ${items.size}")
updatePack(info.id) { it.copy(previewItems = items) }
// Delete the temp file immediately we only needed it for parsing
localFile(info).takeIf { it.exists() }?.delete()
}
} catch (e: Exception) {
Log.e(TAG, "Preview download failed for ${info.id}", e)
}
}
}
// ── Cleanup on screen exit ────────────────────────────────────────────────
/**
* Deletes all downloaded-but-not-yet-imported files and resets those packs to IDLE.
* Called from DisposableEffect.onDispose in ExplorePacksScreen.
*/
fun cleanupDownloadedFiles() {
val toClean = _packs.value.filter { it.downloadState == PackDownloadState.DOWNLOADED }
toClean.forEach { ps ->
val file = localFile(ps.info)
if (file.exists()) {
file.delete()
Log.d(TAG, "Cleaned up on exit: ${ps.info.id}")
}
}
if (toClean.isNotEmpty()) {
_packs.value = _packs.value.map { ps ->
if (ps.downloadState == PackDownloadState.DOWNLOADED)
ps.copy(downloadState = PackDownloadState.IDLE, progress = 0f, previewItems = emptyList())
else ps
}
}
}
// ── Helpers ───────────────────────────────────────────────────────────────
private fun parsePreviewItems(info: VocabCollectionInfo): List<VocabularyItem> {
val json = readPackRawJson(info) ?: return emptyList()
return try {
val wrapper = jsonParser.decodeFromString<PackPreviewWrapper>(json)
wrapper.items.map { it.copy(id = 0) } // reset DB ids these are preview-only
} catch (e: Exception) {
Log.e(TAG, "Error parsing preview items for ${info.id}", e)
emptyList()
}
}
private fun localFile(info: VocabCollectionInfo): File =
File(context.filesDir, "flashcard-collections/${info.filename}")
companion object {
private const val PREFS_NAME = "vocab_packs_imported"
private fun prefKey(packId: String) = "v_$packId"
}
private fun updatePack(packId: String, transform: (PackUiState) -> PackUiState) {
_packs.value = _packs.value.map { if (it.info.id == packId) transform(it) else it }
}

View File

@@ -1,6 +1,9 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<resources xmlns:tools="http://schemas.android.com/tools">
<string name="category_hint_intro">You can create two types of categories to organize your vocabulary:</string>
<string name="cd_achieved">Achieved</string>
<string name="cd_add">Add</string>
<string name="cd_app_logo">App Logo</string>
<string name="cd_back">Back</string>
<string name="cd_clear_search">Clear Search</string>
@@ -8,10 +11,20 @@
<string name="cd_collapse">Collapse</string>
<string name="cd_error">Error</string>
<string name="cd_expand">Expand</string>
<string name="cd_filter">Filter</string>
<string name="cd_filter_options">Filter options</string>
<string name="cd_go">Go</string>
<string name="cd_navigate_back">Navigate back</string>
<string name="cd_options">Options</string>
<string name="cd_paste">Paste</string>
<string name="cd_play">Play</string>
<string name="cd_re_generate_definition">Re-generate Definition</string>
<string name="cd_reload">Reload</string>
<string name="cd_scroll_to_top">Scroll to top</string>
<string name="cd_search">Search</string>
<string name="cd_searchh">Search</string>
<string name="cd_selected">Selected</string>
<string name="cd_settings">Settings</string>
<string name="cd_success">Success</string>
<string name="cd_switch_languages">Switch Languages</string>
<string name="cd_target_met">Target Met</string>
@@ -19,20 +32,9 @@
<string name="cd_toggle_menu">Toggle Menu</string>
<string name="cd_translation_history">Translation History</string>
<string name="label_multiple_choice_desc">Choose the right translation</string>
<string name="label_clear_all">Clear All</string>
<string name="label_close_exercise">Close exercise</string>
<string name="label_close_selection_mode">Close selection mode</string>
<string name="label_colloquial">Colloquial</string>
<string name="contact_developer_description">Contact me for bug reports, ideas, feature requests, and more.</string>
<string name="contact_developer_title">Contact developer</string>
<string name="label_context">Context</string>
<string name="copied_text">Copied Text</string>
<string name="copy_text">Copy text</string>
@@ -42,7 +44,6 @@
<string name="correct_answers_">Correct answers: %1$d</string>
<string name="correct_tone">Tone</string>
<string name="label_create">Create</string>
<string name="create_a_new_custom_language_entry_for_this_id">Create a new custom language entry for this ID.</string>
<string name="create_new_category">Create New Category</string>
<string name="create_new_language">Create New Language</string>
@@ -74,6 +75,9 @@
<string name="delete_new">Delete New</string>
<string name="delete_provider">Delete Provider</string>
<string name="desc_daily_review_due">%1$d words need attention</string>
<string name="desc_expand_your_vocabulary">Expand your vocabulary</string>
<string name="description">Description</string>
<string name="deselect_all">Deselect All</string>
@@ -85,6 +89,7 @@
<string name="due_today_">Due Today: %1$s</string>
<string name="duplicate">Duplicate</string>
<string name="duplicate_detected">Duplicate Detected</string>
<string name="duplicates_only">Duplicates Only</string>
@@ -134,9 +139,17 @@
<string name="fetching_for_d_items">Fetching for %d Items</string>
<string name="fetching_grammar_details">Fetching Grammar Details</string>
<string name="filter_a1">Beginner · A1</string>
<string name="filter_a2">Elementary · A2</string>
<!-- Pack Filter Labels -->
<string name="filter_all">All</string>
<string name="filter_and_sort">Filter and Sort</string>
<string name="label_filter_by_stage">Filter by Stage</string>
<string name="filter_b1">Intermediate · B1</string>
<string name="filter_b2">Upper Int. · B2</string>
<string name="filter_by_word_type">Filter by Word Type</string>
<string name="filter_c1">Advanced · C1</string>
<string name="filter_c2">Proficient · C2</string>
<string name="filter_newest">Newest</string>
<string name="find_translations">Find Translations</string>
@@ -165,8 +178,17 @@
<string name="hide_context">Hide</string>
<string name="hint">Hint: %1$s</string>
<string name="hint_hints_header_advanced">Advanced Features</string>
<string name="hint_hints_header_basics">Getting Started</string>
<string name="hint_hints_header_vocabulary">Vocabulary Management</string>
<string name="hint_hints_overview_description">All hints that are in this app can be found here as well.</string>
<string name="hint_hints_overview_intro">Help Center</string>
<string name="hint_how_to_connect_to_an_ai">How to connect to an AI</string>
<string name="hint_how_to_generate_vocabulary_with_ai">How to generate Vocabulary with AI</string>
<string name="hint_scan_hint_title">Finding the right AI model</string>
<string name="hint_title_hints_overview">Help and Instructions</string>
<string name="hint_translate_how_it_works">How translation works</string>
<string name="hint_vocabulary_progress_hint_title">Vocabulary Progress Tracking</string>
<string name="imperative">Imperative</string>
@@ -190,6 +212,7 @@
<string name="keep_both">Keep Both</string>
<string name="label_2d_days">%1$d Days</string>
<string name="label_about">About</string>
<string name="label_academic">Academic</string>
<string name="label_action_correct">Correct</string>
@@ -198,19 +221,26 @@
<string name="label_add_category">Add Category</string>
<string name="label_add_custom_model">Add Custom Model</string>
<string name="label_add_custom_provider">Add Custom Provider</string>
<string name="label_add_d_words">Add %1$d words</string>
<string name="label_add_d_words_to_library">Add %1$d words to Library</string>
<string name="label_add_key">Add Key</string>
<string name="label_add_model">Add Model</string>
<string name="label_add_model_manually">Add Model Manually</string>
<string name="label_add_synonym">Add synonym</string>
<string name="label_add_to_dictionary">Add to dictionary</string>
<string name="label_add_to_library">Add to Library</string>
<string name="label_add_validate"><![CDATA[Add & Validate]]></string>
<string name="label_add_vocabulary">Add Vocabulary</string>
<string name="label_added">Added</string>
<string name="label_adjective">Adjective</string>
<string name="label_adverb">Adverb</string>
<string name="label_ai_configuration">AI Configuration</string>
<string name="label_ai_generator">AI Generator</string>
<string name="label_ai_model">AI Model</string>
<string name="label_ai_model_and_prompt"><![CDATA[AI Model & Prompt]]></string>
<string name="label_all_cards">All Cards</string>
<string name="label_all_categories">All Categories</string>
<string name="label_all_categoriess">All Categories</string>
<string name="label_all_stages">All Stages</string>
<string name="label_all_types">All Types</string>
<string name="label_all_vocabulary">All Vocabulary</string>
@@ -220,6 +250,8 @@
<string name="label_appearance">Appearance</string>
<string name="label_apply_filters">Apply Filters</string>
<string name="label_article">Article</string>
<string name="label_auto_cycle_dev">Auto Cycle (Dev)</string>
<string name="label_available_collections">Available Collections</string>
<string name="label_backup_and_restore">Backup and Restore</string>
<string name="label_by_language">By Language</string>
<string name="label_cancel">Cancel</string>
@@ -228,19 +260,30 @@
<string name="label_category">Category</string>
<string name="label_category_2d">Category: %1$s</string>
<string name="label_clear">Clear</string>
<string name="label_clear_all">Clear All</string>
<string name="label_close">Close</string>
<string name="label_close_exercise">Close exercise</string>
<string name="label_close_search">Close search</string>
<string name="label_close_selection_mode">Close selection mode</string>
<string name="label_collapse">Collapse</string>
<string name="label_colloquial">Colloquial</string>
<string name="label_column_n">Column %1$d</string>
<string name="label_common">Common</string>
<string name="label_completed">Completed</string>
<string name="label_confirm">Confirm</string>
<string name="label_conjugation">Conjugation: %1$s</string>
<string name="label_conjunction">Conjunction</string>
<string name="label_context">Context</string>
<string name="label_continue">Continue</string>
<string name="label_correct">Correct</string>
<string name="label_create">Create</string>
<string name="label_create_exercise">Create Exercise</string>
<string name="label_current_streak">Current Streak</string>
<string name="label_custom">Custom</string>
<string name="label_d_packs">%1$d packs</string>
<string name="label_daily_goal">Daily Goal</string>
<string name="label_daily_review">Daily Review</string>
<string name="label_declension">Declension</string>
<string name="label_definitions">Definitions</string>
<string name="label_delete">Delete</string>
<string name="label_delete_all">Delete all</string>
@@ -254,110 +297,159 @@
<string name="label_dictionary_content">Dictionary Content</string>
<string name="label_dictionary_manager">Dictionary Manager</string>
<string name="label_dictionary_options">Dictionary Options</string>
<string name="tab_ai_definition">AI Definition</string>
<string name="tab_downloaded">Downloaded</string>
<string name="label_display_name">Display Name</string>
<string name="label_done">Done</string>
<string name="label_download">Download</string>
<string name="label_easy">Easy</string>
<string name="label_edit">Edit</string>
<string name="label_enter_a_text">Enter a text</string>
<string name="label_etymology">Etymology</string>
<string name="label_exercise">Exercise</string>
<string name="label_exercises">Exercises</string>
<string name="label_expand">Expand</string>
<string name="label_feminine">Feminine</string>
<string name="label_filter_by_stage">Filter by Stage</string>
<string name="label_filter_cards">Filter Cards</string>
<string name="label_first_column">First Column</string>
<string name="label_first_language">First Language</string>
<string name="label_from">From</string>
<string name="label_gender">Gender</string>
<string name="label_general">General</string>
<!-- Pack Card -->
<string name="label_get">Get</string>
<string name="label_get_d_words">Get %1$d words</string>
<string name="label_grammar_auxiliary">" (Auxiliary: %1$s)"</string>
<string name="label_grammar_hyphenation">Hyphenation</string>
<string name="label_grammar_inflections">Inflections</string>
<string name="label_grammar_meanings">Meanings</string>
<string name="label_grammar_only">Grammar only</string>
<string name="label_guessing_exercise">Guessing</string>
<string name="label_hard">Hard</string>
<string name="label_header_row">First Row is a Header</string>
<string name="label_hide_examples">Hide examples</string>
<string name="label_home">Home</string>
<string name="label_import">Import</string>
<string name="label_import_csv">Import CSV</string>
<string name="label_import_table_csv_excel">Import Table (CSV)</string>
<string name="label_in_library">In Library</string>
<string name="label_in_stages">In Stages</string>
<string name="label_interjection">Interjection</string>
<string name="label_interval_settings_in_days">Interval Settings</string>
<string name="label_language_auto">Auto</string>
<string name="label_language_direction">Language Direction\n</string>
<string name="label_language_none">None</string>
<string name="label_languages">Languages</string>
<string name="label_learned">Learned</string>
<string name="label_learnedd">learned</string>
<string name="label_learning_criteria">Learning Criteria</string>
<string name="label_library">Library</string>
<string name="label_logs">Logs</string>
<string name="label_masculine">Masculine</string>
<string name="label_medium">Medium</string>
<string name="label_model_id_star">Model ID *</string>
<string name="label_more">More</string>
<string name="label_move_first_stage">Move to First Stage</string>
<string name="label_multiple_choice_desc">Choose the right translation</string>
<string name="label_multiple_choice_exercise">Multiple Choice</string>
<string name="label_neuter">Neuter</string>
<string name="label_new">New</string>
<string name="label_new_words">New Words</string>
<string name="label_new_wordss">New Words</string>
<string name="label_no_category">None</string>
<string name="label_no_history_yet">No history yet</string>
<string name="label_noun">Noun</string>
<string name="label_optional">(Optional)</string>
<string name="label_origin_language">Origin Language</string>
<string name="label_orphaned_files">Orphaned Files</string>
<string name="label_paste">Paste</string>
<string name="label_plural">Plural</string>
<string name="label_preposition">Preposition</string>
<string name="label_preview_first">Preview (first 5) for first column: %1$s</string>
<string name="label_preview_second">Preview (first 5) for second column: %1$s</string>
<string name="label_pronoun">Pronoun</string>
<string name="label_pronunciation">Pronunc iation</string>
<string name="label_providers">Providers</string>
<string name="label_quit_app">Quit App</string>
<string name="label_quit_exercise_qm">Quit Exercise?</string>
<string name="label_raw_data_2d">Raw Data:</string>
<string name="label_read_aloud">Read Aloud</string>
<string name="label_ready">Ready</string>
<string name="label_recently_added">Recently Added</string>
<string name="label_regenerate">Regenerate</string>
<string name="label_related_words">Related Words</string>
<string name="label_reload">Reload</string>
<string name="label_remove_articles">Remove Articles</string>
<string name="label_request_a_pack">Request a Pack</string>
<string name="label_reset">Reset</string>
<string name="label_retry">Retry</string>
<string name="label_retry_download">Retry download</string>
<string name="label_save">Save</string>
<string name="label_scan_for_models">Scan for Models</string>
<string name="label_scanning">Scanning…</string>
<string name="label_search_cards">Search cards</string>
<string name="label_search_models">Search models…</string>
<string name="label_second_column">Second Column</string>
<string name="label_second_language">Second Language</string>
<string name="label_see_history">See History</string>
<string name="label_select">Select</string>
<string name="label_select_stage">Select Stage</string>
<string name="label_send_request">Send Request</string>
<string name="label_settings">Settings</string>
<string name="label_show_2d_more">Show %1$d More</string>
<string name="label_show_dictionary_entry">Show dictionary entry</string>
<string name="label_show_examples">Show examples</string>
<string name="label_show_less">Show Less</string>
<string name="label_show_more">Show More</string>
<string name="label_show_more_actions">Show more actions</string>
<string name="label_size_2d_mb">Size: %1$d MB</string>
<string name="label_sort_by">Sort By</string>
<string name="label_speaking_speed">Speaking Speed</string>
<string name="label_spelling_exercise">Spelling</string>
<string name="label_star_required">*required</string>
<string name="label_start">Start</string>
<string name="label_start_exercise">Start Exercise</string>
<string name="label_start_exercise_2d">Start Exercise (%1$d)</string>
<string name="label_start_required">* required</string>
<string name="label_statistics">Statistics</string>
<string name="label_stats">Stats</string>
<string name="label_status">Status</string>
<string name="label_system">System</string>
<string name="label_target_correct_answers_per_day">Target Correct Answers Per Day</string>
<string name="label_target_language">Target Language</string>
<string name="label_target_tone">Target Tone:</string>
<string name="label_task_model_assignments">Task Model Assignments</string>
<string name="label_tasks">Tasks</string>
<string name="label_tense">Tense</string>
<string name="label_to">To</string>
<string name="label_topic">Topic</string>
<string name="label_total_words">Total Words</string>
<string name="label_training_mode">Training Mode</string>
<string name="label_translate">Translate</string>
<string name="label_translate_from_2d">Translate from %1$s</string>
<string name="label_translation">Translation</string>
<string name="label_translation_server">Translation Server</string>
<string name="label_translation_settings">Translation Settings</string>
<string name="label_translations">Translations</string>
<string name="label_unknown">Unknown</string>
<string name="label_unknown_dictionary_d">Unknown Dictionary (%1$s)</string>
<string name="label_update">Update</string>
<string name="label_variations">Variations</string>
<string name="label_verb">Verb</string>
<string name="label_version_2d">Version: %1$s</string>
<string name="label_view_all">View All</string>
<string name="label_vocabulary">Vocabulary</string>
<string name="label_vocabulary_activity">Vocabulary Activity</string>
<string name="label_vocabulary_settings">Progress Settings</string>
<string name="label_warning">Warning</string>
<string name="label_weekly_progress">Weekly Progress</string>
<string name="label_wiktionary">Wiktionary</string>
<string name="label_word">Word</string>
<string name="label_word_jumble_exercise">Word Jumble</string>
<string name="label_words_in_this_pack">Words in this pack</string>
<string name="label_wrong">Wrong</string>
<string name="label_wrong_answers">Wrong answers</string>
<string name="label_yes">Yes</string>
<string name="label_your_answer">Your Answer</string>
<string name="label_your_translation">Your translation</string>
<string name="labels_1d_models">%1$d models</string>
@@ -412,6 +504,66 @@
<string name="merge">Merge</string>
<string name="merge_items">Merge Items</string>
<string name="message_error_api_key_invalid">API Key is missing or invalid.</string>
<!-- API Key related -->
<string name="message_error_api_key_missing">API Key is missing or invalid.</string>
<string name="message_error_articles_remove_failed">Error removing articles: %1$s</string>
<string name="message_error_category_update_failed">Error updating category: %1$s</string>
<string name="message_error_excel_not_supported">Excel is not supported. Use CSV instead.</string>
<string name="message_error_file_picker_not_initialized">Save File Launcher not initialized.</string>
<string name="message_error_file_save_cancelled">File save cancelled or failed.</string>
<string name="message_error_file_save_failed">Error saving file: %1$s</string>
<string name="message_error_generic">An error occurred</string>
<string name="message_error_grammar_fetch_failed">Could not retrieve grammar details.</string>
<string name="message_error_items_add_failed">Error adding items: %1$s</string>
<string name="message_error_items_delete_failed">Error deleting items: %1$s</string>
<!-- Language related -->
<string name="message_error_language_not_selected">Source and target languages must be selected.</string>
<string name="message_error_no_cards_found">No cards found for the specified filter.</string>
<string name="message_error_no_words_found">No words found in the provided text.</string>
<!-- Operation status -->
<string name="message_error_operation_failed">Operation failed: %1$s</string>
<string name="message_error_repository_wipe_failed">Failed to wipe repository: %1$s</string>
<string name="message_error_stage_update_failed">Error updating stage: %1$s</string>
<string name="message_error_synonyms_generation_failed">Failed to generate synonyms: %1$s</string>
<string name="message_error_translation_failed">Translation failed: %1$s</string>
<string name="message_error_vocabulary_import_failed">Error importing vocabulary items: %1$s</string>
<string name="message_info_generic">Info</string>
<string name="message_loading_card_set">Loading card set</string>
<string name="message_loading_generic">Loading…</string>
<string name="message_loading_grammar_fetch">Fetching grammar for %1$d items…</string>
<string name="message_loading_operation_in_progress">Operation in progress…</string>
<!-- Translation related -->
<string name="message_loading_translating">Translating %1$d words…</string>
<!-- Article removal -->
<string name="message_success_articles_removed">Articles removed successfully.</string>
<string name="message_success_cards_loaded">Successfully loaded card set.</string>
<string name="message_success_category_saved">Category saved to %1$s</string>
<!-- Category operations -->
<string name="message_success_category_updated">Category updated successfully.</string>
<!-- File operations -->
<string name="message_success_file_saved">File saved to %1$s</string>
<!-- Status Messages (for internationalization) -->
<string name="message_success_generic">Success!</string>
<!-- Grammar related -->
<string name="message_success_grammar_updated">Grammar details updated!</string>
<string name="message_success_items_added">Successfully added %1$d new vocabulary items.</string>
<string name="message_success_items_deleted">Successfully deleted vocabulary items.</string>
<string name="message_success_items_merged">Items merged!</string>
<string name="message_success_language_replaced">Language ID updated for %1$d items.</string>
<!-- Repository operations -->
<string name="message_success_repository_wiped">All repository data deleted.</string>
<!-- Stage operations -->
<string name="message_success_stage_updated">Stage updated successfully.</string>
<!-- Synonyms -->
<string name="message_success_synonyms_generated">Synonyms generated successfully.</string>
<string name="message_success_translation_completed">Translation completed.</string>
<!-- Vocabulary related -->
<string name="message_success_vocabulary_imported">Vocabulary items imported successfully.</string>
<string name="message_test_error">Oops, something went wrong :(</string>
<string name="message_test_info">This is a generic info message.</string>
<string name="message_test_success">This is a test success message!</string>
<string name="min_correct_to_advance">Min. Correct to Advance</string>
<string name="model">Model</string>
@@ -445,12 +597,12 @@
<string name="no">No</string>
<string name="no_cards_found_for_the_selected_filters">No cards found for the selected filters.</string>
<string name="no_grammar_configuration_found_for_this_language">No grammar configuration found for this language.</string>
<string name="no_items_due_for_review">No items due for review today. Great job!</string>
<string name="no_items_without_grammar">No Items without Grammar</string>
<string name="no_model_selected_for_the_task">No model selected for the task: %1$s</string>
<string name="no_models_configured">No Models Configured</string>
<string name="no_models_found">No models found</string>
<string name="no_new_vocabulary_to_sort">No New Vocabulary to Sort</string>
<string name="no_items_due_for_review">No items due for review today. Great job!</string>
<string name="no_vocabulary_items_found_perhaps_try_changing_the_filters">No vocabulary items found. Perhaps try changing the filters?</string>
<string name="not_available">Not available</string>
@@ -569,6 +721,8 @@
<string name="result">Result</string>
<string name="review_intro">Review the generated vocabulary before adding it to your collection.</string>
<string name="right">Right</string>
<string name="scan_models">Scan models</string>
@@ -577,6 +731,7 @@
<string name="search_for_a_word_s_origin">Search for a word\'s origin</string>
<string name="search_models">Search Models</string>
<string name="search_topics_phrases">Search topics, phrases…</string>
<string name="secondary_button">Secondary Button</string>
<string name="secondary_inverse">Secondary Inverse</string>
@@ -619,11 +774,14 @@
<string name="sort_by_new_items">Sort by New Items</string>
<string name="sort_by_size">Sort by Size</string>
<string name="sort_new_vocabulary">Sort New Vocabulary</string>
<string name="sort_order_alphabetical">Alphabetical</string>
<string name="sort_order_language">Language</string>
<!-- Sort Order Options -->
<string name="sort_order_newest_first">Newest First</string>
<string name="sort_order_oldest_first">Oldest First</string>
<string name="sorting_hint_title">Vocabulary Sorting</string>
<string name="label_speaking_speed">Speaking Speed</string>
<string name="stage_1">Stage 1</string>
<string name="stage_2">Stage 2</string>
<string name="stage_3">Stage 3</string>
@@ -646,6 +804,15 @@
<string name="status_widget_faulty_items">Faulty Items</string>
<string name="status_widget_new_items">New Items</string>
<string name="strategy_keep_both">Keep Both</string>
<string name="strategy_keep_both_desc">Add all words from the pack as new entries.</string>
<string name="strategy_merge">Merge (Recommended)</string>
<string name="strategy_merge_desc">Keep existing progress; merge categories intelligently.</string>
<string name="strategy_replace">Replace Existing</string>
<string name="strategy_replace_desc">Overwrite matching words with the pack version.</string>
<string name="strategy_skip">Skip Duplicates</string>
<string name="strategy_skip_desc">Only add words that don\'t already exist.</string>
<string name="subjunctive">Subjunctive</string>
<string name="synonym_exists">Synonym exists</string>
@@ -655,9 +822,10 @@
<string name="system_default_font">System Default Font</string>
<string name="system_theme">System Theme</string>
<string name="tap_the_words_below_to_form_the_sentence">Tap the words below to form the sentence</string>
<string name="tab_ai_definition">AI Definition</string>
<string name="tab_downloaded">Downloaded</string>
<string name="label_target_correct_answers_per_day">Target Correct Answers Per Day</string>
<string name="tap_the_words_below_to_form_the_sentence">Tap the words below to form the sentence</string>
<string name="test">Test</string>
@@ -675,12 +843,14 @@
<string name="text_a_simple_list_to">A simple list to manually sort your vocabulary</string>
<string name="text_add_custom_language">Add Custom Language</string>
<string name="text_add_grammar_details">Add grammar details</string>
<string name="text_add_new_word_to_list">Extract a New Word to Your List</string>
<string name="text_add_to_favorites">Add to favorites</string>
<string name="text_ai_failed_to_create_the_exercise">AI failed to create the exercise.</string>
<string name="text_ai_generation_failed_with_an_exception">AI generation failed with an exception</string>
<string name="text_all_dictionaries_deleted_successfully">All dictionaries deleted successfully</string>
<string name="text_all_items_completed">All items completed!</string>
<string name="text_all_languages">All Languages</string>
<string name="text_already_in_your_library">Already in your Library</string>
<string name="text_amount_2d">Amount: %1$d</string>
<string name="text_amount_2d_questions">Amount: %1$d Questions</string>
<string name="text_amount_of_cards">Amount of cards</string>
@@ -712,6 +882,7 @@
<string name="text_check_your_matches">Check your matches!</string>
<string name="text_checksum_mismatch_for_expected_got">Checksum mismatch for %1$s. Expected: %2$s, Got: %3$s</string>
<string name="text_claude">Claude</string>
<string name="text_clipboard_empty">Clipboard is empty</string>
<string name="text_collapse_widget">Collapse Widget</string>
<string name="text_color_palette">Color Palette</string>
<string name="text_common">Common</string>
@@ -722,8 +893,12 @@
<string name="text_copy_corrected_text">Copy corrected text</string>
<string name="text_correct_em">Correct!</string>
<string name="text_could_not_fetch_a_new_word">Could not fetch a new word.</string>
<string name="text_could_not_load_packs">Could not load packs</string>
<string name="text_customize_the_intervals">Customize the intervals and criteria for moving vocabulary cards between stages. Cards in lower stages should be asked more often than those in higher stages.</string>
<string name="text_d_cards">%1$d cards</string>
<string name="text_d_words_will_be_added">%1$d words will be added to your library.</string>
<string name="text_daily_goal_description">How many words do you want to answer correctly each day?</string>
<string name="text_daily_review_placeholder">Daily review screen - implementation pending</string>
<string name="text_dark">Dark</string>
<string name="text_day_streak">Day Streak</string>
<string name="text_days">" days"</string>
@@ -733,6 +908,9 @@
<string name="text_delete_category">Delete Category</string>
<string name="text_delete_custom_language">Delete custom language</string>
<string name="text_delete_vocabulary_item">Delete Vocabulary Item?</string>
<string name="text_desc_no_activity_data_available">No activity data available</string>
<string name="text_desc_organize_vocabulary_groups">Organize Your Vocabulary in Groups</string>
<string name="text_description_dictionary_prompt">Set a model for generating dictionary content and give optional instructions.</string>
<string name="text_developed_by_jonas_gaudian">Developed by Jonas Gaudian\n</string>
<string name="text_dialog_delete_key">Are you sure you want to delete the Key for this Provider?</string>
<string name="text_dialog_delete_model">Are you sure you want to delete the model \"%1$s\" from %2$s? This action cannot be undone.</string>
@@ -742,7 +920,9 @@
<string name="text_dictionary_manager_description">You can download dictionaries for certain languages which can be used insteaf of AI generation for dictionary content.</string>
<string name="text_difficulty_2d">Difficulty: %1$s</string>
<string name="text_do_you_want_to_minimize_the_app">Do you want to minimize the app?</string>
<string name="text_dont_see_what_looking_for">Don\'t see what you\'re looking for?</string>
<string name="text_download_failed_http">Download failed: HTTP %1$d %2$s</string>
<string name="text_downloading">Downloading…</string>
<string name="text_drag_to_reorder">Drag to Reorder</string>
<string name="text_due_today">"Due Today"</string>
<string name="text_due_today_only">Due Today Only</string>
@@ -768,9 +948,9 @@
<string name="text_error_generating_questions">Error generating questions: %1$s</string>
<string name="text_error_loading_stored_values">Error loading stored values: %1$s</string>
<string name="text_error_saving_entry">Error saving entry: %1$s</string>
<string name="message_error_excel_not_supported">Excel is not supported. Use CSV instead.</string>
<string name="text_expand_widget">Expand Widget</string>
<string name="text_explanation">Explanation</string>
<string name="text_explore_more_categories">Explore more categories</string>
<string name="text_export_category">Export Category</string>
<string name="text_failed_to_delete_dictionary">Failed to delete dictionary: %1$s</string>
<string name="text_failed_to_delete_orphaned_file">Failed to delete orphaned file: %1$s</string>
@@ -791,25 +971,30 @@
<string name="text_generate_exercise_with_ai">Generate Exercise with AI</string>
<string name="text_generating_questions_from_video">Generating questions from video…</string>
<string name="text_get_api_key_at">Get API Key at %1$s</string>
<string name="text_translation_instructions">Set model for translation and give optional instructions on how to translate.</string>
<string name="text_here_you_can_set_a_custom_">Here you can set a custom prompt for the AI vocabulary model. This allows you to define how new vocabulary entries are generated.</string>
<string name="text_hint">Hint</string>
<string name="text_how_handle_duplicates">How should duplicates be handled?</string>
<string name="text_importing_d_words">Importing %1$d words…</string>
<string name="text_in_progress">In Progress</string>
<string name="text_incorrect_em">Incorrect!</string>
<string name="text_infrequent">Rare</string>
<string name="label_interval_settings_in_days">Interval Settings</string>
<string name="text_key_active">Key Active</string>
<string name="text_key_optional">Key Optional</string>
<string name="text_label_word">Enter a word\n</string>
<string name="text_language_code">Language Code</string>
<string name="text_language_direction_explanation">You can set an optional preference which language should come first or second.</string>
<string name="text_language_direction_disabled_with_pairs">Clear language pair selection to choose a direction.</string>
<string name="text_language_direction_explanation">You can set an optional preference which language should come first or second.</string>
<string name="text_language_options">Language Options</string>
<string name="text_language_settings_description">Set what languages you want to use in the app. Languages that are not activated will not appear in this app. You can also add your own language to the list, or change an existing language (region/locale)</string>
<string name="text_last_7_days">Last 7 Days</string>
<string name="text_light">Light</string>
<string name="text_list">List</string>
<string name="text_loading_3d">Loading…</string>
<string name="text_loading_packs">Loading packs…</string>
<!-- Pack Preview Dialog -->
<string name="text_loading_preview">Loading preview…</string>
<string name="text_manual_vocabulary_list">Manual vocabulary list</string>
<string name="text_mastered_final_level">You\'ve mastered the final level!</string>
<string name="text_mismatch_between_question_ids_in_exercise_and_questions_found_in_repository">Mismatch between question IDs in exercise and questions found in repository.</string>
<string name="text_mistral">Mistral</string>
<string name="text_more_options">More options</string>
@@ -823,6 +1008,7 @@
<string name="text_no_items_available">No items available</string>
<string name="text_no_key">No Key</string>
<string name="text_no_models_found">No models found</string>
<string name="text_no_packs_match_search">No packs match your search.</string>
<string name="text_no_valid_api_configuration_could_be_found">No valid API configuration could be found in the settings. Before using this app, you have to configure at least one API provider.</string>
<string name="text_no_vocabulary_available">No vocabulary available.</string>
<string name="text_no_vocabulary_due_today">No Vocabulary Due Today</string>
@@ -844,6 +1030,7 @@
<string name="text_remove_from_favorites">Remove from favorites</string>
<string name="text_repeat_wrong">Repeat Wrong</string>
<string name="text_repeat_wrong_guesses">Repeat Wrong Guesses</string>
<string name="text_request_pack_desc">Don\'t see what you need? Let me know and I\'ll add it!</string>
<string name="text_required_enter_a_human_readable_name">Required: Enter a human-readable name</string>
<string name="text_required_enter_the_exact_model_identifier">Required: Enter the exact model identifier</string>
<string name="text_reset_intro">Reset Intro</string>
@@ -852,6 +1039,7 @@
<string name="text_save_key">Save Key</string>
<string name="text_save_prompt">Save Prompt</string>
<string name="text_scan_for_available_models">Scan for Available Models</string>
<string name="text_search">Search</string>
<string name="text_search_3d">Search…</string>
<string name="text_search_history">Search History</string>
<string name="text_search_term">Search Term</string>
@@ -894,6 +1082,7 @@
<string name="text_training_mode">Training Mode</string>
<string name="text_training_mode_description">Training mode is enabled: answers wont affect progress.</string>
<string name="text_translation">Enter translation</string>
<string name="text_translation_instructions">Set model for translation and give optional instructions on how to translate.</string>
<string name="text_translation_will_appear_here">Translation will appear here</string>
<string name="text_true">True</string>
<string name="text_try_first_finding_the_word_on">Try first finding the word on Wiktionary before generating AI response</string>
@@ -925,7 +1114,11 @@
<string name="title_corrector">Corrector</string>
<string name="title_developer_options">Developer Options</string>
<!-- Explore Packs Screen -->
<string name="title_explore_packs">Explore Packs</string>
<string name="title_http_status_codes">HTTP Status Codes</string>
<!-- Conflict Strategy Dialog -->
<string name="title_import_pack">Import \"%1$s\"</string>
<string name="title_items_without_grammar">Items Without Grammar</string>
<string name="title_multiple">Multiple</string>
<string name="title_settings">Settings</string>
@@ -943,7 +1136,6 @@
<string name="translate_the_following_d">Translate the following (%1$s):</string>
<string name="translation_prompt_settings">Translation Prompt Settings</string>
<string name="label_translation_server">Translation Server</string>
<string name="try_again">Try Again</string>
@@ -954,7 +1146,6 @@
<string name="vocabulary_added_successfully">Vocabulary Added</string>
<string name="vocabulary_repository">Vocabulary Repository</string>
<string name="label_vocabulary_settings">Progress Settings</string>
<string name="website_url">Website URL</string>
@@ -971,158 +1162,8 @@
<string name="words_known">%1$d Words Known</string>
<string name="words_required">%1$d words required</string>
<string name="label_wrong_answers">Wrong answers</string>
<string name="label_yes">Yes</string>
<string name="text_mastered_final_level">You\'ve mastered the final level!</string>
<string name="label_your_answer">Your Answer</string>
<string name="your_language_journey">Your Language Journey</string>
<string name="label_start_required">* required</string>
<string name="label_no_history_yet">No history yet</string>
<string name="cd_play">Play</string>
<string name="label_pronunciation">Pronunc iation</string>
<string name="text_clipboard_empty">Clipboard is empty</string>
<string name="label_paste">Paste</string>
<string name="label_target_tone">Target Tone:</string>
<string name="label_grammar_only">Grammar only</string>
<string name="label_declension">Declension</string>
<string name="label_variations">Variations</string>
<string name="label_auto_cycle_dev">Auto Cycle (Dev)</string>
<string name="label_regenerate">Regenerate</string>
<string name="label_read_aloud">Read Aloud</string>
<string name="label_all_categories">All Categories</string>
<string name="text_description_dictionary_prompt">Set a model for generating dictionary content and give optional instructions.</string>
<string name="hint_vocabulary_progress_hint_title">Vocabulary Progress Tracking</string>
<string name="hint_title_hints_overview">Help and Instructions</string>
<string name="hint_hints_overview_intro">Help Center</string>
<string name="hint_hints_overview_description">All hints that are in this app can be found here as well.</string>
<string name="hint_hints_header_basics">Getting Started</string>
<string name="hint_hints_header_vocabulary">Vocabulary Management</string>
<string name="hint_hints_header_advanced">Advanced Features</string>
<string name="category_hint_intro">You can create two types of categories to organize your vocabulary:</string>
<string name="review_intro">Review the generated vocabulary before adding it to your collection.</string>
<string name="duplicate">Duplicate</string>
<string name="hint_scan_hint_title">Finding the right AI model</string>
<string name="hint_translate_how_it_works">How translation works</string>
<string name="label_no_category">None</string>
<string name="text_search">Search</string>
<string name="text_language_settings_description">Set what languages you want to use in the app. Languages that are not activated will not appear in this app. You can also add your own language to the list, or change an existing language (region/locale)</string>
<!-- Status Messages (for internationalization) -->
<string name="message_success_generic">Success!</string>
<string name="message_info_generic">Info</string>
<string name="message_error_generic">An error occurred</string>
<string name="message_loading_generic">Loading…</string>
<!-- Language related -->
<string name="message_error_language_not_selected">Source and target languages must be selected.</string>
<string name="message_error_no_words_found">No words found in the provided text.</string>
<string name="message_success_language_replaced">Language ID updated for %1$d items.</string>
<!-- Vocabulary related -->
<string name="message_success_vocabulary_imported">Vocabulary items imported successfully.</string>
<string name="message_error_vocabulary_import_failed">Error importing vocabulary items: %1$s</string>
<string name="message_success_items_merged">Items merged!</string>
<string name="message_success_items_added">Successfully added %1$d new vocabulary items.</string>
<string name="message_error_items_add_failed">Error adding items: %1$s</string>
<string name="message_success_items_deleted">Successfully deleted vocabulary items.</string>
<string name="message_error_items_delete_failed">Error deleting items: %1$s</string>
<string name="message_error_no_cards_found">No cards found for the specified filter.</string>
<string name="message_success_cards_loaded">Successfully loaded card set.</string>
<!-- Grammar related -->
<string name="message_success_grammar_updated">Grammar details updated!</string>
<string name="message_error_grammar_fetch_failed">Could not retrieve grammar details.</string>
<string name="message_loading_grammar_fetch">Fetching grammar for %1$d items…</string>
<!-- File operations -->
<string name="message_success_file_saved">File saved to %1$s</string>
<string name="message_error_file_save_failed">Error saving file: %1$s</string>
<string name="message_error_file_save_cancelled">File save cancelled or failed.</string>
<string name="message_error_file_picker_not_initialized">Save File Launcher not initialized.</string>
<string name="message_success_category_saved">Category saved to %1$s</string>
<!-- API Key related -->
<string name="message_error_api_key_missing">API Key is missing or invalid.</string>
<string name="message_error_api_key_invalid">API Key is missing or invalid.</string>
<!-- Translation related -->
<string name="message_loading_translating">Translating %1$d words…</string>
<string name="message_success_translation_completed">Translation completed.</string>
<string name="message_error_translation_failed">Translation failed: %1$s</string>
<!-- Repository operations -->
<string name="message_success_repository_wiped">All repository data deleted.</string>
<string name="message_error_repository_wipe_failed">Failed to wipe repository: %1$s</string>
<string name="message_loading_card_set">Loading card set</string>
<!-- Stage operations -->
<string name="message_success_stage_updated">Stage updated successfully.</string>
<string name="message_error_stage_update_failed">Error updating stage: %1$s</string>
<!-- Category operations -->
<string name="message_success_category_updated">Category updated successfully.</string>
<string name="message_error_category_update_failed">Error updating category: %1$s</string>
<!-- Article removal -->
<string name="message_success_articles_removed">Articles removed successfully.</string>
<string name="message_error_articles_remove_failed">Error removing articles: %1$s</string>
<!-- Synonyms -->
<string name="message_success_synonyms_generated">Synonyms generated successfully.</string>
<string name="message_error_synonyms_generation_failed">Failed to generate synonyms: %1$s</string>
<!-- Operation status -->
<string name="message_error_operation_failed">Operation failed: %1$s</string>
<string name="message_loading_operation_in_progress">Operation in progress…</string>
<string name="message_test_info">This is a generic info message.</string>
<string name="message_test_success">This is a test success message!</string>
<string name="message_test_error">Oops, something went wrong :(</string>
<string name="label_stats">Stats</string>
<string name="label_library">Library</string>
<string name="label_edit">Edit</string>
<string name="label_new_words">New Words</string>
<string name="desc_expand_your_vocabulary">Expand your vocabulary</string>
<string name="label_settings">Settings</string>
<string name="label_2d_days">%1$d Days</string>
<string name="label_current_streak">Current Streak</string>
<string name="label_daily_goal">Daily Goal</string>
<string name="label_daily_review">Daily Review</string>
<string name="desc_daily_review_due">%1$d words need attention</string>
<string name="text_daily_review_placeholder">Daily review screen - implementation pending</string>
<string name="text_desc_no_activity_data_available">No activity data available</string>
<string name="label_see_history">See History</string>
<string name="label_weekly_progress">Weekly Progress</string>
<string name="cd_go">Go</string>
<string name="label_sort_by">Sort By</string>
<string name="label_reset">Reset</string>
<string name="label_filter_cards">Filter Cards</string>
<string name="text_desc_organize_vocabulary_groups">Organize Your Vocabulary in Groups</string>
<string name="text_add_new_word_to_list">Extract a New Word to Your List</string>
<string name="cd_scroll_to_top">Scroll to top</string>
<string name="cd_settings">Settings</string>
<string name="label_import_csv">Import CSV</string>
<string name="label_ai_generator">AI Generator</string>
<string name="label_new_wordss">New Words</string>
<string name="label_recently_added">Recently Added</string>
<string name="label_view_all">View All</string>
<string name="text_explore_more_categories">Explore more categories</string>
<string name="cd_options">Options</string>
<string name="cd_selected">Selected</string>
<string name="label_all_cards">All Cards</string>
<string name="cd_filter_options">Filter options</string>
<string name="cd_add">Add</string>
<string name="cd_searchh">Search</string>
<string name="label_search_cards">Search cards</string>
<string name="label_learnedd">learned</string>
<string name="label_all_categoriess">All Categories</string>
<string name="label_show_more">Show More</string>
<!-- Sort Order Options -->
<string name="sort_order_newest_first">Newest First</string>
<string name="sort_order_oldest_first">Oldest First</string>
<string name="sort_order_alphabetical">Alphabetical</string>
<string name="sort_order_language">Language</string>
<!-- Explore Packs Hint -->
<string name="hint_explore_packs_title">About Vocabulary Packs</string>
</resources>