From 88000cac172f51188c3ac03772dfbc1832d15fe3 Mon Sep 17 00:00:00 2001 From: openhands Date: Tue, 15 Sep 2026 22:58:55 -0400 Subject: [PATCH 1/3] =?UTF-8?q?feat(i18n):=20agregar=20traducci=C3=B3n=20a?= =?UTF-8?q?l=20ingl=C3=A9s=20(values-en)=20y=20extraer=20strings=20hardcod?= =?UTF-8?q?eados=20de=20feature/*=20a=20recursos=20(#3)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../code/feature/focus/FocusScreen.kt | 44 +- .../code/feature/games/BinaryGuessGame.kt | 29 +- .../code/feature/games/BugHuntScreen.kt | 76 +-- .../code/feature/games/BugSmasherGame.kt | 18 +- .../code/feature/games/CodeReviewScreen.kt | 22 +- .../code/feature/games/GamesScreen.kt | 8 +- .../code/feature/games/GitRescueScreen.kt | 154 +++--- .../code/feature/games/HackathonScreen.kt | 22 +- .../code/feature/games/MinigamesDialog.kt | 20 +- .../code/feature/games/RefactorRushScreen.kt | 47 +- .../code/feature/games/RockPaperSciGame.kt | 40 +- .../code/feature/home/HomeScreen.kt | 29 +- .../code/feature/learn/LearnScreen.kt | 62 +-- .../feature/onboarding/OnboardingScreen.kt | 2 +- .../code/feature/settings/AboutScreen.kt | 34 +- .../code/feature/settings/SettingsScreen.kt | 32 +- .../code/feature/shop/ShopScreen.kt | 32 +- .../code/feature/skills/SeasonPassScreen.kt | 26 +- .../code/feature/skills/SkillTreeScreen.kt | 16 +- .../feature/skills/WeeklyMissionsScreen.kt | 10 +- app/src/main/res/values-en/strings.xml | 474 ++++++++++++++++++ app/src/main/res/values/strings.xml | 344 +++++++++++++ 22 files changed, 1205 insertions(+), 336 deletions(-) create mode 100644 app/src/main/res/values-en/strings.xml diff --git a/app/src/main/java/com/tamagotchi/code/feature/focus/FocusScreen.kt b/app/src/main/java/com/tamagotchi/code/feature/focus/FocusScreen.kt index 059fd96..0d32a19 100644 --- a/app/src/main/java/com/tamagotchi/code/feature/focus/FocusScreen.kt +++ b/app/src/main/java/com/tamagotchi/code/feature/focus/FocusScreen.kt @@ -17,12 +17,14 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontFamily 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.lifecycle.compose.collectAsStateWithLifecycle +import com.tamagotchi.code.R import com.tamagotchi.code.data.database.PetStateEntity import com.tamagotchi.code.ui.theme.LocalAppTheme import com.tamagotchi.code.data.database.StudySessionEntity @@ -37,11 +39,11 @@ fun FocusScreen( studySessions: List ) { var activeTab by remember { mutableIntStateOf(0) } - val tabs = listOf("Temporizador", "Bitácora") + val tabs = listOf(stringResource(R.string.focus_tab_timer), stringResource(R.string.focus_tab_logs)) Column(modifier = Modifier.fillMaxWidth()) { Text( - text = ">>> MODO FOCO", + text = stringResource(R.string.focus_header), fontSize = 14.sp, fontFamily = FontFamily.Monospace, color = Color(0xFF81C784), @@ -97,7 +99,7 @@ fun TimerPanel( var tempTopic by remember { mutableStateOf("Kotlin") } var showDropdown by remember { mutableStateOf(false) } - val topics = listOf("Kotlin", "JavaScript", "PHP", "Python", "SQL", "Clean Code", "Git", "Estructuras de Datos") + val topics = listOf("Kotlin", "JavaScript", "PHP", "Python", "SQL", "Clean Code", "Git", stringResource(R.string.topic_data_structures)) Column( modifier = Modifier.fillMaxWidth(), @@ -111,7 +113,7 @@ fun TimerPanel( modifier = Modifier.fillMaxWidth().padding(bottom = 8.dp) ) { Text( - text = "MODO NO MOLESTAR ACTIVO: XP x1.5 en sesiones de foco", + text = stringResource(R.string.focus_dnd_banner), fontFamily = FontFamily.Monospace, fontSize = 10.sp, color = Color(0xFFFFB74D), @@ -122,7 +124,7 @@ fun TimerPanel( } if (!isRunning) { Text( - text = ">>> INICIAR BITÁCORA DE ESTUDIO", + text = stringResource(R.string.focus_start_logs), fontSize = 13.sp, fontFamily = FontFamily.Monospace, color = Color(0xFF81C784), @@ -136,7 +138,7 @@ fun TimerPanel( verticalAlignment = Alignment.CenterVertically ) { Text( - text = "Materia:", + text = stringResource(R.string.focus_topic_label), fontSize = 12.sp, fontFamily = FontFamily.Monospace, color = Color.White, @@ -166,7 +168,7 @@ fun TimerPanel( ) Icon( imageVector = Icons.Default.ArrowDropDown, - contentDescription = "Cambiar", + contentDescription = stringResource(R.string.focus_change), tint = Color(0xFF81C784) ) } @@ -204,7 +206,7 @@ fun TimerPanel( verticalAlignment = Alignment.CenterVertically ) { Text( - text = "Tiempo:", + text = stringResource(R.string.focus_time_label), fontSize = 12.sp, fontFamily = FontFamily.Monospace, color = Color.White, @@ -231,7 +233,7 @@ fun TimerPanel( modifier = Modifier.fillMaxSize() ) { Text( - text = "$mins min", + text = stringResource(R.string.focus_min_label, mins), color = if (isSelected) Color.Black else Color(0xFF81C784), fontSize = 11.sp, fontWeight = FontWeight.Bold, @@ -246,7 +248,7 @@ fun TimerPanel( Spacer(modifier = Modifier.height(16.dp)) Text( - text = "Al estudiar: El Tamagotchi entrará en modo concentrado. Completar la sesión te recompensa con Bytes y XP de estudio, pero drenará energía mental.", + text = stringResource(R.string.focus_studying_desc), fontSize = 11.sp, fontFamily = FontFamily.Monospace, color = Color.Gray, @@ -270,7 +272,7 @@ fun TimerPanel( .testTag("timer_start_button") ) { Text( - text = "EMPEZAR COMPILACIÓN", + text = stringResource(R.string.focus_start_button), color = Color.Black, fontWeight = FontWeight.Bold, fontFamily = FontFamily.Monospace @@ -278,7 +280,7 @@ fun TimerPanel( } } else { Text( - text = ">>> MODO: COMPILANDO HORAS DE ESTUDIO", + text = stringResource(R.string.focus_compiling_header), fontSize = 12.sp, fontFamily = FontFamily.Monospace, color = Color(0xFFFFB74D), @@ -287,7 +289,7 @@ fun TimerPanel( ) Spacer(modifier = Modifier.height(4.dp)) Text( - text = "Enfoque: $currentTopic", + text = stringResource(R.string.focus_enfoque, currentTopic), fontSize = 11.sp, fontFamily = FontFamily.Monospace, color = Color.Gray, @@ -309,7 +311,7 @@ fun TimerPanel( textAlign = TextAlign.Center ) Text( - text = "Mascota: ${state.name} está estudiando contigo...", + text = stringResource(R.string.focus_pet_studying, state.name), fontSize = 11.sp, fontFamily = FontFamily.Monospace, color = Color.Gray, @@ -333,7 +335,7 @@ fun TimerPanel( .testTag("timer_cancel_button") ) { Text( - text = "CANCELAR COMPILACIÓN", + text = stringResource(R.string.focus_cancel_button), color = Color.Black, fontWeight = FontWeight.Bold, fontFamily = FontFamily.Monospace, @@ -355,7 +357,7 @@ fun LogsPanel( Column(modifier = Modifier.fillMaxWidth()) { Text( - text = ">>> BITÁCORA DE COMPILACIÓN (ESTUDIOS)", + text = stringResource(R.string.focus_logs_header), fontSize = 13.sp, fontFamily = FontFamily.Monospace, color = Color(0xFF81C784), @@ -372,7 +374,7 @@ fun LogsPanel( contentAlignment = Alignment.Center ) { Text( - text = "[Sin registros de estudio aún]\n¡Empieza un temporizador para compilar tus primeras horas!", + text = stringResource(R.string.focus_logs_empty), fontSize = 11.sp, fontFamily = FontFamily.Monospace, color = Color.Gray, @@ -402,7 +404,7 @@ fun LogsPanel( ) { Column { Text( - text = "Estudio: ${session.topic}", + text = stringResource(R.string.focus_logs_entry, session.topic), fontWeight = FontWeight.Bold, fontFamily = FontFamily.Monospace, fontSize = 12.sp, @@ -417,7 +419,7 @@ fun LogsPanel( } Text( - text = "+${session.durationMinutes} Min", + text = stringResource(R.string.focus_logs_min, session.durationMinutes), color = Color(0xFF81C784), fontWeight = FontWeight.Bold, fontFamily = FontFamily.Monospace, @@ -439,14 +441,14 @@ fun LogsPanel( ) { Column(modifier = Modifier.padding(8.dp)) { Text( - text = "[!] NOTA DEL COMPILADOR DE VIDA:", + text = stringResource(R.string.focus_note_title), fontSize = 10.sp, fontFamily = FontFamily.Monospace, color = Color(0xFFFFB74D), fontWeight = FontWeight.Bold ) Text( - text = "Si dejas de registrar sesiones por más de 36 horas, tu racha de estudio se reiniciará a 0, y tu mascota perderá salud por falta de mantenimiento.", + text = stringResource(R.string.focus_note_body), fontSize = 10.sp, fontFamily = FontFamily.Monospace, color = Color.LightGray, diff --git a/app/src/main/java/com/tamagotchi/code/feature/games/BinaryGuessGame.kt b/app/src/main/java/com/tamagotchi/code/feature/games/BinaryGuessGame.kt index 0be016d..8bcb522 100644 --- a/app/src/main/java/com/tamagotchi/code/feature/games/BinaryGuessGame.kt +++ b/app/src/main/java/com/tamagotchi/code/feature/games/BinaryGuessGame.kt @@ -12,11 +12,13 @@ import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontFamily 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 com.tamagotchi.code.R import com.tamagotchi.code.data.database.PetStateEntity import com.tamagotchi.code.ui.viewmodel.PetViewModel @@ -29,7 +31,8 @@ fun BinaryGuessGame( var round by remember { mutableStateOf(1) } var score by remember { mutableStateOf(0) } var currentSecretBit by remember { mutableStateOf((0..1).random()) } - var feedbackMessage by remember { mutableStateOf("¿Cuál crees que es el bit secreto?") } + var feedbackRes by remember { mutableStateOf(R.string.binary_prompt) } + var feedbackBit by remember { mutableStateOf(0) } var showNextButton by remember { mutableStateOf(false) } var isGameOver by remember { mutableStateOf(false) } @@ -38,7 +41,7 @@ fun BinaryGuessGame( horizontalAlignment = Alignment.CenterHorizontally ) { Text( - text = "ADIVINA EL BIT (Ronda $round de 5)", + text = stringResource(R.string.binary_header, round), fontSize = 11.sp, fontFamily = FontFamily.Monospace, fontWeight = FontWeight.Bold, @@ -54,7 +57,7 @@ fun BinaryGuessGame( contentAlignment = Alignment.Center ) { Text( - text = if (isGameOver) "FIN" else if (showNextButton) "$currentSecretBit" else "?", + text = if (isGameOver) stringResource(R.string.binary_fin) else if (showNextButton) "$currentSecretBit" else "?", fontSize = 42.sp, fontWeight = FontWeight.Bold, fontFamily = FontFamily.Monospace, @@ -64,7 +67,7 @@ fun BinaryGuessGame( Spacer(modifier = Modifier.height(16.dp)) Text( - text = feedbackMessage, + text = stringResource(feedbackRes, feedbackBit), fontSize = 12.sp, fontFamily = FontFamily.Monospace, color = Color.White, @@ -76,7 +79,7 @@ fun BinaryGuessGame( val bytesReward = score * 4 val healthReward = score * 3f Text( - text = "¡Juego Terminado!\nAcertaste: $score de 5\nRecompensa: +$bytesReward Bytes, +${healthReward.toInt()}% Felicidad", + text = stringResource(R.string.binary_gameover, score, bytesReward, healthReward.toInt()), fontSize = 12.sp, fontFamily = FontFamily.Monospace, color = Color(0xFFFFD54F), @@ -93,7 +96,7 @@ fun BinaryGuessGame( shape = RoundedCornerShape(8.dp), modifier = Modifier.fillMaxWidth() ) { - Text("Cobrar Recompensas", fontFamily = FontFamily.Monospace) + Text(stringResource(R.string.game_claim_rewards), fontFamily = FontFamily.Monospace) } } else if (showNextButton) { Button( @@ -101,7 +104,7 @@ fun BinaryGuessGame( if (round < 5) { round += 1 currentSecretBit = (0..1).random() - feedbackMessage = "¿Cuál crees que es el bit secreto?" + feedbackRes = R.string.binary_prompt showNextButton = false } else { isGameOver = true @@ -111,7 +114,7 @@ fun BinaryGuessGame( shape = RoundedCornerShape(8.dp), modifier = Modifier.fillMaxWidth() ) { - Text("Siguiente Ronda", fontFamily = FontFamily.Monospace) + Text(stringResource(R.string.binary_next_round), fontFamily = FontFamily.Monospace) } } else { Row( @@ -123,10 +126,11 @@ fun BinaryGuessGame( val isCorrect = currentSecretBit == 0 if (isCorrect) { score += 1 - feedbackMessage = "¡Excelente! El bit secreto era 0." + feedbackRes = R.string.binary_correct } else { - feedbackMessage = "Incorrecto. El bit secreto era 1." + feedbackRes = R.string.binary_wrong } + feedbackBit = currentSecretBit showNextButton = true }, colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF151D16)), @@ -142,10 +146,11 @@ fun BinaryGuessGame( val isCorrect = currentSecretBit == 1 if (isCorrect) { score += 1 - feedbackMessage = "¡Excelente! El bit secreto era 1." + feedbackRes = R.string.binary_correct } else { - feedbackMessage = "Incorrecto. El bit secreto era 0." + feedbackRes = R.string.binary_wrong } + feedbackBit = currentSecretBit showNextButton = true }, colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF151D16)), diff --git a/app/src/main/java/com/tamagotchi/code/feature/games/BugHuntScreen.kt b/app/src/main/java/com/tamagotchi/code/feature/games/BugHuntScreen.kt index dfcbcae..127693d 100644 --- a/app/src/main/java/com/tamagotchi/code/feature/games/BugHuntScreen.kt +++ b/app/src/main/java/com/tamagotchi/code/feature/games/BugHuntScreen.kt @@ -24,70 +24,70 @@ import kotlinx.coroutines.delay private data class BugSnippet( val lines: List, val bugIndex: Int, - val explanation: String, - val humor: String + val explanationRes: Int, + val humorRes: Int ) private val snippetPool = listOf( BugSnippet( listOf("val name = \"Codey\"", "println(name", "age++", "fun greet() { }"), bugIndex = 1, - explanation = "Falta el paréntesis de cierre en println.", - humor = "¡Era un paréntesis fugitivo! Se escapó sin pagar el alquiler." + explanationRes = R.string.bug_expl_1, + humorRes = R.string.bug_humor_1 ), BugSnippet( listOf("if (x = 5) {", "print(\"Cinco\")", "} else {", "print(\"Otro\")", "}"), bugIndex = 0, - explanation = "En Kotlin la comparación es == no =. = es asignación.", - humor = "Codey: ¡Un solo = es asignación, doble == es comparación! No confundas al compilador." + explanationRes = R.string.bug_expl_2, + humorRes = R.string.bug_humor_2 ), BugSnippet( listOf("fun add(a: Int, b: Int): Int {", "return a + b", "}", "add(2, 3)"), bugIndex = 1, - explanation = "Falta el tipo de retorno explícito, debería ser ': Int'.", - humor = "Codey: Hasta una calculadora de bolsillo sabe qué va a devolver." + explanationRes = R.string.bug_expl_3, + humorRes = R.string.bug_humor_3 ), BugSnippet( listOf("val nums = listOf(1, 2, 3)", "for i in nums {", "print(i)", "}"), bugIndex = 1, - explanation = "En Kotlin el for usa paréntesis: for (i in nums).", - humor = "Codey: ¡Los paréntesis no son decoración! Son parte de la sintaxis." + explanationRes = R.string.bug_expl_4, + humorRes = R.string.bug_humor_4 ), BugSnippet( listOf("fun main() {", "val msg = \"Hola\"", "println(msg)", "}//fin"), bugIndex = 0, - explanation = "main no necesita paréntesis vacíos si es la entrada.", - humor = "Codey: En realidad main() está bien, ¡pero este bug es tramposo!" + explanationRes = R.string.bug_expl_5, + humorRes = R.string.bug_humor_5 ), BugSnippet( listOf("val count = 0", "while (count < 5) {", "println(count)", "count--", "}"), bugIndex = 3, - explanation = "Está decrementando count en vez de incrementarlo. Bucle infinito.", - humor = "Codey: ¡Así nunca llegarás a 5! Es como correr hacia atrás." + explanationRes = R.string.bug_expl_6, + humorRes = R.string.bug_humor_6 ), BugSnippet( listOf("val data = \"123\"", "val number: Int = data", "println(number + 1)"), bugIndex = 1, - explanation = "No se puede asignar un String directamente a Int. Usa toInt().", - humor = "Codey: ¡No puedes convertir strings a Int por ósmosis! Usa .toInt()." + explanationRes = R.string.bug_expl_7, + humorRes = R.string.bug_humor_7 ), BugSnippet( listOf("fun isEven(n: Int) {", "return n % 2 == 0", "}", "val r = isEven(4)"), bugIndex = 0, - explanation = "La función debe declarar tipo de retorno: fun isEven(n: Int): Boolean.", - humor = "Codey: El compilador no es adivino. Dile qué vas a devolver." + explanationRes = R.string.bug_expl_8, + humorRes = R.string.bug_humor_8 ), BugSnippet( listOf("val items = listOf(1, 2, 3)", "items.add(4)", "println(items)"), bugIndex = 1, - explanation = "listOf crea una lista inmutable. Usa mutableListOf.", - humor = "Codey: ¡No puedes modificar una lista inmutable! Es como intentar cambiar el pasado." + explanationRes = R.string.bug_expl_9, + humorRes = R.string.bug_humor_9 ), BugSnippet( listOf("fun greet() {", "println(\"Hola\")", "", "", "", "}"), bugIndex = 2, - explanation = "Líneas vacías innecesarias. El código debe ser limpio.", - humor = "Codey: ¿Estás escribiendo código o una novela? Muy poético pero poco práctico." + explanationRes = R.string.bug_expl_10, + humorRes = R.string.bug_humor_10 ) ) @@ -104,9 +104,9 @@ fun BugHuntScreen( var timeRemaining by remember { mutableStateOf(60) } var score by remember { mutableStateOf(0) } var isGameOver by remember { mutableStateOf(false) } - var codeyReaction by remember { mutableStateOf("¡Encuentra el bug antes de que el compilador explote!") } - var lastExplanation by remember { mutableStateOf(null) } - var lastHumor by remember { mutableStateOf(null) } + var codeyReactionRes by remember { mutableStateOf(R.string.bug_initial) } + var lastExplanationRes by remember { mutableStateOf(null) } + var lastHumor by remember { mutableStateOf(null) } var answeredRound by remember { mutableStateOf(false) } val currentSnippet = snippets.getOrNull(currentRound - 1) @@ -148,7 +148,7 @@ fun BugHuntScreen( Text(stringResource(R.string.game_result_reward, bytesEarned)) Spacer(modifier = Modifier.height(8.dp)) Text( - text = "Acertaste $score de $totalRounds", + text = stringResource(R.string.bug_score, score, totalRounds), style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant ) @@ -184,7 +184,7 @@ fun BugHuntScreen( shape = RoundedCornerShape(8.dp), modifier = Modifier.fillMaxWidth() ) { - Text("Codey: $codeyReaction", modifier = Modifier.padding(16.dp), color = MaterialTheme.colorScheme.onPrimaryContainer) + Text(stringResource(R.string.bug_codey, stringResource(codeyReactionRes)), modifier = Modifier.padding(16.dp), color = MaterialTheme.colorScheme.onPrimaryContainer) } Spacer(modifier = Modifier.height(16.dp)) @@ -194,8 +194,8 @@ fun BugHuntScreen( if (currentSnippet != null) { currentSnippet.lines.forEachIndexed { index, line -> - val isCorrectLine = index == currentSnippet.bugIndex && answeredRound && lastExplanation != null - val isWrongPick = answeredRound && lastExplanation != null && index != currentSnippet.bugIndex + val isCorrectLine = index == currentSnippet.bugIndex && answeredRound && lastExplanationRes != null + val isWrongPick = answeredRound && lastExplanationRes != null && index != currentSnippet.bugIndex Surface( color = when { @@ -210,12 +210,12 @@ fun BugHuntScreen( .clickable(enabled = !answeredRound) { if (index == currentSnippet.bugIndex) { score++ - codeyReaction = currentSnippet.humor + codeyReactionRes = currentSnippet.humorRes } else { - codeyReaction = "¡Esa línea está bien! Sigue buscando..." + codeyReactionRes = R.string.bug_wrong_line } - lastExplanation = currentSnippet.explanation - lastHumor = currentSnippet.humor + lastExplanationRes = currentSnippet.explanationRes + lastHumor = currentSnippet.humorRes answeredRound = true } ) { @@ -227,7 +227,7 @@ fun BugHuntScreen( } } - if (answeredRound && lastExplanation != null) { + if (answeredRound && lastExplanationRes != null) { Spacer(modifier = Modifier.height(16.dp)) Surface( color = if (lastHumor != null && score > 0) Color(0xFF1B5E20).copy(alpha = 0.15f) else Color(0xFFB71C1C).copy(alpha = 0.1f), @@ -235,7 +235,7 @@ fun BugHuntScreen( modifier = Modifier.fillMaxWidth() ) { Text( - text = "Explicación: ${lastExplanation}", + text = stringResource(R.string.bug_explanation, stringResource(lastExplanationRes!!)), modifier = Modifier.padding(12.dp), fontFamily = FontFamily.Monospace, fontSize = MaterialTheme.typography.bodySmall.fontSize, @@ -248,14 +248,14 @@ fun BugHuntScreen( if (currentRound < totalRounds) { currentRound++ answeredRound = false - lastExplanation = null + lastExplanationRes = null lastHumor = null - codeyReaction = "¡Siguiente ronda! ¿Dónde se esconde el bug?" + codeyReactionRes = R.string.bug_next_round } else { isGameOver = true } }) { - Text(if (currentRound < totalRounds) "Siguiente ronda" else "Ver resultados") + Text(if (currentRound < totalRounds) stringResource(R.string.game_next_round) else stringResource(R.string.game_see_results)) } } } diff --git a/app/src/main/java/com/tamagotchi/code/feature/games/BugSmasherGame.kt b/app/src/main/java/com/tamagotchi/code/feature/games/BugSmasherGame.kt index b47605f..3d03c41 100644 --- a/app/src/main/java/com/tamagotchi/code/feature/games/BugSmasherGame.kt +++ b/app/src/main/java/com/tamagotchi/code/feature/games/BugSmasherGame.kt @@ -14,10 +14,12 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import com.tamagotchi.code.R import com.tamagotchi.code.data.database.PetStateEntity import com.tamagotchi.code.ui.viewmodel.PetViewModel @@ -44,7 +46,7 @@ fun BugSmasherGame( horizontalAlignment = Alignment.CenterHorizontally ) { Text( - text = "CAZA DE BUGS", + text = stringResource(R.string.smasher_header), fontSize = 11.sp, fontFamily = FontFamily.Monospace, fontWeight = FontWeight.Bold, @@ -54,7 +56,7 @@ fun BugSmasherGame( if (!isStarted) { Text( - text = "Toca los bugs que aparecen en la cuadrícula de 3x3 tan rápido como puedas. ¡Tienes 10 segundos!", + text = stringResource(R.string.smasher_desc), fontSize = 11.sp, fontFamily = FontFamily.Monospace, color = Color.White, @@ -68,13 +70,13 @@ fun BugSmasherGame( shape = RoundedCornerShape(8.dp), modifier = Modifier.fillMaxWidth() ) { - Text("¡Comenzar!", fontFamily = FontFamily.Monospace) + Text(stringResource(R.string.game_start), fontFamily = FontFamily.Monospace) } } else if (timeRemaining <= 0) { val bytesReward = score * 2 val healthReward = (score * 1.5f).coerceAtMost(30f) Text( - text = "¡Tiempo Agotado!\nBugs atrapados: $score\nRecompensa: +$bytesReward Bytes, +${healthReward.toInt()}% Felicidad", + text = stringResource(R.string.smasher_gameover, score, bytesReward, healthReward.toInt()), fontSize = 12.sp, fontFamily = FontFamily.Monospace, color = Color(0xFFFFD54F), @@ -91,7 +93,7 @@ fun BugSmasherGame( shape = RoundedCornerShape(8.dp), modifier = Modifier.fillMaxWidth() ) { - Text("Cobrar Recompensas", fontFamily = FontFamily.Monospace) + Text(stringResource(R.string.game_claim_rewards), fontFamily = FontFamily.Monospace) } } else { Row( @@ -99,14 +101,14 @@ fun BugSmasherGame( horizontalArrangement = Arrangement.SpaceBetween ) { Text( - text = "Bugs: $score", + text = stringResource(R.string.smasher_bugs, score), fontSize = 12.sp, fontFamily = FontFamily.Monospace, fontWeight = FontWeight.Bold, color = Color.White ) Text( - text = "Tiempo: ${timeRemaining}s", + text = stringResource(R.string.smasher_time, timeRemaining), fontSize = 12.sp, fontFamily = FontFamily.Monospace, fontWeight = FontWeight.Bold, @@ -145,7 +147,7 @@ fun BugSmasherGame( if (isBug) { Icon( imageVector = Icons.Default.BugReport, - contentDescription = "BUG", + contentDescription = stringResource(R.string.smasher_bug_cd), tint = Color(0xFFEF5350), modifier = Modifier.size(36.dp) ) diff --git a/app/src/main/java/com/tamagotchi/code/feature/games/CodeReviewScreen.kt b/app/src/main/java/com/tamagotchi/code/feature/games/CodeReviewScreen.kt index 3386847..8559148 100644 --- a/app/src/main/java/com/tamagotchi/code/feature/games/CodeReviewScreen.kt +++ b/app/src/main/java/com/tamagotchi/code/feature/games/CodeReviewScreen.kt @@ -15,11 +15,13 @@ import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontFamily 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 com.tamagotchi.code.R import com.tamagotchi.code.data.CodeReviewData import com.tamagotchi.code.data.CodeReviewSnippet import com.tamagotchi.code.ui.viewmodel.PetViewModel @@ -48,7 +50,7 @@ fun CodeReviewScreen( horizontalAlignment = Alignment.CenterHorizontally ) { Text( - text = ">>> CODE REVIEW: Ronda ${currentRound + 1}/5", + text = stringResource(R.string.review_round, currentRound + 1), fontFamily = FontFamily.Monospace, fontWeight = FontWeight.Bold, fontSize = 18.sp, @@ -94,7 +96,7 @@ fun CodeReviewScreen( ) { Icon(Icons.Default.CheckCircle, contentDescription = null) Spacer(modifier = Modifier.width(8.dp)) - Text("APROBAR", fontFamily = FontFamily.Monospace, fontWeight = FontWeight.Bold) + Text(stringResource(R.string.review_approve), fontFamily = FontFamily.Monospace, fontWeight = FontWeight.Bold) } Button( @@ -109,7 +111,7 @@ fun CodeReviewScreen( ) { Icon(Icons.Default.BugReport, contentDescription = null) Spacer(modifier = Modifier.width(8.dp)) - Text("SOLICITAR CAMBIOS", fontFamily = FontFamily.Monospace, fontWeight = FontWeight.Bold) + Text(stringResource(R.string.review_request_changes), fontFamily = FontFamily.Monospace, fontWeight = FontWeight.Bold) } } } else { @@ -121,7 +123,7 @@ fun CodeReviewScreen( ) { Column(modifier = Modifier.padding(16.dp)) { Text( - text = if (isCorrect == true) "¡ACERTADO!" else "INCORRECTO", + text = if (isCorrect == true) stringResource(R.string.review_hit) else stringResource(R.string.review_miss), fontWeight = FontWeight.Bold, fontFamily = FontFamily.Monospace, color = if (isCorrect == true) Color(0xFF81C784) else Color(0xFFEF5350) @@ -146,7 +148,7 @@ fun CodeReviewScreen( modifier = Modifier.fillMaxWidth(), colors = ButtonDefaults.buttonColors(containerColor = Color.DarkGray) ) { - Text("CONTINUAR", fontFamily = FontFamily.Monospace) + Text(stringResource(R.string.review_continue), fontFamily = FontFamily.Monospace) } } } @@ -176,7 +178,7 @@ fun CodeReviewResult( horizontalAlignment = Alignment.CenterHorizontally ) { Text( - text = "REVISIÓN COMPLETADA", + text = stringResource(R.string.review_done), fontFamily = FontFamily.Monospace, fontSize = 24.sp, fontWeight = FontWeight.Bold, @@ -184,14 +186,14 @@ fun CodeReviewResult( ) Spacer(modifier = Modifier.height(16.dp)) Text( - text = "Puntuación: $score / $total", + text = stringResource(R.string.review_score, score, total), fontFamily = FontFamily.Monospace, fontSize = 18.sp, color = Color.White ) Spacer(modifier = Modifier.height(24.dp)) Text( - text = "¡Buen trabajo SR. Developer!", + text = stringResource(R.string.review_congrats), fontFamily = FontFamily.Monospace, fontSize = 14.sp, color = Color.Gray, @@ -199,7 +201,7 @@ fun CodeReviewResult( ) Spacer(modifier = Modifier.height(8.dp)) Text( - text = "+$bytesEarned Bytes\n+$xpEarned XP", + text = stringResource(R.string.review_reward, bytesEarned, xpEarned), fontFamily = FontFamily.Monospace, fontSize = 16.sp, fontWeight = FontWeight.Bold, @@ -212,7 +214,7 @@ fun CodeReviewResult( modifier = Modifier.fillMaxWidth().height(50.dp), colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF2E7D32)) ) { - Text("VOLVER AL TERMINAL", fontFamily = FontFamily.Monospace, fontWeight = FontWeight.Bold) + Text(stringResource(R.string.review_back), fontFamily = FontFamily.Monospace, fontWeight = FontWeight.Bold) } } } diff --git a/app/src/main/java/com/tamagotchi/code/feature/games/GamesScreen.kt b/app/src/main/java/com/tamagotchi/code/feature/games/GamesScreen.kt index 9e7e6ef..41b1e9f 100644 --- a/app/src/main/java/com/tamagotchi/code/feature/games/GamesScreen.kt +++ b/app/src/main/java/com/tamagotchi/code/feature/games/GamesScreen.kt @@ -103,8 +103,8 @@ fun GamesScreen( // Game 4: Code Review GameCard( - title = "Code Review", - description = "Revisa snippets de código y detecta bugs.", + title = stringResource(R.string.games_code_review_title), + description = stringResource(R.string.games_code_review_desc), canPlay = canPlay("code_review"), onCooldownText = stringResource(R.string.games_cooldown), onClick = { handleGameClick("code_review", onNavigateToCodeReview) } @@ -112,8 +112,8 @@ fun GamesScreen( // Game 5: Hackatón Semanal GameCard( - title = "Hackatón Semanal", - description = "Desafío de algoritmo de fin de semana.", + title = stringResource(R.string.games_hackathon_title), + description = stringResource(R.string.games_hackathon_desc), canPlay = canPlay("hackathon"), onCooldownText = stringResource(R.string.games_cooldown), onClick = { handleGameClick("hackathon", onNavigateToHackathon) } diff --git a/app/src/main/java/com/tamagotchi/code/feature/games/GitRescueScreen.kt b/app/src/main/java/com/tamagotchi/code/feature/games/GitRescueScreen.kt index acc9788..0d90237 100644 --- a/app/src/main/java/com/tamagotchi/code/feature/games/GitRescueScreen.kt +++ b/app/src/main/java/com/tamagotchi/code/feature/games/GitRescueScreen.kt @@ -20,101 +20,101 @@ import com.tamagotchi.code.ui.viewmodel.PetViewModel import kotlinx.coroutines.delay private data class GitScenario( - val situation: String, - val options: List, + val situationRes: Int, + val optionsRes: List, val correctIndex: Int, - val explanation: String, - val humor: String + val explanationRes: Int, + val humorRes: Int ) private val scenarioPool = listOf( GitScenario( - situation = "Acabas de clonar el repo y vas a trabajar en un nuevo feature. ¿Qué haces primero?", - options = listOf( - "git checkout -b feature/nuevo", - "git commit -m \"inicio\"", - "git push --force" + situationRes = R.string.git_sit_1, + optionsRes = listOf( + R.string.git_opt_1a, + R.string.git_opt_1b, + R.string.git_opt_1c ), correctIndex = 0, - explanation = "Crear una rama nueva desde main es la forma correcta de empezar un feature.", - humor = "Codey: ¡Nunca pushes a main sin antes crear una rama! Eres un cowboy." + explanationRes = R.string.git_expl_1, + humorRes = R.string.git_humor_1 ), GitScenario( - situation = "Has hecho cambios locales y te das cuenta de que rompiste todo. Quieres volver al último commit limpio.", - options = listOf( - "git reset --hard HEAD", - "git revert HEAD", - "git rm -rf ." + situationRes = R.string.git_sit_2, + optionsRes = listOf( + R.string.git_opt_2a, + R.string.git_opt_2b, + R.string.git_opt_2c ), correctIndex = 1, - explanation = "revert crea un nuevo commit que deshace los cambios, preservando la historia.", - humor = "Codey: reset --hard es como una máquina del tiempo sin frenos. revert es más seguro." + explanationRes = R.string.git_expl_2, + humorRes = R.string.git_humor_2 ), GitScenario( - situation = "Tienes un conflicto en un merge. ¿Cuál es el siguiente paso?", - options = listOf( - "Resolver el conflicto en el editor y luego git add", - "git commit --amend", - "git push --force origin main" + situationRes = R.string.git_sit_3, + optionsRes = listOf( + R.string.git_opt_3a, + R.string.git_opt_3b, + R.string.git_opt_3c ), correctIndex = 0, - explanation = "Los conflictos se resuelven editando los archivos, luego git add y git commit.", - humor = "Codey: push --force no resuelve conflictos, los empeora. Es como echar gasolina al fuego." + explanationRes = R.string.git_expl_3, + humorRes = R.string.git_humor_3 ), GitScenario( - situation = "Trabajas en equipo y necesitas traerte los cambios más recientes de la rama main a tu rama feature.", - options = listOf( - "git pull origin main", - "git merge feature main", - "git branch -d main" + situationRes = R.string.git_sit_4, + optionsRes = listOf( + R.string.git_opt_4a, + R.string.git_opt_4b, + R.string.git_opt_4c ), correctIndex = 0, - explanation = "git pull trae los cambios de main a tu rama actual. merge lo haría al revés.", - humor = "Codey: No borres main. Nunca borres main. Es como borrar el diccionario." + explanationRes = R.string.git_expl_4, + humorRes = R.string.git_humor_4 ), GitScenario( - situation = "Hiciste un commit pero olvidaste incluir un archivo. ¿Cómo lo arreglas?", - options = listOf( - "git add archivo && git commit --amend", - "git reset --hard HEAD~1", - "git commit --allow-empty" + situationRes = R.string.git_sit_5, + optionsRes = listOf( + R.string.git_opt_5a, + R.string.git_opt_5b, + R.string.git_opt_5c ), correctIndex = 0, - explanation = "Con amend puedes agregar archivos al commit anterior sin crear uno nuevo.", - humor = "Codey: --allow-empty no arregla nada. Es como poner un post-it en una puerta cerrada." + explanationRes = R.string.git_expl_5, + humorRes = R.string.git_humor_5 ), GitScenario( - situation = "Quieres ver qué archivos modificaste antes de hacer commit. ¿Qué comando usas?", - options = listOf( - "git diff", - "git status", - "git show" + situationRes = R.string.git_sit_6, + optionsRes = listOf( + R.string.git_opt_6a, + R.string.git_opt_6b, + R.string.git_opt_6c ), correctIndex = 1, - explanation = "git status muestra el estado actual: archivos modificados, nuevos y eliminados.", - humor = "Codey: diff es para ver el contenido exacto, status te da el resumen. Dos herramientas diferentes." + explanationRes = R.string.git_expl_6, + humorRes = R.string.git_humor_6 ), GitScenario( - situation = "El historial de commits está lleno de mensajes como 'fix' y 'update'. Quieres limpiarlo antes de hacer merge.", - options = listOf( - "git rebase -i HEAD~5", - "git reset --hard origin/main", - "git commit --fixup" + situationRes = R.string.git_sit_7, + optionsRes = listOf( + R.string.git_opt_7a, + R.string.git_opt_7b, + R.string.git_opt_7c ), correctIndex = 0, - explanation = "rebase interactivo te permite squash, reordenar y renombrar commits.", - humor = "Codey: Los mensajes 'fix' y 'update' son como decir 'cosa' en un examen. Sé descriptivo." + explanationRes = R.string.git_expl_7, + humorRes = R.string.git_humor_7 ), GitScenario( - situation = "Tu rama feature quedó atrás de main y necesitas actualizarla sin crear commits de merge.", - options = listOf( - "git rebase main", - "git merge main", - "git pull --rebase" + situationRes = R.string.git_sit_8, + optionsRes = listOf( + R.string.git_opt_8a, + R.string.git_opt_8b, + R.string.git_opt_8c ), correctIndex = 2, - explanation = "pull --rebase trae cambios y los aplica encima de tus commits locales.", - humor = "Codey: El histórico lineal es como una carretera recta. Los merges son rotondas." + explanationRes = R.string.git_expl_8, + humorRes = R.string.git_humor_8 ) ) @@ -129,8 +129,8 @@ fun GitRescueScreen( var currentStep by remember { mutableStateOf(0) } var score by remember { mutableStateOf(0) } var isGameOver by remember { mutableStateOf(false) } - var codeyReaction by remember { mutableStateOf("¡Rápido, el repo está en llamas!") } - var lastExplanation by remember { mutableStateOf(null) } + var codeyReactionRes by remember { mutableStateOf(R.string.git_initial) } + var lastExplanationRes by remember { mutableStateOf(null) } var answeredStep by remember { mutableStateOf(false) } var branchPosition by remember { mutableStateOf(0f) } var maxBranchSteps by remember { mutableStateOf(scenarios.size.toFloat()) } @@ -162,7 +162,7 @@ fun GitRescueScreen( Text(stringResource(R.string.game_result_reward, bytesEarned)) Spacer(modifier = Modifier.height(8.dp)) Text( - text = "Decisiones correctas: $score de ${scenarios.size}", + text = stringResource(R.string.git_decisions, score, scenarios.size), style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant ) @@ -182,7 +182,7 @@ fun GitRescueScreen( modifier = Modifier.fillMaxWidth() ) { Column(modifier = Modifier.padding(12.dp)) { - Text("Progreso de la rama:", style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant) + Text(stringResource(R.string.git_progress), style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant) Spacer(modifier = Modifier.height(8.dp)) Row( modifier = Modifier.fillMaxWidth(), @@ -229,17 +229,17 @@ fun GitRescueScreen( shape = RoundedCornerShape(8.dp), modifier = Modifier.fillMaxWidth() ) { - Text("Codey: $codeyReaction", modifier = Modifier.padding(16.dp), color = MaterialTheme.colorScheme.onPrimaryContainer) + Text(stringResource(R.string.codey_says, stringResource(codeyReactionRes)), modifier = Modifier.padding(16.dp), color = MaterialTheme.colorScheme.onPrimaryContainer) } Spacer(modifier = Modifier.height(16.dp)) - Text("Paso ${currentStep + 1} de ${scenarios.size}", fontWeight = FontWeight.Bold) + Text(stringResource(R.string.git_step, currentStep + 1, scenarios.size), fontWeight = FontWeight.Bold) Spacer(modifier = Modifier.height(8.dp)) - Text(scenarios[currentStep].situation, style = MaterialTheme.typography.bodyLarge) + Text(stringResource(scenarios[currentStep].situationRes), style = MaterialTheme.typography.bodyLarge) Spacer(modifier = Modifier.height(16.dp)) - scenarios[currentStep].options.forEachIndexed { index, option -> + scenarios[currentStep].optionsRes.forEachIndexed { index, optionRes -> val isCorrect = answeredStep && index == scenarios[currentStep].correctIndex val isWrong = answeredStep && index != scenarios[currentStep].correctIndex @@ -256,23 +256,23 @@ fun GitRescueScreen( .clickable(enabled = !answeredStep) { if (index == scenarios[currentStep].correctIndex) { score++ - codeyReaction = scenarios[currentStep].humor + codeyReactionRes = scenarios[currentStep].humorRes } else { - codeyReaction = "¡Esa no era la mejor opción!" + codeyReactionRes = R.string.git_wrong } - lastExplanation = scenarios[currentStep].explanation + lastExplanationRes = scenarios[currentStep].explanationRes answeredStep = true } ) { Text( - text = "> $option", + text = "> " + stringResource(optionRes), fontFamily = FontFamily.Monospace, modifier = Modifier.padding(16.dp) ) } } - if (answeredStep && lastExplanation != null) { + if (answeredStep && lastExplanationRes != null) { Spacer(modifier = Modifier.height(12.dp)) Surface( color = Color(0xFF1B5E20).copy(alpha = 0.1f), @@ -280,7 +280,7 @@ fun GitRescueScreen( modifier = Modifier.fillMaxWidth() ) { Text( - text = lastExplanation!!, + text = stringResource(lastExplanationRes!!), modifier = Modifier.padding(12.dp), fontFamily = FontFamily.Monospace, fontSize = MaterialTheme.typography.bodySmall.fontSize, @@ -293,13 +293,13 @@ fun GitRescueScreen( if (currentStep < scenarios.size - 1) { currentStep++ answeredStep = false - lastExplanation = null - codeyReaction = "¡Siguiente decisión! El repo te necesita." + lastExplanationRes = null + codeyReactionRes = R.string.git_next } else { isGameOver = true } }) { - Text(if (currentStep < scenarios.size - 1) "Siguiente decisión" else "Ver resultados") + Text(if (currentStep < scenarios.size - 1) stringResource(R.string.game_next_decision) else stringResource(R.string.game_see_results)) } } } diff --git a/app/src/main/java/com/tamagotchi/code/feature/games/HackathonScreen.kt b/app/src/main/java/com/tamagotchi/code/feature/games/HackathonScreen.kt index 4c76837..ad310c9 100644 --- a/app/src/main/java/com/tamagotchi/code/feature/games/HackathonScreen.kt +++ b/app/src/main/java/com/tamagotchi/code/feature/games/HackathonScreen.kt @@ -10,12 +10,14 @@ import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontFamily 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.lifecycle.compose.collectAsStateWithLifecycle +import com.tamagotchi.code.R import com.tamagotchi.code.ui.viewmodel.HackathonData import com.tamagotchi.code.ui.viewmodel.PetViewModel import kotlin.math.min @@ -36,7 +38,7 @@ fun HackathonScreen( Icon(Icons.Default.EmojiEvents, contentDescription = null, tint = Color(0xFFFFD700)) Spacer(modifier = Modifier.width(8.dp)) Text( - text = ">>> HACKATHON SEMANAL", + text = stringResource(R.string.hack_header), fontFamily = FontFamily.Monospace, fontWeight = FontWeight.Bold, fontSize = 18.sp, @@ -45,7 +47,7 @@ fun HackathonScreen( } Text( - text = "Cada fin de semana un desafío especial de algoritmo.", + text = stringResource(R.string.hack_desc), fontFamily = FontFamily.Monospace, fontSize = 12.sp, color = Color.Gray @@ -59,14 +61,14 @@ fun HackathonScreen( ) { Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) { Text( - text = "Desafío de esta semana:", + text = stringResource(R.string.hack_this_week), fontFamily = FontFamily.Monospace, fontWeight = FontWeight.Bold, fontSize = 14.sp, color = Color(0xFF81C784) ) Text( - text = "Ordena este array sin usar sort()\n\nEntrada: [3, 7, 1, 9, 4, 2, 8, 5, 6]\nSalida esperada: [1, 2, 3, 4, 5, 6, 7, 8, 9]", + text = stringResource(R.string.hack_challenge), fontFamily = FontFamily.Monospace, fontSize = 11.sp, color = Color.LightGray, @@ -87,7 +89,7 @@ fun HackathonScreen( horizontalAlignment = Alignment.CenterHorizontally ) { Text( - text = "Intentos restantes: ${3 - data.attempts} / 3", + text = stringResource(R.string.hack_attempts, 3 - data.attempts), fontFamily = FontFamily.Monospace, fontSize = 14.sp, fontWeight = FontWeight.Bold, @@ -95,7 +97,7 @@ fun HackathonScreen( ) if (data.bestTimeMs < Long.MAX_VALUE) { Text( - text = "Mejor tiempo: ${data.bestTimeMs / 1000}s", + text = stringResource(R.string.hack_best_time, data.bestTimeMs / 1000), fontFamily = FontFamily.Monospace, fontSize = 11.sp, color = Color.Gray @@ -122,14 +124,14 @@ fun HackathonScreen( Icon(Icons.Default.PlayArrow, contentDescription = null) Spacer(modifier = Modifier.width(8.dp)) Text( - text = "SOLUCIONAR DESAFÍO", + text = stringResource(R.string.hack_solve), fontFamily = FontFamily.Monospace, fontWeight = FontWeight.Bold ) } Text( - text = "Recompensa: 200 XP + 500 Bytes + Skin exclusiva temporal", + text = stringResource(R.string.hack_reward), fontFamily = FontFamily.Monospace, fontSize = 10.sp, color = Color(0xFFFFB74D), @@ -138,7 +140,7 @@ fun HackathonScreen( ) } else { Text( - text = "Sin intentos restantes. ¡Espera al próximo hackathon!", + text = stringResource(R.string.hack_no_attempts), fontFamily = FontFamily.Monospace, fontSize = 12.sp, color = Color(0xFFEF5350), @@ -155,7 +157,7 @@ fun HackathonScreen( shape = RoundedCornerShape(8.dp), modifier = Modifier.fillMaxWidth().height(40.dp) ) { - Text("VOLVER", fontFamily = FontFamily.Monospace, fontWeight = FontWeight.Bold) + Text(stringResource(R.string.game_back), fontFamily = FontFamily.Monospace, fontWeight = FontWeight.Bold) } } } diff --git a/app/src/main/java/com/tamagotchi/code/feature/games/MinigamesDialog.kt b/app/src/main/java/com/tamagotchi/code/feature/games/MinigamesDialog.kt index 8f0a949..d107253 100644 --- a/app/src/main/java/com/tamagotchi/code/feature/games/MinigamesDialog.kt +++ b/app/src/main/java/com/tamagotchi/code/feature/games/MinigamesDialog.kt @@ -18,6 +18,8 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.compose.ui.window.Dialog +import androidx.compose.ui.res.stringResource +import com.tamagotchi.code.R import com.tamagotchi.code.data.database.PetStateEntity import com.tamagotchi.code.ui.viewmodel.PetViewModel @@ -48,7 +50,7 @@ fun MinigamesDialog( verticalAlignment = Alignment.CenterVertically ) { Text( - text = if (selectedGame == null) ">>> MINI-JUEGOS" else ">>> DETALLE DE JUEGO", + text = if (selectedGame == null) stringResource(R.string.mg_header) else stringResource(R.string.mg_detail), fontFamily = FontFamily.Monospace, fontSize = 12.sp, fontWeight = FontWeight.Bold, @@ -65,7 +67,7 @@ fun MinigamesDialog( ) { Icon( imageVector = if (selectedGame != null) Icons.AutoMirrored.Filled.KeyboardArrowLeft else Icons.Default.Close, - contentDescription = "Volver", + contentDescription = stringResource(R.string.mg_back), tint = Color.White ) } @@ -75,7 +77,7 @@ fun MinigamesDialog( when (selectedGame) { null -> { Text( - text = "Juega con ${state.name} para aumentar su felicidad y conseguir Bytes extra.", + text = stringResource(R.string.mg_intro, state.name), fontSize = 11.sp, fontFamily = FontFamily.Monospace, color = Color.LightGray, @@ -85,24 +87,24 @@ fun MinigamesDialog( Spacer(modifier = Modifier.height(16.dp)) GameOptionCard( - title = "Adivina el Bit", - description = "Intenta predecir el siguiente bit binario (0 o 1). Juego rápido de 5 rondas.", + title = stringResource(R.string.mg_binary_title), + description = stringResource(R.string.mg_binary_desc), icon = Icons.Default.Code, onClick = { selectedGame = "BINARY_GUESS" } ) Spacer(modifier = Modifier.height(8.dp)) GameOptionCard( - title = "Caza de Bugs", - description = "¡Rápido! Los bugs se están escapando. Atrapa todos los que puedas en 10 segundos.", + title = stringResource(R.string.mg_smasher_title), + description = stringResource(R.string.mg_smasher_desc), icon = Icons.Default.BugReport, onClick = { selectedGame = "BUG_SMASHER" } ) Spacer(modifier = Modifier.height(8.dp)) GameOptionCard( - title = "Servidor, Script, Hacker", - description = "Piedra, Papel o Tijera versión informática. ¡Derrota al compilador!", + title = stringResource(R.string.mg_rps_title), + description = stringResource(R.string.mg_rps_desc), icon = Icons.Default.Security, onClick = { selectedGame = "ROCK_PAPER_SCI" } ) diff --git a/app/src/main/java/com/tamagotchi/code/feature/games/RefactorRushScreen.kt b/app/src/main/java/com/tamagotchi/code/feature/games/RefactorRushScreen.kt index 445c355..6fbdaf2 100644 --- a/app/src/main/java/com/tamagotchi/code/feature/games/RefactorRushScreen.kt +++ b/app/src/main/java/com/tamagotchi/code/feature/games/RefactorRushScreen.kt @@ -19,13 +19,13 @@ import com.tamagotchi.code.R import com.tamagotchi.code.ui.viewmodel.PetViewModel private data class RefactorPuzzle( - val title: String, + val titleRes: Int, val blocks: List ) private val puzzleBank = listOf( RefactorPuzzle( - "Calcular total", + R.string.refactor_puzzle_1, listOf( "fun calculateTotal(items: List): Int {", " var total = 0", @@ -37,7 +37,7 @@ private val puzzleBank = listOf( ) ), RefactorPuzzle( - "Filtrar pares", + R.string.refactor_puzzle_2, listOf( "fun filterEven(numbers: List): List {", " val result = mutableListOf()", @@ -51,7 +51,7 @@ private val puzzleBank = listOf( ) ), RefactorPuzzle( - "Saludo personalizado", + R.string.refactor_puzzle_3, listOf( "fun greet(name: String, age: Int): String {", " val greeting = \"Hola, \$name\"", @@ -61,7 +61,7 @@ private val puzzleBank = listOf( ) ), RefactorPuzzle( - "Buscar máximo", + R.string.refactor_puzzle_4, listOf( "fun findMax(values: List): Int? {", " if (values.isEmpty()) return null", @@ -74,7 +74,7 @@ private val puzzleBank = listOf( ) ), RefactorPuzzle( - "Contar vocales", + R.string.refactor_puzzle_5, listOf( "fun countVowels(text: String): Int {", " val vowels = setOf('a', 'e', 'i', 'o', 'u')", @@ -87,7 +87,7 @@ private val puzzleBank = listOf( ) ), RefactorPuzzle( - "Invertir lista", + R.string.refactor_puzzle_6, listOf( "fun reverseList(items: List): List {", " val result = mutableListOf()", @@ -99,7 +99,7 @@ private val puzzleBank = listOf( ) ), RefactorPuzzle( - "Es palíndromo", + R.string.refactor_puzzle_7, listOf( "fun isPalindrome(word: String): Boolean {", " val cleaned = word.lowercase().filter { it.isLetter() }", @@ -108,7 +108,7 @@ private val puzzleBank = listOf( ) ), RefactorPuzzle( - "Promedio de notas", + R.string.refactor_puzzle_8, listOf( "fun average(grades: List): Double {", " if (grades.isEmpty()) return 0.0", @@ -118,7 +118,7 @@ private val puzzleBank = listOf( ) ), RefactorPuzzle( - "Generar rango", + R.string.refactor_puzzle_9, listOf( "fun generateRange(start: Int, end: Int): List {", " val range = mutableListOf()", @@ -130,7 +130,7 @@ private val puzzleBank = listOf( ) ), RefactorPuzzle( - "Validar email", + R.string.refactor_puzzle_10, listOf( "fun isValidEmail(email: String): Boolean {", " if (!email.contains('@')) return false", @@ -141,7 +141,7 @@ private val puzzleBank = listOf( ) ), RefactorPuzzle( - "Fibonacci", + R.string.refactor_puzzle_11, listOf( "fun fibonacci(n: Int): List {", " if (n <= 0) return emptyList()", @@ -154,7 +154,7 @@ private val puzzleBank = listOf( ) ), RefactorPuzzle( - "Capitalizar palabras", + R.string.refactor_puzzle_12, listOf( "fun capitalizeWords(sentence: String): String {", " return sentence.split(\" \")", @@ -176,7 +176,8 @@ fun RefactorRushScreen( val originalBlocks = remember { puzzle.blocks } var isGameOver by remember { mutableStateOf(false) } - var codeyReaction by remember { mutableStateOf("¡Ordena este desastre!") } + var codeyReactionRes by remember { mutableStateOf(R.string.refactor_initial) } + var reactionAttempts by remember { mutableIntStateOf(0) } var currentBlocks by remember { mutableStateOf(originalBlocks.shuffled()) } var attempts by remember { mutableStateOf(0) } @@ -203,10 +204,12 @@ fun RefactorRushScreen( fun checkResult() { attempts++ if (currentBlocks == originalBlocks) { - codeyReaction = "¡Excelente! Código limpio y ordenado en $attempts intento(s)." + codeyReactionRes = R.string.refactor_win + reactionAttempts = attempts isGameOver = true } else { - codeyReaction = "Sigue intentando, todavía no compila (intento $attempts)." + codeyReactionRes = R.string.refactor_keep_trying + reactionAttempts = attempts } } @@ -233,7 +236,7 @@ fun RefactorRushScreen( if (isGameOver) { Text(stringResource(R.string.game_result_title), style = MaterialTheme.typography.headlineMedium) Spacer(modifier = Modifier.height(8.dp)) - Text(puzzle.title, fontWeight = FontWeight.Bold, style = MaterialTheme.typography.titleMedium) + Text(stringResource(puzzle.titleRes), fontWeight = FontWeight.Bold, style = MaterialTheme.typography.titleMedium) Spacer(modifier = Modifier.height(16.dp)) val bytesEarned = maxOf(50 - (attempts * 5), 10) Text(stringResource(R.string.game_result_reward, bytesEarned)) @@ -251,12 +254,12 @@ fun RefactorRushScreen( shape = RoundedCornerShape(8.dp), modifier = Modifier.fillMaxWidth() ) { - Text("Codey: $codeyReaction", modifier = Modifier.padding(16.dp), color = MaterialTheme.colorScheme.onPrimaryContainer) + Text(stringResource(R.string.codey_says, stringResource(codeyReactionRes, reactionAttempts)), modifier = Modifier.padding(16.dp), color = MaterialTheme.colorScheme.onPrimaryContainer) } Spacer(modifier = Modifier.height(16.dp)) - Text(puzzle.title, fontWeight = FontWeight.Bold, style = MaterialTheme.typography.titleMedium) + Text(stringResource(puzzle.titleRes), fontWeight = FontWeight.Bold, style = MaterialTheme.typography.titleMedium) Spacer(modifier = Modifier.height(4.dp)) Text(stringResource(R.string.refactor_rush_instruction), style = MaterialTheme.typography.bodyMedium) @@ -276,10 +279,10 @@ fun RefactorRushScreen( ) { Column { IconButton(onClick = { moveUp(index) }, enabled = index > 0) { - Icon(Icons.Default.KeyboardArrowUp, contentDescription = "Up") + Icon(Icons.Default.KeyboardArrowUp, contentDescription = stringResource(R.string.game_move_up)) } IconButton(onClick = { moveDown(index) }, enabled = index < currentBlocks.size - 1) { - Icon(Icons.Default.KeyboardArrowDown, contentDescription = "Down") + Icon(Icons.Default.KeyboardArrowDown, contentDescription = stringResource(R.string.game_move_down)) } } Text( @@ -295,7 +298,7 @@ fun RefactorRushScreen( Spacer(modifier = Modifier.height(24.dp)) Button(onClick = { checkResult() }) { - Text("Verificar") + Text(stringResource(R.string.refactor_verify)) } } } diff --git a/app/src/main/java/com/tamagotchi/code/feature/games/RockPaperSciGame.kt b/app/src/main/java/com/tamagotchi/code/feature/games/RockPaperSciGame.kt index 7f35ee0..890d319 100644 --- a/app/src/main/java/com/tamagotchi/code/feature/games/RockPaperSciGame.kt +++ b/app/src/main/java/com/tamagotchi/code/feature/games/RockPaperSciGame.kt @@ -12,11 +12,14 @@ import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontFamily 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 com.tamagotchi.code.R import com.tamagotchi.code.data.database.PetStateEntity import com.tamagotchi.code.ui.viewmodel.PetViewModel @@ -26,13 +29,20 @@ fun RockPaperSciGame( viewModel: PetViewModel, onFinish: () -> Unit ) { + val context = LocalContext.current var userWins by remember { mutableStateOf(0) } var cpuWins by remember { mutableStateOf(0) } - var roundMessage by remember { mutableStateOf("Elige tu jugada para iniciar la ronda.") } + var roundMessage by remember { mutableStateOf(context.getString(R.string.rps_initial)) } var userChoice by remember { mutableStateOf(null) } var cpuChoice by remember { mutableStateOf(null) } var isGameOver by remember { mutableStateOf(false) } + fun choiceName(choice: String): String = when (choice) { + "Servidor" -> context.getString(R.string.rps_choice_server) + "Script" -> context.getString(R.string.rps_choice_script) + else -> context.getString(R.string.rps_choice_hacker) + } + val choices = listOf("Servidor", "Script", "Hacker") val icons = mapOf( "Servidor" to Icons.Default.Computer, @@ -45,7 +55,7 @@ fun RockPaperSciGame( horizontalAlignment = Alignment.CenterHorizontally ) { Text( - text = "SERVIDOR, SCRIPT, HACKER (RPS)", + text = stringResource(R.string.rps_header), fontSize = 11.sp, fontFamily = FontFamily.Monospace, fontWeight = FontWeight.Bold, @@ -54,7 +64,7 @@ fun RockPaperSciGame( Spacer(modifier = Modifier.height(10.dp)) Text( - text = "TÚ: $userWins | CPU: $cpuWins (Mejor de 3)", + text = stringResource(R.string.rps_score, userWins, cpuWins), fontSize = 12.sp, fontWeight = FontWeight.Bold, fontFamily = FontFamily.Monospace, @@ -72,31 +82,31 @@ fun RockPaperSciGame( verticalAlignment = Alignment.CenterVertically ) { Column(horizontalAlignment = Alignment.CenterHorizontally) { - Text("TÚ", fontSize = 10.sp, fontFamily = FontFamily.Monospace, color = Color.Gray) + Text(stringResource(R.string.rps_you), fontSize = 10.sp, fontFamily = FontFamily.Monospace, color = Color.Gray) Spacer(modifier = Modifier.height(4.dp)) Icon( imageVector = icons[userChoice] ?: Icons.Default.QuestionMark, - contentDescription = userChoice ?: "Pregunta", + contentDescription = userChoice?.let { choiceName(it) } ?: stringResource(R.string.rps_question), tint = if (userChoice != null) Color(0xFF81C784) else Color.Gray, modifier = Modifier.size(32.dp) ) Spacer(modifier = Modifier.height(4.dp)) - Text(userChoice ?: "Selecciona...", fontSize = 11.sp, fontFamily = FontFamily.Monospace, color = Color.White) + Text(userChoice?.let { choiceName(it) } ?: stringResource(R.string.rps_select), fontSize = 11.sp, fontFamily = FontFamily.Monospace, color = Color.White) } Text("VS", fontSize = 14.sp, fontWeight = FontWeight.Bold, fontFamily = FontFamily.Monospace, color = Color(0xFFEF5350)) Column(horizontalAlignment = Alignment.CenterHorizontally) { - Text("COMPILADOR", fontSize = 10.sp, fontFamily = FontFamily.Monospace, color = Color.Gray) + Text(stringResource(R.string.rps_compiler), fontSize = 10.sp, fontFamily = FontFamily.Monospace, color = Color.Gray) Spacer(modifier = Modifier.height(4.dp)) Icon( imageVector = icons[cpuChoice] ?: Icons.Default.QuestionMark, - contentDescription = cpuChoice ?: "Pregunta", + contentDescription = cpuChoice?.let { choiceName(it) } ?: stringResource(R.string.rps_question), tint = if (cpuChoice != null) Color(0xFFEF5350) else Color.Gray, modifier = Modifier.size(32.dp) ) Spacer(modifier = Modifier.height(4.dp)) - Text(cpuChoice ?: "Esperando...", fontSize = 11.sp, fontFamily = FontFamily.Monospace, color = Color.White) + Text(cpuChoice?.let { choiceName(it) } ?: stringResource(R.string.rps_waiting), fontSize = 11.sp, fontFamily = FontFamily.Monospace, color = Color.White) } } @@ -117,7 +127,7 @@ fun RockPaperSciGame( val bytesReward = if (playerWon) 20 else 5 val healthReward = if (playerWon) 25f else 10f Text( - text = if (playerWon) "¡Felicidades! Derrotaste al compilador.\nRecompensa: +$bytesReward Bytes, +${healthReward.toInt()}% Felicidad" else "Has perdido contra el compilador.\nRecompensa: +$bytesReward Bytes, +${healthReward.toInt()}% Felicidad", + text = stringResource(if (playerWon) R.string.rps_game_win else R.string.rps_game_lose, bytesReward, healthReward.toInt()), fontSize = 11.sp, fontFamily = FontFamily.Monospace, color = Color(0xFFFFD54F), @@ -134,7 +144,7 @@ fun RockPaperSciGame( shape = RoundedCornerShape(8.dp), modifier = Modifier.fillMaxWidth() ) { - Text("Cobrar Recompensas", fontFamily = FontFamily.Monospace) + Text(stringResource(R.string.game_claim_rewards), fontFamily = FontFamily.Monospace) } } else { Row( @@ -149,22 +159,22 @@ fun RockPaperSciGame( cpuChoice = selectedCpu if (choice == selectedCpu) { - roundMessage = "Empate en esta ronda con $choice." + roundMessage = context.getString(R.string.rps_draw, choiceName(choice)) } else if ( (choice == "Servidor" && selectedCpu == "Hacker") || (choice == "Script" && selectedCpu == "Servidor") || (choice == "Hacker" && selectedCpu == "Script") ) { userWins += 1 - roundMessage = "¡Ganaste la ronda! $choice vence a $selectedCpu." + roundMessage = context.getString(R.string.rps_round_win, choiceName(choice), choiceName(selectedCpu)) } else { cpuWins += 1 - roundMessage = "Perdiste la ronda. $selectedCpu vence a $choice." + roundMessage = context.getString(R.string.rps_round_lose, choiceName(selectedCpu), choiceName(choice)) } if (userWins >= 2 || cpuWins >= 2) { isGameOver = true - roundMessage = if (userWins >= 2) "¡Has ganado la partida!" else "El compilador ha ganado la partida." + roundMessage = context.getString(if (userWins >= 2) R.string.rps_match_win else R.string.rps_match_lose) } }, colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF151D16)), diff --git a/app/src/main/java/com/tamagotchi/code/feature/home/HomeScreen.kt b/app/src/main/java/com/tamagotchi/code/feature/home/HomeScreen.kt index 9da5126..56e064f 100644 --- a/app/src/main/java/com/tamagotchi/code/feature/home/HomeScreen.kt +++ b/app/src/main/java/com/tamagotchi/code/feature/home/HomeScreen.kt @@ -16,11 +16,13 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.tamagotchi.code.R import com.tamagotchi.code.data.database.PetStateEntity import com.tamagotchi.code.ui.components.DailyRewardDialog import com.tamagotchi.code.ui.components.CodeCardDialog @@ -73,7 +75,7 @@ fun HomeScreen( modifier = Modifier.size(16.dp) ) Text( - text = if (viewModel.isDailyRewardClaimedToday.value) "RECOMPENSAS DIARIAS (RECLAMADO)" else "RECOMPENSAS DIARIAS (¡DISPONIBLE!)", + text = if (viewModel.isDailyRewardClaimedToday.value) stringResource(R.string.home_daily_claimed) else stringResource(R.string.home_daily_available), fontFamily = FontFamily.Monospace, fontWeight = FontWeight.Bold, fontSize = 12.sp, @@ -91,11 +93,11 @@ fun HomeScreen( if (viewModel.showOfflineRewardDialog.value) { AlertDialog( onDismissRequest = { viewModel.dismissOfflineRewardDialog() }, - title = { Text("Sesión completada mientras estabas fuera") }, - text = { Text("¡Felicidades! Has ganado ${viewModel.offlineRewardXp.value} XP y ${viewModel.offlineRewardBytes.value} Bytes por tu sesión de Focus.") }, + title = { Text(stringResource(R.string.home_offline_title)) }, + text = { Text(stringResource(R.string.home_offline_text, viewModel.offlineRewardXp.value, viewModel.offlineRewardBytes.value)) }, confirmButton = { TextButton(onClick = { viewModel.dismissOfflineRewardDialog() }) { - Text("Aceptar") + Text(stringResource(R.string.dialog_accept)) } } ) @@ -120,7 +122,7 @@ fun HomeScreen( modifier = Modifier.size(24.dp) ) Text( - "${state?.name ?: "Codey"} ha muerto", + stringResource(R.string.home_death_title, state?.name ?: stringResource(R.string.default_pet_name)), fontFamily = FontFamily.Monospace, fontWeight = FontWeight.Bold ) @@ -129,8 +131,7 @@ fun HomeScreen( text = { Column { Text( - "Tu mascota ha fallecido por falta de cuidados.\n\n" + - "Pero no todo está perdido... puedes revivirla:", + stringResource(R.string.home_death_desc), fontFamily = FontFamily.Monospace, fontSize = 13.sp, lineHeight = 18.sp @@ -139,7 +140,7 @@ fun HomeScreen( Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(6.dp)) { Icon(Icons.Filled.Save, contentDescription = null, tint = MaterialTheme.colorScheme.primary, modifier = Modifier.size(16.dp)) Text( - "Revivir por ${viewModel.deathReviveCost.value} Bytes", + stringResource(R.string.home_revive_bytes_desc, viewModel.deathReviveCost.value), fontFamily = FontFamily.Monospace, fontSize = 12.sp, fontWeight = FontWeight.Bold, @@ -150,7 +151,7 @@ fun HomeScreen( Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(6.dp)) { Icon(Icons.Filled.Bolt, contentDescription = null, tint = MaterialTheme.colorScheme.secondary, modifier = Modifier.size(16.dp)) Text( - "Revivir gratis (stats bajos)", + stringResource(R.string.home_revive_free_desc), fontFamily = FontFamily.Monospace, fontSize = 12.sp, fontWeight = FontWeight.Bold, @@ -170,7 +171,7 @@ fun HomeScreen( Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(6.dp)) { Icon(Icons.Filled.Save, contentDescription = null, tint = MaterialTheme.colorScheme.onPrimary, modifier = Modifier.size(16.dp)) Text( - "Revivir (${viewModel.deathReviveCost.value} Bytes)", + stringResource(R.string.home_revive_bytes_button, viewModel.deathReviveCost.value), fontFamily = FontFamily.Monospace, fontWeight = FontWeight.Bold, fontSize = 12.sp @@ -186,7 +187,7 @@ fun HomeScreen( Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(6.dp)) { Icon(Icons.Filled.Bolt, contentDescription = null, tint = MaterialTheme.colorScheme.primary, modifier = Modifier.size(16.dp)) Text( - "Revivir gratis", + stringResource(R.string.home_revive_free), fontFamily = FontFamily.Monospace, fontWeight = FontWeight.Bold, fontSize = 12.sp @@ -217,7 +218,7 @@ fun HomeScreen( modifier = Modifier.size(24.dp) ) Text( - "Codey hizo commit", + stringResource(R.string.home_commit_title), fontFamily = FontFamily.Monospace, fontWeight = FontWeight.Bold ) @@ -226,7 +227,7 @@ fun HomeScreen( text = { Column { Text( - "Resumen de ayer:", + stringResource(R.string.home_commit_summary), fontFamily = FontFamily.Monospace, fontSize = 12.sp, color = MaterialTheme.colorScheme.onSurfaceVariant @@ -250,7 +251,7 @@ fun HomeScreen( }, confirmButton = { TextButton(onClick = { viewModel.dismissCommitDialog() }) { - Text("Fusionar (Aceptar)", fontFamily = FontFamily.Monospace) + Text(stringResource(R.string.home_commit_merge), fontFamily = FontFamily.Monospace) } } ) diff --git a/app/src/main/java/com/tamagotchi/code/feature/learn/LearnScreen.kt b/app/src/main/java/com/tamagotchi/code/feature/learn/LearnScreen.kt index bdd4b3a..e330f47 100644 --- a/app/src/main/java/com/tamagotchi/code/feature/learn/LearnScreen.kt +++ b/app/src/main/java/com/tamagotchi/code/feature/learn/LearnScreen.kt @@ -16,12 +16,14 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.scale import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontFamily 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.lifecycle.compose.collectAsStateWithLifecycle +import com.tamagotchi.code.R import com.tamagotchi.code.data.CodingChallenge import com.tamagotchi.code.data.database.PetStateEntity import com.tamagotchi.code.ui.viewmodel.PetViewModel @@ -37,7 +39,7 @@ fun LearnScreen( state: PetStateEntity ) { var activeTab by remember { mutableIntStateOf(0) } - val tabs = listOf("Retos Normales", "Retos Especiales", "Insignias") + val tabs = listOf(stringResource(R.string.learn_tab_quiz), stringResource(R.string.learn_tab_special), stringResource(R.string.learn_tab_badges)) Column(modifier = Modifier.fillMaxWidth()) { Row( @@ -47,14 +49,14 @@ fun LearnScreen( ) { Column(modifier = Modifier.weight(1f)) { Text( - text = ">>> APRENDER", + text = stringResource(R.string.learn_header), fontSize = 14.sp, fontFamily = FontFamily.Monospace, color = Color(0xFF81C784), fontWeight = FontWeight.Bold ) Text( - text = if (activeTab == 0) "Estudiando con ${state.name}" else "Desafíos de élite", + text = if (activeTab == 0) stringResource(R.string.learn_subtitle_studying, state.name) else stringResource(R.string.learn_subtitle_elite), fontSize = 11.sp, fontFamily = FontFamily.Monospace, color = Color.Gray @@ -113,7 +115,7 @@ fun BadgesPanel(viewModel: PetViewModel) { Column(modifier = Modifier.fillMaxWidth()) { Text( - text = ">>> INSIGNIAS POR LENGUAJE", + text = stringResource(R.string.badges_header), fontSize = 13.sp, fontFamily = FontFamily.Monospace, color = Color(0xFF81C784), @@ -122,7 +124,7 @@ fun BadgesPanel(viewModel: PetViewModel) { ) Spacer(modifier = Modifier.height(10.dp)) Text( - text = "Completa retos para desbloquear insignias: Bronce (50%), Plata (75%), Oro (100%)", + text = stringResource(R.string.badges_hint), fontSize = 11.sp, fontFamily = FontFamily.Monospace, color = Color.Gray @@ -181,7 +183,7 @@ fun BadgesPanel(viewModel: PetViewModel) { color = Color.White ) Text( - text = "$completed/$total retos ($pct%)", + text = stringResource(R.string.badges_progress, completed, total, pct), fontFamily = FontFamily.Monospace, fontSize = 10.sp, color = Color.Gray @@ -200,9 +202,9 @@ fun BadgesPanel(viewModel: PetViewModel) { enter = scaleIn() + fadeIn() ) { val badgeName = when (badgeLevel) { - 3 -> "ORO" - 2 -> "PLATA" - 1 -> "BRONCE" + 3 -> stringResource(R.string.badge_gold) + 2 -> stringResource(R.string.badge_silver) + 1 -> stringResource(R.string.badge_bronze) else -> "" } Text( @@ -230,7 +232,7 @@ fun QuizPanel(viewModel: PetViewModel) { Column(modifier = Modifier.fillMaxWidth()) { Text( - text = ">>> DESAFÍO DE PROGRAMACIÓN: Arena $currentLang", + text = stringResource(R.string.learn_arena, currentLang), fontSize = 13.sp, fontFamily = FontFamily.Monospace, color = Color(0xFF81C784), @@ -241,7 +243,7 @@ fun QuizPanel(viewModel: PetViewModel) { if (challengesList.isEmpty()) { Text( - text = "Cargando acertijos...", + text = stringResource(R.string.learn_loading), fontFamily = FontFamily.Monospace, color = Color.Gray, fontSize = 12.sp @@ -250,12 +252,10 @@ fun QuizPanel(viewModel: PetViewModel) { val challenge = challengesList[currentIndex] // Comentario dinámico de la mascota - val petTip = remember(challenge.id) { - when(challenge.type) { - "DEBUG" -> "¡Cuidado! Ese bug muerde. Revisa bien los puntos y coma." - "TRIVIA" -> "Esta es fácil... si has leído la documentación." - else -> "Concéntrate, mi CPU depende de tu respuesta." - } + val petTip = when(challenge.type) { + "DEBUG" -> stringResource(R.string.learn_tip_debug) + "TRIVIA" -> stringResource(R.string.learn_tip_trivia) + else -> stringResource(R.string.learn_tip_default) } Surface( @@ -274,7 +274,12 @@ fun QuizPanel(viewModel: PetViewModel) { } Text( - text = "Desafío ${currentIndex + 1} de ${challengesList.size} (${if (challenge.type == "DEBUG") "Desbuguear" else "Trivia"}):", + text = stringResource( + R.string.learn_challenge_counter, + currentIndex + 1, + challengesList.size, + if (challenge.type == "DEBUG") stringResource(R.string.learn_type_debug) else stringResource(R.string.learn_type_trivia) + ), fontSize = 11.sp, fontFamily = FontFamily.Monospace, color = Color.Gray @@ -388,7 +393,7 @@ fun QuizPanel(viewModel: PetViewModel) { ) Spacer(modifier = Modifier.width(6.dp)) Text( - text = if (fb == "CORRECT") "¡ACERTADO! +Alimento +Bytes +XP" else "RESPUESTA INCORRECTA", + text = if (fb == "CORRECT") stringResource(R.string.learn_correct) else stringResource(R.string.learn_incorrect), fontFamily = FontFamily.Monospace, fontWeight = FontWeight.Bold, fontSize = 12.sp, @@ -418,7 +423,7 @@ fun QuizPanel(viewModel: PetViewModel) { .testTag("quiz_next_button") ) { Text( - text = if (currentIndex + 1 < challengesList.size) "SIGUIENTE ACERTIJO" else "CARGAR MÁS RETOS", + text = if (currentIndex + 1 < challengesList.size) stringResource(R.string.learn_next) else stringResource(R.string.learn_load_more), color = Color.Black, fontWeight = FontWeight.Bold, fontFamily = FontFamily.Monospace, @@ -437,6 +442,7 @@ fun SpecialChallengesPanel(viewModel: PetViewModel, state: PetStateEntity) { var showFeedback by remember { mutableStateOf(null) } var unlockedThemeName by remember { mutableStateOf(null) } var isAnswered by remember { mutableStateOf(false) } + val allThemesUnlocked = stringResource(R.string.special_all_unlocked) Column( modifier = Modifier @@ -451,17 +457,17 @@ fun SpecialChallengesPanel(viewModel: PetViewModel, state: PetStateEntity) { verticalAlignment = Alignment.CenterVertically ) { Text( - text = "RETOS ESPECIALES", + text = stringResource(R.string.special_title), fontFamily = FontFamily.Monospace, fontWeight = FontWeight.Bold, fontSize = 18.sp, color = MaterialTheme.colorScheme.primary ) - Icon(Icons.Default.Star, contentDescription = "Especial", tint = MaterialTheme.colorScheme.primary) + Icon(Icons.Default.Star, contentDescription = stringResource(R.string.special_cd), tint = MaterialTheme.colorScheme.primary) } Text( - text = "Resuelve ejercicios avanzados de lógica y algoritmos para desbloquear nuevos temas visuales exclusivos.", + text = stringResource(R.string.special_desc), fontSize = 12.sp, color = MaterialTheme.colorScheme.onSurface, fontFamily = FontFamily.Monospace @@ -537,7 +543,7 @@ fun SpecialChallengesPanel(viewModel: PetViewModel, state: PetStateEntity) { unlockedThemeName = randomTheme viewModel.unlockTheme(randomTheme) } else { - unlockedThemeName = "¡Ya tienes todos!" + unlockedThemeName = allThemesUnlocked } } else { showFeedback = false @@ -563,7 +569,7 @@ fun SpecialChallengesPanel(viewModel: PetViewModel, state: PetStateEntity) { } if (isAnswered) { - val message = if (showFeedback == true) "¡Respuesta Correcta!" else "Incorrecto." + val message = if (showFeedback == true) stringResource(R.string.special_correct) else stringResource(R.string.special_incorrect) val color = if (showFeedback == true) Color(0xFF4CAF50) else Color(0xFFEF5350) Column( @@ -587,10 +593,10 @@ fun SpecialChallengesPanel(viewModel: PetViewModel, state: PetStateEntity) { textAlign = TextAlign.Center ) - if (showFeedback == true && unlockedThemeName != null && unlockedThemeName != "¡Ya tienes todos!") { + if (showFeedback == true && unlockedThemeName != null && unlockedThemeName != allThemesUnlocked) { Spacer(modifier = Modifier.height(8.dp)) Text( - text = "¡Nuevo tema desbloqueado: $unlockedThemeName!", + text = stringResource(R.string.special_theme_unlocked, unlockedThemeName!!), color = MaterialTheme.colorScheme.primary, fontWeight = FontWeight.Bold, fontFamily = FontFamily.Monospace, @@ -610,7 +616,7 @@ fun SpecialChallengesPanel(viewModel: PetViewModel, state: PetStateEntity) { }, colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.primary) ) { - Text("Siguiente Reto", color = MaterialTheme.colorScheme.onPrimary, fontFamily = FontFamily.Monospace) + Text(stringResource(R.string.special_next), color = MaterialTheme.colorScheme.onPrimary, fontFamily = FontFamily.Monospace) } } } diff --git a/app/src/main/java/com/tamagotchi/code/feature/onboarding/OnboardingScreen.kt b/app/src/main/java/com/tamagotchi/code/feature/onboarding/OnboardingScreen.kt index c3e3182..1e48ff8 100644 --- a/app/src/main/java/com/tamagotchi/code/feature/onboarding/OnboardingScreen.kt +++ b/app/src/main/java/com/tamagotchi/code/feature/onboarding/OnboardingScreen.kt @@ -510,7 +510,7 @@ private fun Step4Content( onNameChange(it) if (it.isNotEmpty()) onUseDefaultChange(false) }, - label = { Text("Nombre (máx 15)") }, + label = { Text(stringResource(R.string.onboarding_name_label)) }, singleLine = true, enabled = !useDefaultName, modifier = Modifier.fillMaxWidth(), diff --git a/app/src/main/java/com/tamagotchi/code/feature/settings/AboutScreen.kt b/app/src/main/java/com/tamagotchi/code/feature/settings/AboutScreen.kt index 86f87bf..a77f162 100644 --- a/app/src/main/java/com/tamagotchi/code/feature/settings/AboutScreen.kt +++ b/app/src/main/java/com/tamagotchi/code/feature/settings/AboutScreen.kt @@ -9,10 +9,12 @@ import androidx.compose.material3.* import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +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 com.tamagotchi.code.R @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -21,7 +23,7 @@ fun AboutScreen(onNavigateBack: () -> Unit) { containerColor = androidx.compose.ui.graphics.Color.Transparent, topBar = { TopAppBar( - title = { Text("Acerca de", fontWeight = FontWeight.Bold) }, + title = { Text(stringResource(R.string.about_title), fontWeight = FontWeight.Bold) }, navigationIcon = { IconButton(onClick = onNavigateBack) { Text("<", style = MaterialTheme.typography.titleLarge) @@ -50,7 +52,7 @@ fun AboutScreen(onNavigateBack: () -> Unit) { Spacer(modifier = Modifier.height(16.dp)) Text( - text = "Code Tamagotchi", + text = stringResource(R.string.app_name), style = MaterialTheme.typography.headlineMedium, fontWeight = FontWeight.Bold, textAlign = TextAlign.Center, @@ -60,7 +62,7 @@ fun AboutScreen(onNavigateBack: () -> Unit) { Spacer(modifier = Modifier.height(8.dp)) Text( - text = "Versión 1.0.0", + text = stringResource(R.string.about_version), style = MaterialTheme.typography.bodyLarge, color = MaterialTheme.colorScheme.onSurfaceVariant, textAlign = TextAlign.Center @@ -73,7 +75,7 @@ fun AboutScreen(onNavigateBack: () -> Unit) { Spacer(modifier = Modifier.height(24.dp)) Text( - text = "¿Qué es Code Tamagotchi?", + text = stringResource(R.string.about_what), style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold, modifier = Modifier.fillMaxWidth(), @@ -83,7 +85,7 @@ fun AboutScreen(onNavigateBack: () -> Unit) { Spacer(modifier = Modifier.height(8.dp)) Text( - text = "Code Tamagotchi es una aplicación que combina el aprendizaje de programación con el cuidado de una mascota virtual. Resuelve desafíos de código, completa sesiones de estudio y juega minijuegos para mantener a tu mascota feliz y saludable mientras mejoras tus habilidades de programación.", + text = stringResource(R.string.about_what_desc), style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant, textAlign = TextAlign.Start @@ -92,7 +94,7 @@ fun AboutScreen(onNavigateBack: () -> Unit) { Spacer(modifier = Modifier.height(16.dp)) Text( - text = "Características principales", + text = stringResource(R.string.about_features_title), style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold, modifier = Modifier.fillMaxWidth(), @@ -102,13 +104,13 @@ fun AboutScreen(onNavigateBack: () -> Unit) { Spacer(modifier = Modifier.height(8.dp)) val features = listOf( - "\u2022 Desafíos de código en Kotlin, JavaScript, PHP, Python y más", - "\u2022 Temporizador de estudio Pomodoro integrado", - "\u2022 Minijuegos: Bug Hunt, Git Rescue, Refactor Rush", - "\u2022 Mascota virtual con estados de ánimo y necesidades", - "\u2022 Múltiples temas visuales para personalizar la experiencia", - "\u2022 Sistema de logros y recompensas", - "\u2022 Widget para la pantalla de inicio" + stringResource(R.string.about_feature_1), + stringResource(R.string.about_feature_2), + stringResource(R.string.about_feature_3), + stringResource(R.string.about_feature_4), + stringResource(R.string.about_feature_5), + stringResource(R.string.about_feature_6), + stringResource(R.string.about_feature_7) ) features.forEach { feature -> @@ -127,7 +129,7 @@ fun AboutScreen(onNavigateBack: () -> Unit) { Spacer(modifier = Modifier.height(24.dp)) Text( - text = "Tecnologías usadas", + text = stringResource(R.string.about_tech_title), style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold, modifier = Modifier.fillMaxWidth(), @@ -137,7 +139,7 @@ fun AboutScreen(onNavigateBack: () -> Unit) { Spacer(modifier = Modifier.height(8.dp)) Text( - text = "Desarrollada con Jetpack Compose, Kotlin, Room, Koin y WorkManager.", + text = stringResource(R.string.about_tech), style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant ) @@ -145,7 +147,7 @@ fun AboutScreen(onNavigateBack: () -> Unit) { Spacer(modifier = Modifier.height(32.dp)) Text( - text = "\u00A9 2026 Code Tamagotchi. Todos los derechos reservados.", + text = stringResource(R.string.about_copyright), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant, textAlign = TextAlign.Center, diff --git a/app/src/main/java/com/tamagotchi/code/feature/settings/SettingsScreen.kt b/app/src/main/java/com/tamagotchi/code/feature/settings/SettingsScreen.kt index 361a52d..fcdad4f 100644 --- a/app/src/main/java/com/tamagotchi/code/feature/settings/SettingsScreen.kt +++ b/app/src/main/java/com/tamagotchi/code/feature/settings/SettingsScreen.kt @@ -220,7 +220,7 @@ fun SettingsScreen( // Default theme mode selector Text( - text = "Modo de tema base", + text = stringResource(R.string.settings_theme_mode), style = MaterialTheme.typography.titleSmall, fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.primary, @@ -232,7 +232,11 @@ fun SettingsScreen( .padding(horizontal = 16.dp), horizontalArrangement = Arrangement.spacedBy(8.dp) ) { - listOf("Sistema" to "SYSTEM", "Claro" to "LIGHT", "Oscuro" to "DARK").forEach { (label, mode) -> + listOf( + stringResource(R.string.theme_mode_system) to "SYSTEM", + stringResource(R.string.theme_mode_light) to "LIGHT", + stringResource(R.string.theme_mode_dark) to "DARK" + ).forEach { (label, mode) -> FilterChip( selected = defaultThemeMode == mode, onClick = { viewModel.setDefaultThemeMode(mode) }, @@ -313,20 +317,20 @@ fun SettingsScreen( HorizontalDivider() // D3. Características avanzadas - SettingsSectionTitle("Características Avanzadas") + SettingsSectionTitle(stringResource(R.string.settings_section_advanced)) SettingsItemClickable( - title = "Árbol de Habilidades", - subtitle = "Desbloquea habilidades pasivas con XP", + title = stringResource(R.string.settings_advanced_skilltree), + subtitle = stringResource(R.string.settings_advanced_skilltree_sub), onClick = onNavigateToSkillTree ) SettingsItemClickable( - title = "Pase de Temporada", - subtitle = "20 niveles de recompensas", + title = stringResource(R.string.settings_advanced_seasonpass), + subtitle = stringResource(R.string.settings_advanced_seasonpass_sub), onClick = onNavigateToSeasonPass ) SettingsItemClickable( - title = "Misiones Semanales", - subtitle = "3 misiones rotativas cada lunes", + title = stringResource(R.string.settings_advanced_weekly), + subtitle = stringResource(R.string.settings_advanced_weekly_sub), onClick = onNavigateToWeeklyMissions ) @@ -338,7 +342,7 @@ fun SettingsScreen( modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp) ) { Text( - text = "Moodlet: ${moodlet!!.moodletType}", + text = stringResource(R.string.settings_moodlet, moodlet!!.moodletType), fontSize = 11.sp, color = MaterialTheme.colorScheme.tertiary, modifier = Modifier.padding(8.dp) @@ -354,7 +358,7 @@ fun SettingsScreen( modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp) ) { Text( - text = "Modo No Molestar activo: XP x1.5 en modo foco", + text = stringResource(R.string.settings_dnd_active), fontSize = 11.sp, color = Color(0xFFFFB74D), modifier = Modifier.padding(8.dp) @@ -473,7 +477,7 @@ fun ThemePreviewCard( if (!isUnlocked) { Icon( imageVector = Icons.Default.Lock, - contentDescription = "Bloqueado", + contentDescription = stringResource(R.string.theme_locked), tint = Color.White.copy(alpha = 0.6f), modifier = Modifier.size(18.dp) ) @@ -486,7 +490,7 @@ fun ThemePreviewCard( Box(contentAlignment = Alignment.Center) { Icon( imageVector = Icons.Default.Check, - contentDescription = "Seleccionado", + contentDescription = stringResource(R.string.theme_selected), tint = theme.primary, modifier = Modifier.size(14.dp) ) @@ -525,7 +529,7 @@ fun ThemePreviewCard( // Description Text( - text = if (isUnlocked) theme.description else "Bloqueado", + text = if (isUnlocked) theme.description else stringResource(R.string.theme_locked), fontSize = 9.sp, color = if (isUnlocked) { if (theme.isDark) Color.LightGray else theme.textSecondary diff --git a/app/src/main/java/com/tamagotchi/code/feature/shop/ShopScreen.kt b/app/src/main/java/com/tamagotchi/code/feature/shop/ShopScreen.kt index 6928f64..ad726f4 100644 --- a/app/src/main/java/com/tamagotchi/code/feature/shop/ShopScreen.kt +++ b/app/src/main/java/com/tamagotchi/code/feature/shop/ShopScreen.kt @@ -14,10 +14,12 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.tamagotchi.code.R import com.tamagotchi.code.data.database.PetStateEntity import com.tamagotchi.code.ui.theme.LocalAppTheme import com.tamagotchi.code.ui.viewmodel.PetViewModel @@ -35,7 +37,7 @@ fun ShopScreen( .verticalScroll(rememberScrollState()) ) { Text( - text = ">>> TIENDA", + text = stringResource(R.string.shop_header), fontSize = 14.sp, style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.primary, @@ -56,15 +58,15 @@ fun ShopPanel( val appTheme = LocalAppTheme.current val shopItems = listOf( - ShopItemData("Café Negro (CPU Booster)", 10, "Restaura +20 Energía mental", 0f, 0f, 20f, Icons.Default.Coffee), - ShopItemData("Pizza de Código (Bytes Snack)", 15, "Restaura +35 Alimento", 35f, 0f, 0f, Icons.Default.LocalPizza), - ShopItemData("Píldora Desbugueadora", 25, "Cura de infecciones y sana +30 Salud", 0f, 30f, 0f, Icons.Default.Medication), - ShopItemData("Vacuna Super Compiler", 55, "Restaura +75 Salud, +40 Alimento, +40 Energía", 40f, 75f, 40f, Icons.Default.Shield) + ShopItemData(R.string.shop_item_cafe, R.string.shop_item_cafe_effect, 10, 0f, 0f, 20f, Icons.Default.Coffee, "cafe"), + ShopItemData(R.string.shop_item_pizza, R.string.shop_item_pizza_effect, 15, 35f, 0f, 0f, Icons.Default.LocalPizza, "pizza"), + ShopItemData(R.string.shop_item_pill, R.string.shop_item_pill_effect, 25, 0f, 30f, 0f, Icons.Default.Medication, "pill"), + ShopItemData(R.string.shop_item_vaccine, R.string.shop_item_vaccine_effect, 55, 40f, 75f, 40f, Icons.Default.Shield, "vaccine") ) Column(modifier = Modifier.fillMaxWidth()) { Text( - text = "Compra raciones o medicinas con tus Bytes de estudio acumulados.", + text = stringResource(R.string.shop_desc), fontSize = 11.sp, style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant, @@ -74,6 +76,7 @@ fun ShopPanel( Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { shopItems.forEach { item -> + val itemName = stringResource(item.nameRes) val canAfford = state.bytes >= item.cost Surface( shape = RoundedCornerShape(appTheme.cornerRadius.coerceAtMost(8.dp)), @@ -87,7 +90,7 @@ fun ShopPanel( ) { Icon( imageVector = item.icon, - contentDescription = item.name, + contentDescription = itemName, tint = if (canAfford) appTheme.accent else MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f), modifier = Modifier.size(24.dp) ) @@ -95,14 +98,14 @@ fun ShopPanel( Column(modifier = Modifier.weight(1f)) { Text( - text = item.name, + text = itemName, fontWeight = FontWeight.Bold, style = MaterialTheme.typography.bodyMedium, fontSize = 12.sp, color = MaterialTheme.colorScheme.onSurface ) Text( - text = item.effect, + text = stringResource(item.effectRes), style = MaterialTheme.typography.bodySmall, fontSize = 10.sp, color = MaterialTheme.colorScheme.onSurfaceVariant @@ -112,7 +115,7 @@ fun ShopPanel( Button( onClick = { viewModel.buyShopItem( - itemName = item.name, + itemName = itemName, cost = item.cost, hungerRestore = item.hungerRestore, healthRestore = item.healthRestore, @@ -128,7 +131,7 @@ fun ShopPanel( contentPadding = PaddingValues(horizontal = 12.dp, vertical = 4.dp), modifier = Modifier .height(32.dp) - .testTag("shop_buy_${item.name.lowercase().replace(" ", "_")}") + .testTag("shop_buy_${item.id}") ) { Text( text = "${item.cost} B", @@ -146,11 +149,12 @@ fun ShopPanel( } data class ShopItemData( - val name: String, + val nameRes: Int, + val effectRes: Int, val cost: Int, - val effect: String, val hungerRestore: Float, val healthRestore: Float, val energyRestore: Float, - val icon: ImageVector + val icon: ImageVector, + val id: String ) diff --git a/app/src/main/java/com/tamagotchi/code/feature/skills/SeasonPassScreen.kt b/app/src/main/java/com/tamagotchi/code/feature/skills/SeasonPassScreen.kt index a685413..4cb5b06 100644 --- a/app/src/main/java/com/tamagotchi/code/feature/skills/SeasonPassScreen.kt +++ b/app/src/main/java/com/tamagotchi/code/feature/skills/SeasonPassScreen.kt @@ -12,11 +12,13 @@ import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.tamagotchi.code.R import com.tamagotchi.code.ui.viewmodel.PetViewModel @Composable @@ -35,7 +37,7 @@ fun SeasonPassScreen( Icon(Icons.Default.ConfirmationNumber, contentDescription = null, tint = Color(0xFF81C784)) Spacer(modifier = Modifier.width(8.dp)) Text( - text = ">>> PASE DE TEMPORADA", + text = stringResource(R.string.season_header), fontFamily = FontFamily.Monospace, fontWeight = FontWeight.Bold, fontSize = 18.sp, @@ -44,7 +46,7 @@ fun SeasonPassScreen( } Text( - text = "Gana XP de pase completando actividades. 20 niveles de recompensas!", + text = stringResource(R.string.season_desc), fontFamily = FontFamily.Monospace, fontSize = 11.sp, color = Color.Gray @@ -62,7 +64,7 @@ fun SeasonPassScreen( verticalArrangement = Arrangement.spacedBy(8.dp) ) { Text( - text = "NIVEL ${data.level} / 20", + text = stringResource(R.string.season_level, data.level), fontFamily = FontFamily.Monospace, fontSize = 24.sp, fontWeight = FontWeight.Bold, @@ -75,7 +77,7 @@ fun SeasonPassScreen( modifier = Modifier.fillMaxWidth().height(8.dp) ) Text( - text = "XP del pase: ${data.xp}/100", + text = stringResource(R.string.season_xp, data.xp), fontFamily = FontFamily.Monospace, fontSize = 11.sp, color = Color.Gray @@ -90,7 +92,7 @@ fun SeasonPassScreen( modifier = Modifier.fillMaxWidth() ) { Text( - text = "PASE PREMIUM: Skins exclusivas + sombreros raros + XP boost", + text = stringResource(R.string.season_premium), fontFamily = FontFamily.Monospace, fontSize = 10.sp, color = Color(0xFFFFD700), @@ -102,7 +104,7 @@ fun SeasonPassScreen( } Text( - text = "Recompensas del pase:", + text = stringResource(R.string.season_rewards_title), fontFamily = FontFamily.Monospace, fontSize = 13.sp, fontWeight = FontWeight.Bold, @@ -110,11 +112,11 @@ fun SeasonPassScreen( ) val rewards = listOf( - "Nivel 1: 50 Bytes", - "Nivel 5: Sombrero básico", - "Nivel 10: 200 XP", - "Nivel 15: Carta de código rara", - "Nivel 20: Skin legendaria!" + stringResource(R.string.season_reward_1), + stringResource(R.string.season_reward_5), + stringResource(R.string.season_reward_10), + stringResource(R.string.season_reward_15), + stringResource(R.string.season_reward_20) ) rewards.forEachIndexed { idx, reward -> @@ -157,7 +159,7 @@ fun SeasonPassScreen( shape = RoundedCornerShape(8.dp), modifier = Modifier.fillMaxWidth().height(40.dp) ) { - Text("VOLVER", fontFamily = FontFamily.Monospace, fontWeight = FontWeight.Bold) + Text(stringResource(R.string.game_back), fontFamily = FontFamily.Monospace, fontWeight = FontWeight.Bold) } } } diff --git a/app/src/main/java/com/tamagotchi/code/feature/skills/SkillTreeScreen.kt b/app/src/main/java/com/tamagotchi/code/feature/skills/SkillTreeScreen.kt index cf8c132..8fb0230 100644 --- a/app/src/main/java/com/tamagotchi/code/feature/skills/SkillTreeScreen.kt +++ b/app/src/main/java/com/tamagotchi/code/feature/skills/SkillTreeScreen.kt @@ -18,11 +18,13 @@ import androidx.compose.ui.graphics.Path import androidx.compose.ui.graphics.drawscope.Stroke import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.tamagotchi.code.R import com.tamagotchi.code.ui.viewmodel.PetViewModel import com.tamagotchi.code.ui.viewmodel.SkillNodeData @@ -43,7 +45,7 @@ fun SkillTreeScreen( Icon(Icons.Default.AccountTree, contentDescription = null, tint = Color(0xFF81C784)) Spacer(modifier = Modifier.width(8.dp)) Text( - text = ">>> ÁRBOL DE HABILIDADES", + text = stringResource(R.string.skill_header), fontFamily = FontFamily.Monospace, fontWeight = FontWeight.Bold, fontSize = 18.sp, @@ -52,14 +54,14 @@ fun SkillTreeScreen( } Text( - text = "Gasta XP para desbloquear habilidades pasivas. Cada nivel cuesta más XP.", + text = stringResource(R.string.skill_desc), fontFamily = FontFamily.Monospace, fontSize = 11.sp, color = Color.Gray ) Text( - text = "XP disponible: $xp", + text = stringResource(R.string.skill_xp_available, xp), fontFamily = FontFamily.Monospace, fontSize = 12.sp, fontWeight = FontWeight.Bold, @@ -135,7 +137,7 @@ fun SkillTreeScreen( modifier = Modifier.fillMaxWidth().height(4.dp) ) Text( - text = "Nivel ${skill.currentTier}/${skill.maxTier}", + text = stringResource(R.string.skill_level, skill.currentTier, skill.maxTier), fontFamily = FontFamily.Monospace, fontSize = 9.sp, color = Color.Gray @@ -153,7 +155,7 @@ fun SkillTreeScreen( modifier = Modifier.height(28.dp) ) { Text( - text = "$cost XP", + text = stringResource(R.string.skill_cost, cost), color = Color.Black, fontWeight = FontWeight.Bold, fontSize = 9.sp, @@ -163,7 +165,7 @@ fun SkillTreeScreen( } else { Icon( imageVector = Icons.Default.CheckCircle, - contentDescription = "Maxed", + contentDescription = stringResource(R.string.skill_maxed), tint = Color(0xFFFFD700), modifier = Modifier.size(20.dp) ) @@ -180,7 +182,7 @@ fun SkillTreeScreen( shape = RoundedCornerShape(8.dp), modifier = Modifier.fillMaxWidth().height(40.dp) ) { - Text("VOLVER", fontFamily = FontFamily.Monospace, fontWeight = FontWeight.Bold) + Text(stringResource(R.string.game_back), fontFamily = FontFamily.Monospace, fontWeight = FontWeight.Bold) } } } diff --git a/app/src/main/java/com/tamagotchi/code/feature/skills/WeeklyMissionsScreen.kt b/app/src/main/java/com/tamagotchi/code/feature/skills/WeeklyMissionsScreen.kt index 18c8426..52a8931 100644 --- a/app/src/main/java/com/tamagotchi/code/feature/skills/WeeklyMissionsScreen.kt +++ b/app/src/main/java/com/tamagotchi/code/feature/skills/WeeklyMissionsScreen.kt @@ -12,11 +12,13 @@ import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.tamagotchi.code.R import com.tamagotchi.code.ui.viewmodel.PetViewModel @Composable @@ -35,7 +37,7 @@ fun WeeklyMissionsScreen( Icon(Icons.Default.Assignment, contentDescription = null, tint = Color(0xFF81C784)) Spacer(modifier = Modifier.width(8.dp)) Text( - text = ">>> MISIONES SEMANALES", + text = stringResource(R.string.weekly_header), fontFamily = FontFamily.Monospace, fontWeight = FontWeight.Bold, fontSize = 18.sp, @@ -57,7 +59,7 @@ fun WeeklyMissionsScreen( Icon(Icons.Default.EmojiEvents, contentDescription = null, tint = Color(0xFFFFD700)) Spacer(modifier = Modifier.width(8.dp)) Text( - text = "BONUS COMPLETO: 500 XP + Sombrero exclusivo!", + text = stringResource(R.string.weekly_bonus), fontFamily = FontFamily.Monospace, fontWeight = FontWeight.Bold, fontSize = 12.sp, @@ -68,7 +70,7 @@ fun WeeklyMissionsScreen( } Text( - text = "Completa las 3 misiones semanales para un bonus especial.", + text = stringResource(R.string.weekly_desc), fontFamily = FontFamily.Monospace, fontSize = 11.sp, color = Color.Gray @@ -142,7 +144,7 @@ fun WeeklyMissionsScreen( shape = RoundedCornerShape(8.dp), modifier = Modifier.fillMaxWidth().height(40.dp) ) { - Text("VOLVER", fontFamily = FontFamily.Monospace, fontWeight = FontWeight.Bold) + Text(stringResource(R.string.game_back), fontFamily = FontFamily.Monospace, fontWeight = FontWeight.Bold) } } } diff --git a/app/src/main/res/values-en/strings.xml b/app/src/main/res/values-en/strings.xml new file mode 100644 index 0000000..a87346e --- /dev/null +++ b/app/src/main/res/values-en/strings.xml @@ -0,0 +1,474 @@ + + Code Tamagotchi + + + Skip + Next + + + Your code buddy + The virtual pet that keeps you company while you practice programming. + \> hello_world + Hi! I\'m Codey, your code buddy.\nReady for this adventure? + + + Your practice makes it evolve + Solve challenges + Complete Focus + Take care of your pet + Challenges and Focus grant XP; Bytes are used for care, games and aesthetics. + To grow together, solve challenges,\ncomplete Focus sessions and take care of me.\nEvery action counts! + + + Choose your path + To start, pick up to 3 topics + Initial path + Kotlin + JavaScript + Python + PHP + SQL + Git + Clean Code + Data Structures + Select the topics you\'re most\ninterested in. That way I can help you better. + + + Name your copilot + \> Compiled! I\'m %1$s. + Create %1$s + Use default name: Codey + Codey + What do you want to call me?\nOr you can keep the name I came with. + + + Skip setup? + Your pet will be named Codey and will use the Initial Path. + Yes, skip + No, go back + + + Settings + Profile + Pet name + Current level: %1$d + Learning + Languages and topics + Default Focus duration + Set in the Focus screen + Experience + Visual theme + Sound + Vibration + Reduce animations + Active + Achievements + Reminders + Daily study reminder + Data and Help + Export progress + Reset progress + About + + + Reset all progress? + This action will permanently delete your pet, level, XP, Bytes and settings. This action cannot be undone. + Yes, reset + Cancel + Are you sure? + This is your last chance. All your progress will be lost forever. Confirm? + Yes, I\'m sure + + + Rename pet + New name (max 15) + Save + Already regretting the name? + + + Languages and Topics + Primary Language + Active Topics + Difficulty + Beginner + Elementary + Intermediate + Mixed + Current rotation: %1$d topics at %2$s difficulty. + + + Debugging Arcade + Bug Hunt + Find the runaway semicolon and other syntactic horrors. + Git Rescue + A branch is about to collapse. Make the right calls. + Refactor Rush + Reorder the code so it makes sense before the build fails. + Classic Arcade + Old-school minigames to kill time. + On cooldown (Come back later for a bigger reward) + Play + + + Terminal Panic + Find the line with the bug. + Round %1$d / %2$d + + + Git Rescue + Pick the right command to save the repo. + + + Refactor Rush + Sort the logical blocks of the function. + + + Build complete! + You earned %1$d Bytes. + Finish + + + Accept + Codey: %1$s + BACK + Start! + Claim Rewards + Next round + Next decision + See results + Up + Down + + + DAILY REWARDS (CLAIMED) + DAILY REWARDS (AVAILABLE!) + Session completed while you were away + Congrats! You earned %1$d XP and %2$d Bytes for your Focus session. + %1$s has died + Your pet passed away from lack of care.\n\nBut all is not lost... you can revive it: + Revive for %1$d Bytes + Free revive (low stats) + Revive (%1$d Bytes) + Free revive + Codey made a commit + Yesterday\'s summary: + Merge (Accept) + + + Timer + Logbook + >>> FOCUS MODE + DO NOT DISTURB ACTIVE: 1.5x XP in focus sessions + >>> START STUDY LOGBOOK + Subject: + Time: + Change + %1$d min + While studying: The Tamagotchi enters focused mode. Completing the session rewards you with Bytes and study XP, but drains mental energy. + START COMPILATION + >>> MODE: COMPILING STUDY HOURS + Focus: %1$s + Pet: %1$s is studying with you... + CANCEL COMPILATION + >>> COMPILATION LOGBOOK (STUDIES) + [No study records yet]\nStart a timer to compile your first hours! + Study: %1$s + +%1$d Min + [!] NOTE FROM THE LIFE COMPILER: + If you stop logging sessions for more than 36 hours, your study streak resets to 0, and your pet loses health from lack of maintenance. + + + Regular Challenges + Special Challenges + Badges + >>> LEARN + Studying with %1$s + Elite challenges + >>> PROGRAMMING CHALLENGE: %1$s Arena + Loading riddles... + Careful! That bug bites. Double-check the dots and semicolons. + This one is easy... if you\'ve read the documentation. + Focus, my CPU depends on your answer. + Challenge %1$d of %2$d (%3$s): + Debug + Trivia + CORRECT! +Food +Bytes +XP + WRONG ANSWER + NEXT RIDDLE + LOAD MORE CHALLENGES + + + >>> BADGES BY LANGUAGE + Complete challenges to unlock badges: Bronze (50%), Silver (75%), Gold (100%) + %1$d/%2$d challenges (%3$d%%) + BRONZE + SILVER + GOLD + + + SPECIAL CHALLENGES + Special + Solve advanced logic and algorithm exercises to unlock exclusive new visual themes. + You already have them all! + Correct Answer! + Incorrect. + New theme unlocked: %1$s! + Next Challenge + + + Name (max 15) + + + Code Review + Review code snippets and spot bugs. + Weekly Hackathon + Weekend algorithm challenge. + + + Codey: %1$s + Find the bug before the compiler blows up! + That line is fine! Keep looking... + Next round! Where is the bug hiding? + You got %1$d of %2$d + Explanation: %1$s + The closing parenthesis is missing in println. + It was a fugitive parenthesis! It escaped without paying rent. + In Kotlin comparison is ==, not =. = is assignment. + Codey: One = is assignment, double == is comparison! Don\'t confuse the compiler. + The explicit return type is missing, it should be \': Int\'. + Codey: Even a pocket calculator knows what it\'s going to return. + In Kotlin, for uses parentheses: for (i in nums). + Codey: Parentheses aren\'t decoration! They\'re part of the syntax. + main doesn\'t need empty parentheses if it\'s the entry point. + Codey: Actually main() is fine, but this bug is a cheater! + It\'s decrementing count instead of incrementing it. Infinite loop. + Codey: You\'ll never reach 5 that way! It\'s like running backwards. + You can\'t assign a String directly to an Int. Use toInt(). + Codey: You can\'t convert strings to Int by osmosis! Use .toInt(). + The function must declare a return type: fun isEven(n: Int): Boolean. + Codey: The compiler isn\'t a fortune teller. Tell it what you\'re returning. + listOf creates an immutable list. Use mutableListOf. + Codey: You can\'t modify an immutable list! That\'s like trying to change the past. + Unnecessary blank lines. Code should be clean. + Codey: Are you writing code or a novel? Very poetic, not very practical. + + + Quick, the repo is on fire! + That wasn\'t the best option! + Next decision! The repo needs you. + Correct decisions: %1$d of %2$d + Branch progress: + Step %1$d of %2$d + You just cloned the repo and are about to work on a new feature. What do you do first? + git checkout -b feature/new + git commit -m \"start\" + git push --force + Creating a new branch from main is the right way to start a feature. + Codey: Never push to main without creating a branch first! You cowboy. + You made local changes and realize you broke everything. You want to go back to the last clean commit. + git reset --hard HEAD + git revert HEAD + git rm -rf . + revert creates a new commit that undoes changes, preserving history. + Codey: reset --hard is a time machine without brakes. revert is safer. + You have a merge conflict. What\'s the next step? + Resolve the conflict in the editor, then git add + git commit --amend + git push --force origin main + Conflicts are resolved by editing the files, then git add and git commit. + Codey: push --force doesn\'t resolve conflicts, it makes them worse. Like throwing gasoline on fire. + You\'re working in a team and need to bring the latest changes from main into your feature branch. + git pull origin main + git merge feature main + git branch -d main + git pull brings main\'s changes into your current branch. merge would do it the other way around. + Codey: Don\'t delete main. Never delete main. It\'s like deleting the dictionary. + You committed but forgot to include a file. How do you fix it? + git add file && git commit --amend + git reset --hard HEAD~1 + git commit --allow-empty + With amend you can add files to the previous commit without creating a new one. + Codey: --allow-empty fixes nothing. It\'s like putting a post-it on a closed door. + You want to see which files you modified before committing. Which command do you use? + git diff + git status + git show + git status shows the current state: modified, new and deleted files. + Codey: diff shows the exact content, status gives you the summary. Two different tools. + The commit history is full of messages like \'fix\' and \'update\'. You want to clean it up before merging. + git rebase -i HEAD~5 + git reset --hard origin/main + git commit --fixup + Interactive rebase lets you squash, reorder and rename commits. + Codey: \'fix\' and \'update\' messages are like writing \'thing\' on an exam. Be descriptive. + Your feature branch fell behind main and you need to update it without creating merge commits. + git rebase main + git merge main + git pull --rebase + pull --rebase fetches changes and applies them on top of your local commits. + Codey: Linear history is like a straight highway. Merges are roundabouts. + + + Sort this mess out! + Excellent! Clean, ordered code in %1$d attempt(s). + Keep trying, it still doesn\'t compile (attempt %1$d). + Verify + Compute total + Filter evens + Personalized greeting + Find maximum + Count vowels + Reverse list + Is palindrome + Grade average + Generate range + Validate email + Fibonacci + Capitalize words + + + What do you think the secret bit is? + GUESS THE BIT (Round %1$d of 5) + END + Excellent! The secret bit was %1$d. + Wrong. The secret bit was %1$d. + Game Over!\nYou got: %1$d of 5\nReward: +%2$d Bytes, +%3$d%% Happiness + Next Round + + + BUG SMASHER + Tap the bugs that appear in the 3x3 grid as fast as you can. You have 10 seconds! + Time\'s Up!\nBugs caught: %1$d\nReward: +%2$d Bytes, +%3$d%% Happiness + Bugs: %1$d + Time: %1$ds + BUG + + + Pick your move to start the round. + Server + Script + Hacker + SERVER, SCRIPT, HACKER (RPS) + YOU: %1$d | CPU: %2$d (Best of 3) + YOU + COMPILER + Question + Select... + Waiting... + Draw in this round with %1$s. + You won the round! %1$s beats %2$s. + You lost the round. %1$s beats %2$s. + You won the match! + The compiler won the match. + Congrats! You defeated the compiler.\nReward: +%1$d Bytes, +%2$d%% Happiness + You lost against the compiler.\nReward: +%1$d Bytes, +%2$d%% Happiness + + + >>> MINI-GAMES + >>> GAME DETAIL + Back + Play with %1$s to boost its happiness and earn extra Bytes. + Guess the Bit + Try to predict the next binary bit (0 or 1). Quick 5-round game. + Bug Smasher + Quick! The bugs are escaping. Catch as many as you can in 10 seconds. + Server, Script, Hacker + Rock, Paper or Scissors, tech edition. Defeat the compiler! + + + >>> CODE REVIEW: Round %1$d/5 + APPROVE + REQUEST CHANGES + CORRECT! + WRONG + CONTINUE + REVIEW COMPLETE + Score: %1$d / %2$d + Good job, Sr. Developer! + +%1$d Bytes\n+%2$d XP + BACK TO THE TERMINAL + + + >>> WEEKLY HACKATHON + Every weekend, a special algorithm challenge. + This week\'s challenge: + Sort this array without using sort()\n\nInput: [3, 7, 1, 9, 4, 2, 8, 5, 6]\nExpected output: [1, 2, 3, 4, 5, 6, 7, 8, 9] + Attempts left: %1$d / 3 + Best time: %1$ds + SOLVE CHALLENGE + Reward: 200 XP + 500 Bytes + exclusive temporary skin + No attempts left. Wait for the next hackathon! + + + >>> SHOP + Buy rations or medicine with your accumulated study Bytes. + Black Coffee (CPU Booster) + Restores +20 Mental Energy + Code Pizza (Bytes Snack) + Restores +35 Food + Debugging Pill + Cures infections and heals +30 Health + Super Compiler Vaccine + Restores +75 Health, +40 Food, +40 Energy + + + Base theme mode + System + Light + Dark + Locked + Selected + Advanced Features + Skill Tree + Unlock passive skills with XP + Season Pass + 20 levels of rewards + Weekly Missions + 3 rotating missions every Monday + Moodlet: %1$s + Do Not Disturb active: 1.5x XP in focus mode + + + About + Version 1.0.0 + What is Code Tamagotchi? + Code Tamagotchi is an app that combines learning to program with taking care of a virtual pet. Solve coding challenges, complete study sessions and play minigames to keep your pet happy and healthy while you level up your programming skills. + Main features + • Coding challenges in Kotlin, JavaScript, PHP, Python and more + • Built-in Pomodoro study timer + • Minigames: Bug Hunt, Git Rescue, Refactor Rush + • Virtual pet with moods and needs + • Multiple visual themes to customize the experience + • Achievements and rewards system + • Home screen widget + Technologies used + Built with Jetpack Compose, Kotlin, Room, Koin and WorkManager. + © 2026 Code Tamagotchi. All rights reserved. + + + >>> SKILL TREE + Spend XP to unlock passive skills. Each level costs more XP. + Available XP: %1$d + Level %1$d/%2$d + %1$d XP + Maxed + + + >>> SEASON PASS + Earn pass XP by completing activities. 20 levels of rewards! + LEVEL %1$d / 20 + Pass XP: %1$d/100 + PREMIUM PASS: Exclusive skins + rare hats + XP boost + Pass rewards: + Level 1: 50 Bytes + Level 5: Basic hat + Level 10: 200 XP + Level 15: Rare code card + Level 20: Legendary skin! + + + >>> WEEKLY MISSIONS + FULL BONUS: 500 XP + Exclusive hat! + Complete the 3 weekly missions for a special bonus. + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 4d72a3a..5fbace7 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -127,4 +127,348 @@ Ganaste %1$d Bytes. Finalizar + + Aceptar + Codey: %1$s + VOLVER + ¡Comenzar! + Cobrar Recompensas + Siguiente ronda + Siguiente decisión + Ver resultados + Up + Down + + + RECOMPENSAS DIARIAS (RECLAMADO) + RECOMPENSAS DIARIAS (¡DISPONIBLE!) + Sesión completada mientras estabas fuera + ¡Felicidades! Has ganado %1$d XP y %2$d Bytes por tu sesión de Focus. + %1$s ha muerto + Tu mascota ha fallecido por falta de cuidados.\n\nPero no todo está perdido... puedes revivirla: + Revivir por %1$d Bytes + Revivir gratis (stats bajos) + Revivir (%1$d Bytes) + Revivir gratis + Codey hizo commit + Resumen de ayer: + Fusionar (Aceptar) + + + Temporizador + Bitácora + >>> MODO FOCO + MODO NO MOLESTAR ACTIVO: XP x1.5 en sesiones de foco + >>> INICIAR BITÁCORA DE ESTUDIO + Materia: + Tiempo: + Cambiar + %1$d min + Al estudiar: El Tamagotchi entrará en modo concentrado. Completar la sesión te recompensa con Bytes y XP de estudio, pero drenará energía mental. + EMPEZAR COMPILACIÓN + >>> MODO: COMPILANDO HORAS DE ESTUDIO + Enfoque: %1$s + Mascota: %1$s está estudiando contigo... + CANCELAR COMPILACIÓN + >>> BITÁCORA DE COMPILACIÓN (ESTUDIOS) + [Sin registros de estudio aún]\n¡Empieza un temporizador para compilar tus primeras horas! + Estudio: %1$s + +%1$d Min + [!] NOTA DEL COMPILADOR DE VIDA: + Si dejas de registrar sesiones por más de 36 horas, tu racha de estudio se reiniciará a 0, y tu mascota perderá salud por falta de mantenimiento. + + + Retos Normales + Retos Especiales + Insignias + >>> APRENDER + Estudiando con %1$s + Desafíos de élite + >>> DESAFÍO DE PROGRAMACIÓN: Arena %1$s + Cargando acertijos... + ¡Cuidado! Ese bug muerde. Revisa bien los puntos y coma. + Esta es fácil... si has leído la documentación. + Concéntrate, mi CPU depende de tu respuesta. + Desafío %1$d de %2$d (%3$s): + Desbuguear + Trivia + ¡ACERTADO! +Alimento +Bytes +XP + RESPUESTA INCORRECTA + SIGUIENTE ACERTIJO + CARGAR MÁS RETOS + + + >>> INSIGNIAS POR LENGUAJE + Completa retos para desbloquear insignias: Bronce (50%), Plata (75%), Oro (100%) + %1$d/%2$d retos (%3$d%%) + BRONCE + PLATA + ORO + + + RETOS ESPECIALES + Especial + Resuelve ejercicios avanzados de lógica y algoritmos para desbloquear nuevos temas visuales exclusivos. + ¡Ya tienes todos! + ¡Respuesta Correcta! + Incorrecto. + ¡Nuevo tema desbloqueado: %1$s! + Siguiente Reto + + + Nombre (máx 15) + + + Code Review + Revisa snippets de código y detecta bugs. + Hackatón Semanal + Desafío de algoritmo de fin de semana. + + + Codey: %1$s + ¡Encuentra el bug antes de que el compilador explote! + ¡Esa línea está bien! Sigue buscando... + ¡Siguiente ronda! ¿Dónde se esconde el bug? + Acertaste %1$d de %2$d + Explicación: %1$s + Falta el paréntesis de cierre en println. + ¡Era un paréntesis fugitivo! Se escapó sin pagar el alquiler. + En Kotlin la comparación es == no =. = es asignación. + Codey: ¡Un solo = es asignación, doble == es comparación! No confundas al compilador. + Falta el tipo de retorno explícito, debería ser \': Int\'. + Codey: Hasta una calculadora de bolsillo sabe qué va a devolver. + En Kotlin el for usa paréntesis: for (i in nums). + Codey: ¡Los paréntesis no son decoración! Son parte de la sintaxis. + main no necesita paréntesis vacíos si es la entrada. + Codey: En realidad main() está bien, ¡pero este bug es tramposo! + Está decrementando count en vez de incrementarlo. Bucle infinito. + Codey: ¡Así nunca llegarás a 5! Es como correr hacia atrás. + No se puede asignar un String directamente a Int. Usa toInt(). + Codey: ¡No puedes convertir strings a Int por ósmosis! Usa .toInt(). + La función debe declarar tipo de retorno: fun isEven(n: Int): Boolean. + Codey: El compilador no es adivino. Dile qué vas a devolver. + listOf crea una lista inmutable. Usa mutableListOf. + Codey: ¡No puedes modificar una lista inmutable! Es como intentar cambiar el pasado. + Líneas vacías innecesarias. El código debe ser limpio. + Codey: ¿Estás escribiendo código o una novela? Muy poético pero poco práctico. + + + ¡Rápido, el repo está en llamas! + ¡Esa no era la mejor opción! + ¡Siguiente decisión! El repo te necesita. + Decisiones correctas: %1$d de %2$d + Progreso de la rama: + Paso %1$d de %2$d + Acabas de clonar el repo y vas a trabajar en un nuevo feature. ¿Qué haces primero? + git checkout -b feature/nuevo + git commit -m \"inicio\" + git push --force + Crear una rama nueva desde main es la forma correcta de empezar un feature. + Codey: ¡Nunca pushes a main sin antes crear una rama! Eres un cowboy. + Has hecho cambios locales y te das cuenta de que rompiste todo. Quieres volver al último commit limpio. + git reset --hard HEAD + git revert HEAD + git rm -rf . + revert crea un nuevo commit que deshace los cambios, preservando la historia. + Codey: reset --hard es como una máquina del tiempo sin frenos. revert es más seguro. + Tienes un conflicto en un merge. ¿Cuál es el siguiente paso? + Resolver el conflicto en el editor y luego git add + git commit --amend + git push --force origin main + Los conflictos se resuelven editando los archivos, luego git add y git commit. + Codey: push --force no resuelve conflictos, los empeora. Es como echar gasolina al fuego. + Trabajas en equipo y necesitas traerte los cambios más recientes de la rama main a tu rama feature. + git pull origin main + git merge feature main + git branch -d main + git pull trae los cambios de main a tu rama actual. merge lo haría al revés. + Codey: No borres main. Nunca borres main. Es como borrar el diccionario. + Hiciste un commit pero olvidaste incluir un archivo. ¿Cómo lo arreglas? + git add archivo && git commit --amend + git reset --hard HEAD~1 + git commit --allow-empty + Con amend puedes agregar archivos al commit anterior sin crear uno nuevo. + Codey: --allow-empty no arregla nada. Es como poner un post-it en una puerta cerrada. + Quieres ver qué archivos modificaste antes de hacer commit. ¿Qué comando usas? + git diff + git status + git show + git status muestra el estado actual: archivos modificados, nuevos y eliminados. + Codey: diff es para ver el contenido exacto, status te da el resumen. Dos herramientas diferentes. + El historial de commits está lleno de mensajes como \'fix\' y \'update\'. Quieres limpiarlo antes de hacer merge. + git rebase -i HEAD~5 + git reset --hard origin/main + git commit --fixup + rebase interactivo te permite squash, reordenar y renombrar commits. + Codey: Los mensajes \'fix\' y \'update\' son como decir \'cosa\' en un examen. Sé descriptivo. + Tu rama feature quedó atrás de main y necesitas actualizarla sin crear commits de merge. + git rebase main + git merge main + git pull --rebase + pull --rebase trae cambios y los aplica encima de tus commits locales. + Codey: El histórico lineal es como una carretera recta. Los merges son rotondas. + + + ¡Ordena este desastre! + ¡Excelente! Código limpio y ordenado en %1$d intento(s). + Sigue intentando, todavía no compila (intento %1$d). + Verificar + Calcular total + Filtrar pares + Saludo personalizado + Buscar máximo + Contar vocales + Invertir lista + Es palíndromo + Promedio de notas + Generar rango + Validar email + Fibonacci + Capitalizar palabras + + + ¿Cuál crees que es el bit secreto? + ADIVINA EL BIT (Ronda %1$d de 5) + FIN + ¡Excelente! El bit secreto era %1$d. + Incorrecto. El bit secreto era %1$d. + ¡Juego Terminado!\nAcertaste: %1$d de 5\nRecompensa: +%2$d Bytes, +%3$d%% Felicidad + Siguiente Ronda + + + CAZA DE BUGS + Toca los bugs que aparecen en la cuadrícula de 3x3 tan rápido como puedas. ¡Tienes 10 segundos! + ¡Tiempo Agotado!\nBugs atrapados: %1$d\nRecompensa: +%2$d Bytes, +%3$d%% Felicidad + Bugs: %1$d + Tiempo: %1$ds + BUG + + + Elige tu jugada para iniciar la ronda. + Servidor + Script + Hacker + SERVIDOR, SCRIPT, HACKER (RPS) + TÚ: %1$d | CPU: %2$d (Mejor de 3) + TÚ + COMPILADOR + Pregunta + Selecciona... + Esperando... + Empate en esta ronda con %1$s. + ¡Ganaste la ronda! %1$s vence a %2$s. + Perdiste la ronda. %1$s vence a %2$s. + ¡Has ganado la partida! + El compilador ha ganado la partida. + ¡Felicidades! Derrotaste al compilador.\nRecompensa: +%1$d Bytes, +%2$d%% Felicidad + Has perdido contra el compilador.\nRecompensa: +%1$d Bytes, +%2$d%% Felicidad + + + >>> MINI-JUEGOS + >>> DETALLE DE JUEGO + Volver + Juega con %1$s para aumentar su felicidad y conseguir Bytes extra. + Adivina el Bit + Intenta predecir el siguiente bit binario (0 o 1). Juego rápido de 5 rondas. + Caza de Bugs + ¡Rápido! Los bugs se están escapando. Atrapa todos los que puedas en 10 segundos. + Servidor, Script, Hacker + Piedra, Papel o Tijera versión informática. ¡Derrota al compilador! + + + >>> CODE REVIEW: Ronda %1$d/5 + APROBAR + SOLICITAR CAMBIOS + ¡ACERTADO! + INCORRECTO + CONTINUAR + REVISIÓN COMPLETADA + Puntuación: %1$d / %2$d + ¡Buen trabajo SR. Developer! + +%1$d Bytes\n+%2$d XP + VOLVER AL TERMINAL + + + >>> HACKATHON SEMANAL + Cada fin de semana un desafío especial de algoritmo. + Desafío de esta semana: + Ordena este array sin usar sort()\n\nEntrada: [3, 7, 1, 9, 4, 2, 8, 5, 6]\nSalida esperada: [1, 2, 3, 4, 5, 6, 7, 8, 9] + Intentos restantes: %1$d / 3 + Mejor tiempo: %1$ds + SOLUCIONAR DESAFÍO + Recompensa: 200 XP + 500 Bytes + Skin exclusiva temporal + Sin intentos restantes. ¡Espera al próximo hackathon! + + + >>> TIENDA + Compra raciones o medicinas con tus Bytes de estudio acumulados. + Café Negro (CPU Booster) + Restaura +20 Energía mental + Pizza de Código (Bytes Snack) + Restaura +35 Alimento + Píldora Desbugueadora + Cura de infecciones y sana +30 Salud + Vacuna Super Compiler + Restaura +75 Salud, +40 Alimento, +40 Energía + + + Modo de tema base + Sistema + Claro + Oscuro + Bloqueado + Seleccionado + Características Avanzadas + Árbol de Habilidades + Desbloquea habilidades pasivas con XP + Pase de Temporada + 20 niveles de recompensas + Misiones Semanales + 3 misiones rotativas cada lunes + Moodlet: %1$s + Modo No Molestar activo: XP x1.5 en modo foco + + + Acerca de + Versión 1.0.0 + ¿Qué es Code Tamagotchi? + Code Tamagotchi es una aplicación que combina el aprendizaje de programación con el cuidado de una mascota virtual. Resuelve desafíos de código, completa sesiones de estudio y juega minijuegos para mantener a tu mascota feliz y saludable mientras mejoras tus habilidades de programación. + Características principales + • Desafíos de código en Kotlin, JavaScript, PHP, Python y más + • Temporizador de estudio Pomodoro integrado + • Minijuegos: Bug Hunt, Git Rescue, Refactor Rush + • Mascota virtual con estados de ánimo y necesidades + • Múltiples temas visuales para personalizar la experiencia + • Sistema de logros y recompensas + • Widget para la pantalla de inicio + Tecnologías usadas + Desarrollada con Jetpack Compose, Kotlin, Room, Koin y WorkManager. + © 2026 Code Tamagotchi. Todos los derechos reservados. + + + >>> ÁRBOL DE HABILIDADES + Gasta XP para desbloquear habilidades pasivas. Cada nivel cuesta más XP. + XP disponible: %1$d + Nivel %1$d/%2$d + %1$d XP + Maxed + + + >>> PASE DE TEMPORADA + Gana XP de pase completando actividades. 20 niveles de recompensas! + NIVEL %1$d / 20 + XP del pase: %1$d/100 + PASE PREMIUM: Skins exclusivas + sombreros raros + XP boost + Recompensas del pase: + Nivel 1: 50 Bytes + Nivel 5: Sombrero básico + Nivel 10: 200 XP + Nivel 15: Carta de código rara + Nivel 20: Skin legendaria! + + + >>> MISIONES SEMANALES + BONUS COMPLETO: 500 XP + Sombrero exclusivo! + Completa las 3 misiones semanales para un bonus especial. + From be228a789fd2b95e9d767824e50adb581c269117 Mon Sep 17 00:00:00 2001 From: openhands Date: Thu, 17 Sep 2026 18:52:23 -0400 Subject: [PATCH 2/3] feat(i18n): UI y contenido totalmente bilingues ES/EN - Externaliza a recursos (values + values-en) todos los strings hardcodeados restantes: viewport (frases por estado como string-arrays), pair buddy, code cards, misiones/seeds, logros, recompensas diarias, moodlets, navegacion, splash, widget y notificaciones (canales + workers). - Logros/habilidades/misiones/moodlets/quests se resuelven por id estable (LocalizedGameText) sin cambiar el formato persistido; QuestType y Achievement quedan sin texto embebido. - Contenido educativo bilingue: ChallengesDataEn/SpecialChallengesDataEn/CodeReviewDataEn/CodeCardsEn (traduccion EN por id) + LocalizedContent.localized() segun locale del sistema; traduce literales dentro de snippets de codigo. - Topics y dificultad se persisten como claves canonicas (TopicKey/DifficultyKey) normalizando valores legacy localizados (bug: en EN la dificultad no activaba logica y comparaciones de topics fallaban). - Correcciones de exactitud git game (escenarios 2/4/8), capitalizacion y exclamaciones ES. - Test LocalizedContentTest: paridad ES<->EN (cobertura de ids, orden y cantidad de opciones). --- .../com/tamagotchi/code/CodeTamagotchiApp.kt | 22 +- .../com/tamagotchi/code/SplashActivity.kt | 2 +- .../tamagotchi/code/data/ChallengesDataEn.kt | 1185 +++++++++++++++++ .../com/tamagotchi/code/data/CodeCardsEn.kt | 47 + .../tamagotchi/code/data/CodeReviewDataEn.kt | 47 + .../tamagotchi/code/data/LocalizedContent.kt | 43 + .../code/data/PersonalityMissions.kt | 19 +- .../code/data/SpecialChallengesDataEn.kt | 74 + .../data/repository/AchievementsRepository.kt | 29 +- .../repository/UserPreferencesRepository.kt | 7 +- .../code/feature/games/BugHuntScreen.kt | 6 +- .../code/feature/games/CodeReviewScreen.kt | 3 +- .../code/feature/games/RefactorRushScreen.kt | 4 +- .../code/feature/learn/LearnScreen.kt | 5 +- .../feature/onboarding/OnboardingScreen.kt | 34 +- .../settings/SettingsLanguageScreen.kt | 46 +- .../code/feature/settings/SettingsScreen.kt | 10 +- .../code/feature/skills/SkillTreeScreen.kt | 10 +- .../feature/skills/WeeklyMissionsScreen.kt | 6 +- .../code/navigation/AppNavigation.kt | 23 +- .../code/ui/components/CodeCardDialog.kt | 16 +- .../code/ui/components/CodePetWidget.kt | 11 +- .../code/ui/components/DailyRewardDialog.kt | 20 +- .../code/ui/components/LocalizedGameText.kt | 131 ++ .../code/ui/components/PairBuddy.kt | 14 +- .../code/ui/components/ViewportCard.kt | 114 +- .../code/ui/viewmodel/PetViewModel.kt | 70 +- .../com/tamagotchi/code/util/CommitWorker.kt | 4 +- .../com/tamagotchi/code/util/ContentKeys.kt | 68 + .../code/util/DailyCommitGenerator.kt | 99 +- .../tamagotchi/code/util/PetCheckWorker.kt | 26 +- .../code/widget/CodePetWidgetProvider.kt | 44 +- app/src/main/res/layout/code_pet_widget.xml | 15 +- app/src/main/res/values-en/strings.xml | 291 +++- app/src/main/res/values/strings.xml | 297 ++++- .../tamagotchi/code/LocalizedContentTest.kt | 58 + 36 files changed, 2530 insertions(+), 370 deletions(-) create mode 100644 app/src/main/java/com/tamagotchi/code/data/ChallengesDataEn.kt create mode 100644 app/src/main/java/com/tamagotchi/code/data/CodeCardsEn.kt create mode 100644 app/src/main/java/com/tamagotchi/code/data/CodeReviewDataEn.kt create mode 100644 app/src/main/java/com/tamagotchi/code/data/LocalizedContent.kt create mode 100644 app/src/main/java/com/tamagotchi/code/data/SpecialChallengesDataEn.kt create mode 100644 app/src/main/java/com/tamagotchi/code/ui/components/LocalizedGameText.kt create mode 100644 app/src/main/java/com/tamagotchi/code/util/ContentKeys.kt create mode 100644 app/src/test/java/com/tamagotchi/code/LocalizedContentTest.kt diff --git a/app/src/main/java/com/tamagotchi/code/CodeTamagotchiApp.kt b/app/src/main/java/com/tamagotchi/code/CodeTamagotchiApp.kt index 1d55437..072f7ee 100644 --- a/app/src/main/java/com/tamagotchi/code/CodeTamagotchiApp.kt +++ b/app/src/main/java/com/tamagotchi/code/CodeTamagotchiApp.kt @@ -36,27 +36,27 @@ class CodeTamagotchiApp : Application() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { val manager = getSystemService(NotificationManager::class.java) val groupId = "pet_care" - NotificationChannelGroup(groupId, "Cuidado de Codey").let { manager.createNotificationChannelGroup(it) } + NotificationChannelGroup(groupId, getString(R.string.channel_group_care)).let { manager.createNotificationChannelGroup(it) } val channels = listOf( - NotificationChannel("pet_hunger", "Hambre de Codey", NotificationManager.IMPORTANCE_DEFAULT).apply { - description = "Codey tiene hambre" + NotificationChannel("pet_hunger", getString(R.string.channel_hunger_name), NotificationManager.IMPORTANCE_DEFAULT).apply { + description = getString(R.string.channel_hunger_desc) this.group = groupId }, - NotificationChannel("pet_health", "Salud de Codey", NotificationManager.IMPORTANCE_HIGH).apply { - description = "Codey está enfermo" + NotificationChannel("pet_health", getString(R.string.channel_health_name), NotificationManager.IMPORTANCE_HIGH).apply { + description = getString(R.string.channel_health_desc) this.group = groupId }, - NotificationChannel("pet_energy", "Energía de Codey", NotificationManager.IMPORTANCE_DEFAULT).apply { - description = "Codey está cansado" + NotificationChannel("pet_energy", getString(R.string.channel_energy_name), NotificationManager.IMPORTANCE_DEFAULT).apply { + description = getString(R.string.channel_energy_desc) this.group = groupId }, - NotificationChannel("pet_critical", "Codey en peligro", NotificationManager.IMPORTANCE_HIGH).apply { - description = "Codey está en estado crítico" + NotificationChannel("pet_critical", getString(R.string.channel_critical_name), NotificationManager.IMPORTANCE_HIGH).apply { + description = getString(R.string.channel_critical_desc) this.group = groupId }, - NotificationChannel(NOTIFICATION_CHANNEL_ID, "Recordatorios de Codey", NotificationManager.IMPORTANCE_DEFAULT).apply { - description = "Recordatorios sobre el estado de tu mascota virtual" + NotificationChannel(NOTIFICATION_CHANNEL_ID, getString(R.string.channel_reminders_name), NotificationManager.IMPORTANCE_DEFAULT).apply { + description = getString(R.string.channel_reminders_desc) this.group = groupId } ) diff --git a/app/src/main/java/com/tamagotchi/code/SplashActivity.kt b/app/src/main/java/com/tamagotchi/code/SplashActivity.kt index f0de20f..4c02cc9 100644 --- a/app/src/main/java/com/tamagotchi/code/SplashActivity.kt +++ b/app/src/main/java/com/tamagotchi/code/SplashActivity.kt @@ -71,7 +71,7 @@ class SplashActivity : Activity() { // Tagline val taglineTextView = android.widget.TextView(this@SplashActivity).apply { - text = "Tu compañero de programación" + text = getString(R.string.splash_tagline) textSize = 14f setTextColor(android.graphics.Color.parseColor("#E6E9FF")) layoutParams = android.view.ViewGroup.LayoutParams( diff --git a/app/src/main/java/com/tamagotchi/code/data/ChallengesDataEn.kt b/app/src/main/java/com/tamagotchi/code/data/ChallengesDataEn.kt new file mode 100644 index 0000000..ab1c32a --- /dev/null +++ b/app/src/main/java/com/tamagotchi/code/data/ChallengesDataEn.kt @@ -0,0 +1,1185 @@ +package com.tamagotchi.code.data + +// English translations keyed by CodingChallenge.id. Keep in sync with ChallengesData.kt: +// same entry count, same option ORDER (correctAnswerIndex depends on it). +object ChallengesDataEn { + val translations: Map = mapOf( + // ========== KOTLIN ========== + 1 to ChallengeTextsEn( + language = "Kotlin", + title = "Immutability in Kotlin", + question = "What's the main difference between 'val' and 'var'?", + codeSnippet = null, + options = listOf( + "val is mutable, var is immutable", + "val defines a read-only (immutable) reference, var is mutable", + "val is a compile-time constant, var is evaluated at runtime", + "No difference, they're aliases inherited from Java" + ), + explanation = "In Kotlin, 'val' defines a read-only variable (its value can't be reassigned), while 'var' defines a standard mutable variable." + ), + 2 to ChallengeTextsEn( + language = "Kotlin", + title = "Safe Calls (Null Safety)", + question = "This code throws a compile error. How do you fix it to allow safe calls to 'length'?", + codeSnippet = null, + options = listOf( + "Change to: val len = name?.length", + "Change to: val len = name!!?.length", + "Change to: val len = name.length()", + "Kotlin doesn't allow nullable variables of any kind" + ), + explanation = "Using the safe call operator '?.' (name?.length) returns the length of the string if it's not null, or 'null' otherwise, avoiding null pointer errors." + ), + 3 to ChallengeTextsEn( + language = "Kotlin", + title = "Scope Functions", + question = "Which scope function returns the result of the lambda block and uses 'this' as the receiver?", + codeSnippet = null, + options = listOf( + "apply", + "also", + "run", + "let" + ), + explanation = "'run' executes the block using 'this' as the call receiver and returns the result of the last expression inside the lambda block." + ), + 12 to ChallengeTextsEn( + language = "Kotlin", + title = "Data Class", + question = "What does the 'data' keyword automatically generate in a class?", + codeSnippet = null, + options = listOf( + "Only getters and setters", + "equals(), hashCode(), toString(), copy() and componentN()", + "A no-argument constructor", + "Serializable implementation" + ), + explanation = "'data class' types automatically generate equals(), hashCode(), toString(), copy(), and componentN() functions for destructuring." + ), + 13 to ChallengeTextsEn( + language = "Kotlin", + title = "Elvis Operator", + question = "What does this code print?", + codeSnippet = "val name: String? = null\nprintln(name ?: \"Guest\")", + options = listOf( + "null", + "Guest", + "name", + "Compile error" + ), + explanation = "The Elvis operator '?:' returns the left side if it's not null, or the right side if the left one is null. Here name is null, so it prints 'Guest'." + ), + 14 to ChallengeTextsEn( + language = "Kotlin", + title = "Extension Functions", + question = "What does an extension function let you do in Kotlin?", + codeSnippet = null, + options = listOf( + "Modify a sealed class from another class", + "Add new functionality to a class without inheriting from it", + "Create a function that only runs in .kt file extensions", + "Extend the runtime of a recursive function" + ), + explanation = "Extension functions let you add new methods to an existing class without modifying its source code or inheriting from it." + ), + 15 to ChallengeTextsEn( + language = "Kotlin", + title = "Null Safety in Lists", + question = "How do you safely get the first element of a nullable list?", + codeSnippet = null, + options = listOf( + "val first = items?.firstOrNull()", + "val first = items!!.first()", + "val first = items.first()", + "val first = items?.get(0)" + ), + explanation = "'items?.firstOrNull()' is the safest form: if items is null, firstOrNull() is never called and the result is null. If it's not null, it returns the first element, or null if the list is empty." + ), + 16 to ChallengeTextsEn( + language = "Kotlin", + title = "Coroutines - Dispatchers", + question = "What is the purpose of Dispatchers.IO in Kotlin coroutines?", + codeSnippet = null, + options = listOf( + "Run UI update tasks", + "Run I/O operations in the background (files, network, DB)", + "Run code sequentially on the main thread", + "It's not a real dispatcher, just a theoretical concept" + ), + explanation = "Dispatchers.IO is optimized for input/output operations such as file reads, network calls, or database queries." + ), + 17 to ChallengeTextsEn( + language = "Kotlin", + title = "When Expression", + question = "How does 'when' differ from Java's 'switch'?", + codeSnippet = null, + options = listOf( + "when only works with integers", + "when can be used as an expression (returns a value) and doesn't need break", + "when requires a mandatory default block", + "when only works with String types" + ), + explanation = "'when' in Kotlin is more powerful than 'switch': it can be used as an expression that returns values, doesn't need break, and supports any kind of condition." + ), + 18 to ChallengeTextsEn( + language = "Kotlin", + title = "Sealed Class", + question = "What is a sealed class used for in Kotlin?", + codeSnippet = null, + options = listOf( + "To hide a class's implementation details", + "To restrict the inheritance hierarchy to a fixed set of known subtypes", + "To prevent any instance of the class from being created", + "To mark a class as deprecated and discouraged" + ), + explanation = "'sealed class' types define a restricted inheritance hierarchy: all direct subtypes must be declared in the same file, which enables exhaustive when." + ), + 19 to ChallengeTextsEn( + language = "Kotlin", + title = "Smart Cast", + question = "Why does this code compile without an explicit cast?", + codeSnippet = null, + options = listOf( + "Because Kotlin infers that obj is Object and Object has length", + "Because Kotlin applies a smart cast: after checking 'is String', it treats obj as a String", + "Because the compiler converts Any to String automatically", + "It doesn't compile, it needs (obj as String).length" + ), + explanation = "Kotlin performs 'smart cast': when you check that a variable is of a certain type with 'is', the compiler automatically treats it as that type within the block." + ), + 20 to ChallengeTextsEn( + language = "Kotlin", + title = "Flow vs LiveData", + question = "What advantage does a Kotlin Flow have over LiveData?", + codeSnippet = null, + options = listOf( + "Flow is tied to the Activity lifecycle", + "Flow is reactive without Android dependency imports and has operators like map, filter", + "Flow only works in Java", + "LiveData can no longer be used in Kotlin" + ), + explanation = "Flow is part of Kotlin (not Android), so it's platform-agnostic and offers functional operators like map, filter, transform, etc." + ), + 21 to ChallengeTextsEn( + language = "Kotlin", + title = "Immutable Collections", + question = "What's the difference between listOf() and mutableListOf()?", + codeSnippet = null, + options = listOf( + "Both create identical lists, only the name changes", + "listOf() creates a read-only list, mutableListOf() allows modifying elements", + "listOf() throws an error if the list has more than 10 elements", + "mutableListOf() doesn't allow null elements" + ), + explanation = "listOf() returns an immutable (read-only) List, while mutableListOf() returns a MutableList that allows adding, removing, and modifying elements." + ), + 22 to ChallengeTextsEn( + language = "Kotlin", + title = "Inline Functions", + question = "What is the main benefit of 'inline' functions in Kotlin?", + codeSnippet = null, + options = listOf( + "They allow multiple inheritance", + "They eliminate the overhead of creating lambda objects by copying the code to the call site", + "They make functions run on a separate thread", + "They make functions globally visible" + ), + explanation = "'inline' copies the function body directly at the call site, eliminating lambda object creation and reducing memory overhead." + ), + 23 to ChallengeTextsEn( + language = "Kotlin", + title = "Try as an Expression", + question = "What value does this code print?", + codeSnippet = null, + options = listOf( + "Compile error", + "10", + "0", + "\"10\"" + ), + explanation = "'try' in Kotlin is an expression and returns the value of the last block executed. '10'.toInt() succeeds, so result = 10." + ), + 24 to ChallengeTextsEn( + language = "Kotlin", + title = "Operator Overloading", + question = "How do you overload the '+' operator for a class in Kotlin?", + codeSnippet = null, + options = listOf( + "By defining a method called add()", + "By defining an 'operator fun plus()'", + "By using the @Overload annotation", + "Operators can't be overloaded in Kotlin" + ), + explanation = "In Kotlin, operators are overloaded by implementing functions marked with 'operator': plus() for '+', minus() for '-', times() for '*', etc." + ), + 25 to ChallengeTextsEn( + language = "Kotlin", + title = "Scope Functions - also", + question = "What makes 'also' different from 'apply'?", + codeSnippet = null, + options = listOf( + "also uses 'this', apply uses 'it'", + "also uses 'it' as the receiver, apply uses 'this'", + "also is synchronous, apply is asynchronous", + "No difference, they're aliases" + ), + explanation = "'apply' uses 'this' (implicit receiver) to configure objects, while 'also' uses 'it' (explicit parameter) and is usually used for side effects." + ), + 86 to ChallengeTextsEn( + language = "Kotlin", + title = "Object Keyword", + question = "What is the 'object' keyword used for in Kotlin?", + codeSnippet = null, + options = listOf( + "Only for creating new objects of an existing class", + "To declare a singleton class (a single global instance)", + "It's the equivalent of 'new' in Java", + "To mark a method as deprecated" + ), + explanation = "'object' declares a singleton class: only one instance of it exists, created lazily and safely on first access." + ), + 87 to ChallengeTextsEn( + language = "Kotlin", + title = "Lambda and fold", + question = "What result does this operation produce?", + codeSnippet = null, + options = listOf( + "10", + "0", + "1234", + "Compile error" + ), + explanation = "fold(0) iterates with an initial value of 0: 0+1=1, 1+2=3, 3+3=6, 6+4=10. The result is the total sum, 10." + ), + + // ========== JAVASCRIPT ========== + 4 to ChallengeTextsEn( + language = "JavaScript", + title = "Weird Data Types", + question = "What does the expression 'typeof null' return in JavaScript?", + codeSnippet = null, + options = listOf( + "\"null\"", + "\"undefined\"", + "\"object\"", + "\"string\"" + ), + explanation = "Historically in JavaScript, 'typeof null' returns '\"object\"'. It's considered a mistake in the original design of the language, but it hasn't been changed for compatibility." + ), + 5 to ChallengeTextsEn( + language = "JavaScript", + title = "Strict Comparison", + question = "Why does the strict comparison '0 === false' evaluate to 'false'?", + codeSnippet = null, + options = listOf( + "Because === performs automatic type coercion", + "Because === compares both the value and the type without coercion", + "Because 0 is not a falsy value", + "The JS compiler has a bug with numbers" + ), + explanation = "The '==' operator converts both sides to a common type (coercion), so 0 and false match. The '===' operator is strict and does no coercion, catching that Number is not Boolean." + ), + 6 to ChallengeTextsEn( + language = "JavaScript", + title = "Closures", + question = "What is a Closure in JavaScript?", + codeSnippet = null, + options = listOf( + "A function that closes the browser window", + "The process of compressing code files for production", + "The combination of a function and the lexical environment in which it was declared", + "A reserved method for destroying local variables" + ), + explanation = "A closure gives an inner function access to its outer function's scope, even after the outer function has finished executing." + ), + 26 to ChallengeTextsEn( + language = "JavaScript", + title = "let vs var", + question = "What's the main difference between 'let' and 'var'?", + codeSnippet = null, + options = listOf( + "No difference, let is just a modern alternative", + "let has block scope, var has function scope", + "var can't be used in modern browsers", + "let only works inside objects" + ), + explanation = "'let' has block scope (it only exists inside {}), while 'var' has function scope and gets hoisted." + ), + 27 to ChallengeTextsEn( + language = "JavaScript", + title = "Promises - then", + question = "What does the following code print?", + codeSnippet = null, + options = listOf( + "1", + "2", + "undefined", + "Promise {}" + ), + explanation = "Promise.resolve(1) creates a promise with the value 1. The first then adds 1 (result 2). The second then prints 2." + ), + 28 to ChallengeTextsEn( + language = "JavaScript", + title = "Spread Operator", + question = "What does the '...' (spread) operator do on an array?", + codeSnippet = null, + options = listOf( + "Concatenates all the arrays in the application", + "Expands an array into its individual elements", + "Removes the last element of the array", + "Creates an empty copy of the array" + ), + explanation = "The spread operator '...' expands an iterable (like an array) into its individual elements, useful for copying or combining arrays." + ), + 29 to ChallengeTextsEn( + language = "JavaScript", + title = "Template Literals", + question = "How do you interpolate a variable into a string with template literals?", + codeSnippet = null, + options = listOf( + "\"Hello \" + name + \"!\"", + "`Hello \${name}!`", + "'Hello {name}!'", + "\"Hello %name%!\"" + ), + explanation = "Template literals use backticks ` and the \${} syntax to interpolate variables or expressions directly into the string." + ), + 30 to ChallengeTextsEn( + language = "JavaScript", + title = "Arrow Functions", + question = "What distinguishes an arrow function from a traditional function?", + codeSnippet = null, + options = listOf( + "It can't have parameters", + "It doesn't have its own 'this', inheriting it from the parent scope", + "It only works with numbers", + "It can't return values" + ), + explanation = "Arrow functions don't have their own 'this', inheriting it from the surrounding scope. They also lack 'arguments' and can't be used as constructors." + ), + 31 to ChallengeTextsEn( + language = "JavaScript", + title = "Hoisting with var", + question = "What does this code return?", + codeSnippet = null, + options = listOf( + "5", + "undefined", + "ReferenceError: x is not defined", + "null" + ), + explanation = "With 'var', the declaration is hoisted to the top, but the assignment isn't. When console.log runs, x is declared but its value is undefined." + ), + 32 to ChallengeTextsEn( + language = "JavaScript", + title = "Destructuring Assignment", + question = "What does destructuring do with arrays?", + codeSnippet = null, + options = listOf( + "Removes unwanted elements from the array", + "Extracts values from the array into individual variables with a concise syntax", + "Encrypts the array's data for security", + "Converts the array into an object" + ), + explanation = "Destructuring lets you extract values from arrays or objects into individual variables: const [a, b] = [1, 2] assigns 1 to 'a' and 2 to 'b'." + ), + 33 to ChallengeTextsEn( + language = "JavaScript", + title = "map method", + question = "What does the array 'map' method return?", + codeSnippet = null, + options = listOf( + "A new array with the same number of elements, transformed", + "A new array with only the elements that meet a condition", + "The first element that meets a condition", + "A boolean indicating whether all elements meet the condition" + ), + explanation = "'map' iterates over each element and applies a function, returning a new array of the same length with the transformed values." + ), + 34 to ChallengeTextsEn( + language = "JavaScript", + title = "Event Loop", + question = "In what order are these messages printed?", + codeSnippet = null, + options = listOf( + "A, B, C", + "B, A, C", + "A, C, B", + "C, B, A" + ), + explanation = "A and C are synchronous (they run first). setTimeout, with a 0 delay, goes to the task queue and runs after the synchronous stack completes." + ), + 35 to ChallengeTextsEn( + language = "JavaScript", + title = "Falsy Values", + question = "How many of these values are falsy in JavaScript? 0, '', false, null, undefined, NaN, []", + codeSnippet = null, + options = listOf( + "All of them are falsy", + "6, except [] (empty arrays are truthy)", + "4: 0, '', false, null", + "Only 0 and false" + ), + explanation = "The falsy values are: 0, '' (empty string), false, null, undefined, NaN (6 in total). [] (empty array) is truthy." + ), + 36 to ChallengeTextsEn( + language = "JavaScript", + title = "Fetch API", + question = "How is the response of a fetch call handled?", + codeSnippet = null, + options = listOf( + "fetch directly returns the JSON", + "fetch returns a Promise that resolves to a Response object", + "fetch is synchronous and blocks the thread", + "fetch only works with local files" + ), + explanation = "fetch() returns a Promise that resolves to a Response object. To get the JSON, you must call response.json() (also a Promise)." + ), + 37 to ChallengeTextsEn( + language = "JavaScript", + title = "Implicit Coercion", + question = "What does '5' - 3 return in JavaScript?", + codeSnippet = null, + options = listOf( + "\"53\"", + "2", + "\"5-3\"", + "TypeError error" + ), + explanation = "With the '-' operator, JavaScript converts the string '5' to a number (5) and subtracts 3, giving 2. By contrast, '5' + 3 would give \"53\" due to concatenation." + ), + 38 to ChallengeTextsEn( + language = "JavaScript", + title = "this in a Function", + question = "What does this code print (in the browser)?", + codeSnippet = null, + options = listOf( + "undefined", + "The global object (window)", + "null", + "The foo object" + ), + explanation = "In a normal non-strict function, 'this' inside the function refers to the global object (window in the browser). In strict mode it would be undefined." + ), + 39 to ChallengeTextsEn( + language = "JavaScript", + title = "JSON.stringify", + question = "What does JSON.stringify() do?", + codeSnippet = null, + options = listOf( + "Converts a JSON string into a JavaScript object", + "Converts a JavaScript object into a JSON string", + "Validates whether a string is valid JSON", + "Minifies a string by removing spaces" + ), + explanation = "JSON.stringify() serializes a JavaScript object to its JSON string representation. The opposite is JSON.parse()." + ), + 40 to ChallengeTextsEn( + language = "JavaScript", + title = "null vs undefined", + question = "What's the difference between null and undefined?", + codeSnippet = null, + options = listOf( + "They're exactly the same, synonyms", + "undefined means 'not assigned', null means 'intentionally empty'", + "null only exists in strict mode", + "undefined is a string, null is an object" + ), + explanation = "undefined indicates that a variable was declared but not assigned a value. null is a value intentionally assigned to indicate 'no value'." + ), + + // ========== PHP ========== + 7 to ChallengeTextsEn( + language = "PHP", + title = "Basic Variable Syntax", + question = "What is the mandatory prefix to declare and use any variable in PHP?", + codeSnippet = null, + options = listOf( + "The percent sign (%)", + "The dollar sign ($)", + "The 'var' keyword", + "No prefix, it's defined like in C++" + ), + explanation = "In PHP, every variable must start with the '$' symbol followed by the variable name." + ), + 8 to ChallengeTextsEn( + language = "PHP", + title = "Merging Arrays", + question = "Which native PHP function is used to combine two indexed arrays preserving values?", + codeSnippet = "\$array1 = [1, 2];\n\$array2 = [3, 4];\n\$result = ???(\$array1, \$array2);", + options = listOf( + "array_combine", + "array_merge", + "array_concat", + "implode" + ), + explanation = "'array_merge()' combines the elements of one or more arrays together, so the values of one are appended to the end of the previous one." + ), + 9 to ChallengeTextsEn( + language = "PHP", + title = "Strict Comparison", + question = "What is the result of '123 == \"123\"' versus '123 === \"123\"' in PHP?", + codeSnippet = null, + options = listOf( + "Both return true", + "Both return false", + "The first is true (coercion), the second is false (strict)", + "The first is false, the second is true" + ), + explanation = "Like JS, '==' in PHP performs type coercion (converting the string to a number for the comparison), while '===' strictly checks the type (int vs string)." + ), + 41 to ChallengeTextsEn( + language = "PHP", + title = "Superglobal Variables", + question = "Which superglobal contains the data sent by a form with the POST method?", + codeSnippet = null, + options = listOf( + "\$_GET", + "\$_POST", + "\$_SERVER", + "\$_REQUEST" + ), + explanation = "\$_POST contains form data sent with method='post', including files (combined with \$_FILES)." + ), + 42 to ChallengeTextsEn( + language = "PHP", + title = "foreach with Arrays", + question = "What is the correct syntax to iterate over an associative array?", + codeSnippet = null, + options = listOf( + "foreach(\$array as \$value)", + "foreach(\$array as \$key => \$value)", + "for(\$i = 0; \$i < count(\$array); \$i++)", + "while(\$item = next(\$array))" + ), + explanation = "The foreach(\$array as \$key => \$value) syntax iterates over associative arrays giving access to both the key and the value." + ), + 43 to ChallengeTextsEn( + language = "PHP", + title = "isset vs empty", + question = "Which function returns true if a variable exists and is not null?", + codeSnippet = null, + options = listOf( + "true because 0 is a valid value", + "false because 0 is considered empty", + "true because empty only checks whether the variable exists", + "It throws a type error" + ), + explanation = "empty(\$var) returns true for 'empty' values: \"\", 0, \"0\", null, false, array(), and undefined variables. Since \$var = 0, empty() returns true." + ), + 44 to ChallengeTextsEn( + language = "PHP", + title = "Classes and Objects", + question = "How is a class defined in PHP?", + codeSnippet = null, + options = listOf( + "class Person { }", + "new class Person() { }", + "function Person() { }", + "type Person struct { }" + ), + explanation = "In PHP, classes are defined with 'class ClassName { }', similar to other languages like Java or C++." + ), + 45 to ChallengeTextsEn( + language = "PHP", + title = "Inheritance in PHP", + question = "Which keyword is used to inherit from a parent class?", + codeSnippet = null, + options = listOf( + "implements", + "extends", + "inherits", + "parent" + ), + explanation = "PHP uses 'extends' for class inheritance: class Child extends Parent { }. For interfaces, 'implements' is used." + ), + 46 to ChallengeTextsEn( + language = "PHP", + title = "echo vs print_r", + question = "Which function is more suitable for inspecting the contents of an array?", + codeSnippet = "\$data = ['a' => 1, 'b' => 2];\necho \$data; // Error\n???", + options = listOf( + "echo only works for strings, use print_r(\$data) or var_dump(\$data)", + "Use echo \$data with double quotes", + "Arrays can't be inspected in PHP", + "Use console.log(\$data)" + ), + explanation = "echo only prints strings. To see the contents of an array or object, use print_r() or var_dump(), which show the full structure." + ), + 47 to ChallengeTextsEn( + language = "PHP", + title = "Sessions in PHP", + question = "Which function starts or resumes a session in PHP?", + codeSnippet = null, + options = listOf( + "start_session()", + "session_start()", + "begin_session()", + "init_session()" + ), + explanation = "session_start() starts a new session or resumes the existing one based on the session ID (in a cookie or URL)." + ), + 48 to ChallengeTextsEn( + language = "PHP", + title = "include vs require", + question = "What's the difference between include and require?", + codeSnippet = null, + options = listOf( + "include includes the file, require executes it", + "require throws a fatal error if the file doesn't exist, include only a warning", + "No difference, they're aliases", + "include loads the file asynchronously" + ), + explanation = "Both include files, but require produces a fatal error (E_COMPILE_ERROR) if it fails, stopping the script. include only emits a warning (E_WARNING)." + ), + 49 to ChallengeTextsEn( + language = "PHP", + title = "Associative Arrays", + question = "How do you access the value with the key 'name' in an associative array?", + codeSnippet = null, + options = listOf( + "\$person->name", + "\$person['name']", + "\$person{name}", + "\$person::name" + ), + explanation = "Associative arrays in PHP use brackets with the key in quotes: \$person['name']. The arrow '->' is used for object properties." + ), + 50 to ChallengeTextsEn( + language = "PHP", + title = "String Concatenation", + question = "What is the concatenation operator in PHP?", + codeSnippet = "\$a = 'Hello ';\n\$b = 'World';\n\$c = ???", + options = listOf( + "\$a + \$b", + "\$a . \$b", + "\$a & \$b", + "\$a :: \$b" + ), + explanation = "PHP uses the dot (.) as the string concatenation operator: 'Hello ' . 'World' produces 'Hello World'." + ), + 51 to ChallengeTextsEn( + language = "PHP", + title = "Variable Functions", + question = "What do you call a function whose name is stored in a variable?", + codeSnippet = null, + options = listOf( + "\$functionName()", + "call(\$functionName)", + "invoke(\$functionName)", + "Not possible in PHP" + ), + explanation = "PHP allows calling functions by dynamic name: if \$functionName = 'strlen', then \$functionName('hello') calls strlen('hello')." + ), + 52 to ChallengeTextsEn( + language = "PHP", + title = "Static Methods", + question = "How do you access a static method from inside the same class?", + codeSnippet = null, + options = listOf( + "\$this->method()", + "self::method()", + "static.method()", + "class::method()" + ), + explanation = "Inside a class, static methods and properties are accessed with self:: or static:: (late static binding)." + ), + 53 to ChallengeTextsEn( + language = "PHP", + title = "PDO and Prepared Statements", + question = "What is the main benefit of prepared statements in PDO?", + codeSnippet = null, + options = listOf( + "They're faster than regular queries", + "They prevent SQL injection by separating the SQL structure from the data", + "They allow connecting multiple databases at once", + "They don't require a database connection" + ), + explanation = "Prepared statements send the query structure separately from the data, preventing malicious data from altering the intent of the SQL." + ), + 54 to ChallengeTextsEn( + language = "PHP", + title = "Cookies in PHP", + question = "Which function sets a cookie in PHP?", + codeSnippet = "// What is the correct function?\n???('user', 'john', time() + 3600)", + options = listOf( + "set_cookie()", + "setcookie()", + "cookie_set()", + "http_set_cookie()" + ), + explanation = "setcookie() is PHP's native function for setting cookies. It must be called before any HTML output." + ), + 55 to ChallengeTextsEn( + language = "PHP", + title = "Ternary Operator", + question = "What is the syntax of the ternary operator in PHP?", + codeSnippet = null, + options = listOf( + "cond ? if_true : if_false", + "cond :: if_true :: if_false", + "if cond then if_true else if_false", + "cond -> if_true -> if_false" + ), + explanation = "PHP uses the same syntax as C/Java: condition ? value_if_true : value_if_false." + ), + + // ========== PYTHON ========== + 10 to ChallengeTextsEn( + language = "Python", + title = "Mutable Data Types", + question = "Which of the following Python data structures is IMMUTABLE?", + codeSnippet = null, + options = listOf( + "List [1, 2, 3]", + "Dictionary {'a': 1}", + "Tuple (1, 2, 3)", + "Set {1, 2, 3}" + ), + explanation = "In Python, tuples '(1, 2, 3)' are immutable. Once created, their elements can't be modified, added, or removed." + ), + 11 to ChallengeTextsEn( + language = "Python", + title = "List Comprehension", + question = "What is the result of the following list comprehension?", + codeSnippet = "numbers = [1, 2, 3, 4]\nsquares = [x**2 for x in numbers if x % 2 == 0]", + options = listOf( + "[1, 4, 9, 16]", + "[4, 16]", + "[1, 9]", + "[2, 4]" + ), + explanation = "The loop filters the even numbers (2 and 4) using 'if x % 2 == 0', then computes their squares (2**2 = 4 and 4**2 = 16), giving [4, 16]." + ), + 56 to ChallengeTextsEn( + language = "Python", + title = "PEP 8 - Naming", + question = "What is the PEP 8 convention for naming functions and variables in Python?", + codeSnippet = null, + options = listOf( + "camelCase", + "snake_case", + "PascalCase", + "kebab-case" + ), + explanation = "PEP 8 recommends using snake_case (words_separated_by_underscores) for functions, variables, and methods. PascalCase for classes." + ), + 57 to ChallengeTextsEn( + language = "Python", + title = "Lists vs Tuples", + question = "Why are tuples faster than lists in certain contexts?", + codeSnippet = null, + options = listOf( + "Tuples are stored on disk, not in memory", + "Tuples are immutable, so Python can optimize their storage and access", + "Tuples don't have methods, only global functions", + "Tuples are compiled to machine code" + ), + explanation = "Being immutable, Python can apply memory optimizations to tuples. They don't need extra space for potential modifications." + ), + 58 to ChallengeTextsEn( + language = "Python", + title = "with Statement", + question = "What is the 'with' statement used for in Python?", + codeSnippet = null, + options = listOf( + "To run code blocks in parallel", + "To manage resources (files, connections) ensuring they're automatically closed", + "To declare variables inside a temporary scope", + "It's a synonym for 'if' in modern versions" + ), + explanation = "'with' uses context managers to ensure resources are properly cleaned up when leaving the block, such as closing files automatically." + ), + 59 to ChallengeTextsEn( + language = "Python", + title = "Mutable as Default", + question = "What problem does this function have?", + codeSnippet = "def add_item(item, items=[]):\n items.append(item)\n return items", + options = listOf( + "No problem, it works correctly", + "The mutable default argument is shared across all calls", + "append doesn't exist for lists", + "You can't assign a default to a parameter" + ), + explanation = "Default arguments are evaluated once when the function is defined. If the default is mutable, all calls share the same list." + ), + 60 to ChallengeTextsEn( + language = "Python", + title = "args and kwargs", + question = "What does *args do in a Python function?", + codeSnippet = null, + options = listOf( + "Converts the arguments into a NumPy array", + "Allows passing a variable number of positional arguments as a tuple", + "Indicates the function is private", + "Multiplies the argument values" + ), + explanation = "*args collects the extra positional arguments into a tuple. **kwargs collects keyword arguments into a dictionary." + ), + 61 to ChallengeTextsEn( + language = "Python", + title = "Decorators", + question = "What is a decorator in Python?", + codeSnippet = null, + options = listOf( + "A function that modifies the behavior of another function", + "A special method like __init__", + "A class that extends system functionality", + "A syntax for declaring constant variables" + ), + explanation = "A decorator is a function that takes another function and extends its behavior without modifying its source code, using @decorator_name." + ), + 62 to ChallengeTextsEn( + language = "Python", + title = "range in Loops", + question = "What does this code print?", + codeSnippet = null, + options = listOf( + "1 2 3", + "0 1 2", + "0 1 2 3", + "1 2" + ), + explanation = "range(3) generates the numbers 0, 1, 2 (exclusive stop). By default it starts at 0. It prints '0 1 2'." + ), + 63 to ChallengeTextsEn( + language = "Python", + title = "Dictionaries", + question = "How do you get a value from a dictionary safely (without KeyError)?", + codeSnippet = null, + options = listOf( + "dict.value('key')", + "dict.get('key', default)", + "dict['key'] with a mandatory try-except", + "dict.fetch('key')" + ), + explanation = "dict.get('key', default) returns the value if the key exists, or the default (None if not specified) without raising an exception." + ), + 64 to ChallengeTextsEn( + language = "Python", + title = "F-Strings", + question = "What is the correct f-string syntax?", + codeSnippet = null, + options = listOf( + "f'Hello {name}'", + "F('Hello %s', name)", + "'Hello {name}'.format(name)", + "printf('Hello', name)" + ), + explanation = "f-strings (Python 3.6+) are written with the 'f' or 'F' prefix and use { } to interpolate variables or expressions." + ), + 65 to ChallengeTextsEn( + language = "Python", + title = "Exceptions", + question = "What is the correct syntax for catching an exception?", + codeSnippet = null, + options = listOf( + "try { } catch (e) { }", + "try: ... except Exception as e: ...", + "try: ... catch (e): ...", + "attempt: ... except: ..." + ), + explanation = "Python uses 'try: ... except ErrorType as e: ...' for exception handling. It doesn't use braces, but indentation." + ), + 66 to ChallengeTextsEn( + language = "Python", + title = "Multiple Inheritance", + question = "Does Python support multiple inheritance?", + codeSnippet = null, + options = listOf( + "No, Python doesn't allow multiple inheritance", + "Yes, class Child(Parent1, Parent2):", + "Only if you use the @multiple decorator", + "Yes, but only with abstract classes" + ), + explanation = "Python supports multiple inheritance: class Child(Parent1, Parent2):. The MRO (Method Resolution Order) determines the lookup order." + ), + 67 to ChallengeTextsEn( + language = "Python", + title = "Generators and yield", + question = "What distinguishes a generator from a regular function?", + codeSnippet = null, + options = listOf( + "Generators can't have parameters", + "Generators use yield and return an iterator that produces values on demand", + "Generators only work with integers", + "No difference, they're the same" + ), + explanation = "A generator uses 'yield' instead of 'return' and produces a sequence of values that can be iterated over, keeping its state between calls." + ), + 68 to ChallengeTextsEn( + language = "Python", + title = "Shallow Copies", + question = "What happens when you modify a list inside a copy made with list()?", + codeSnippet = null, + options = listOf( + "1 (the copy is independent)", + "99 (list() makes a shallow copy, the inner elements are shared)", + "Error: nested lists can't be copied", + "None" + ), + explanation = "list() makes a shallow copy. The inner lists are the same objects, so modifying one affects the other." + ), + 69 to ChallengeTextsEn( + language = "Python", + title = "Sets in Python", + question = "What characteristic defines a set in Python?", + codeSnippet = null, + options = listOf( + "It keeps insertion order and allows duplicates", + "It doesn't allow duplicate elements and has no defined order", + "It can only contain strings", + "It's the same as a list but with extra methods" + ), + explanation = "A set is an unordered collection of unique elements. It's used for membership tests and set operations (union, intersection)." + ), + 70 to ChallengeTextsEn( + language = "Python", + title = "Modules and Packages", + question = "What file does a directory need to be considered a package in Python?", + codeSnippet = null, + options = listOf( + "package.json", + "__init__.py", + "main.py", + "setup.py" + ), + explanation = "The __init__.py file (which can be empty) indicates that a directory is a Python package, allowing its modules to be imported." + ), + 71 to ChallengeTextsEn( + language = "Python", + title = "Division in Python 3", + question = "What is the result of 5 / 2 in Python 3?", + codeSnippet = "result = 5 / 2\nprint(result)", + options = listOf( + "2", + "2.5", + "2.0", + "Error: division not allowed" + ), + explanation = "In Python 3, '/' always returns a float (2.5). For integer division use '//' (5 // 2 = 2)." + ), + 72 to ChallengeTextsEn( + language = "Python", + title = "Class Methods", + question = "How do you define a class method in Python?", + codeSnippet = null, + options = listOf( + "With the @staticmethod decorator", + "With the @classmethod decorator and 'cls' as the first parameter", + "With the 'class' keyword inside the method", + "Class methods can't be defined" + ), + explanation = "@classmethod receives the class (cls) as the first argument, unlike @staticmethod which receives neither cls nor self." + ), + 73 to ChallengeTextsEn( + language = "Python", + title = "zip function", + question = "What does the zip() function do in Python?", + codeSnippet = null, + options = listOf( + "Compresses files in ZIP format", + "Combines multiple iterables into tuples, in parallel", + "Sorts the elements of a list", + "Converts strings to numbers" + ), + explanation = "zip() takes multiple iterables and returns an iterator of tuples, where the i-th tuple contains the i-th element from each iterable." + ), + 74 to ChallengeTextsEn( + language = "Python", + title = "Global Variables", + question = "How do you modify a global variable inside a function?", + codeSnippet = null, + options = listOf( + "By simply assigning it a new value", + "Using the 'global' keyword followed by the variable name", + "Putting 'var' in front of the name", + "Global variables can't be modified from functions" + ), + explanation = "To modify a global variable inside a function, it must be declared with 'global variable_name' before assigning a value to it." + ), + 75 to ChallengeTextsEn( + language = "Python", + title = "Dictionary Comprehension", + question = "What is the syntax of a dictionary comprehension?", + codeSnippet = null, + options = listOf( + "{k.upper(): v for k, v in dict.items()}", + "[k: v for k, v in dict.items()]", + "dict(k.upper(): v for k, v in dict.items())", + "for k, v in dict.items(): {k: v}" + ), + explanation = "Dictionary comprehensions use braces {} with key: value: {key: value for item in iterable}. Similar to lists but with a :." + ), + 76 to ChallengeTextsEn( + language = "Python", + title = "lambda", + question = "What is a lambda function in Python?", + codeSnippet = null, + options = listOf( + "An anonymous single-expression function", + "A function that can have multiple lines and decorators", + "The integer division operator", + "An async function with await" + ), + explanation = "lambda creates anonymous one-line functions: lambda args: expression. It's equivalent to a small, throwaway function." + ), + 77 to ChallengeTextsEn( + language = "Python", + title = "re.match vs re.search", + question = "What's the difference between re.match() and re.search()?", + codeSnippet = null, + options = listOf( + "match searches the whole string, search only at the start", + "match only searches at the start of the string, search looks through the whole string", + "Both do the same thing, match is obsolete", + "match uses regex, search uses exact matching" + ), + explanation = "re.match() checks only from the beginning of the string; re.search() scans the whole string for the first match." + ), + 78 to ChallengeTextsEn( + language = "Python", + title = "assert in Python", + question = "What does the 'assert' statement do?", + codeSnippet = null, + options = listOf( + "Stops execution if the condition is true", + "Raises AssertionError if the condition is false, used for debugging", + "Affirms that a variable exists in the global scope", + "Declares a variable as constant" + ), + explanation = "assert condition, message raises AssertionError with the message if the condition is false. It's used to validate invariants at development time." + ), + 79 to ChallengeTextsEn( + language = "Python", + title = "enumerate in Loops", + question = "What does the enumerate() function do in a for loop?", + codeSnippet = null, + options = listOf( + "Returns the index and the value of each element", + "Counts how many elements are in the iterable", + "Assigns a unique number to each element", + "Sorts the elements numerically" + ), + explanation = "enumerate() returns (index, value) tuples for each element, avoiding the need for a manual counter variable." + ), + 80 to ChallengeTextsEn( + language = "Python", + title = "Mutable vs Immutable", + question = "What does this code print?", + codeSnippet = null, + options = listOf( + "[1, 2, 3]", + "[1, 2, 3, 4]", + "[4]", + "Error: lists can't be assigned" + ), + explanation = "b = a doesn't copy the list; b references the same list as a. Modifying a affects b. To copy, use a.copy() or list(a)." + ), + 81 to ChallengeTextsEn( + language = "Python", + title = "sys.argv", + question = "What does sys.argv contain?", + codeSnippet = null, + options = listOf( + "The command-line arguments passed to the script", + "The system environment variables", + "The currently imported modules", + "The Python search paths" + ), + explanation = "sys.argv is a list with the command-line arguments. sys.argv[0] is the script name, sys.argv[1:] are the arguments." + ), + 82 to ChallengeTextsEn( + language = "Python", + title = "Python and JSON", + question = "Which function converts a JSON string into a Python object?", + codeSnippet = null, + options = listOf( + "json.stringify()", + "json.loads()", + "json.parse()", + "json.decode()" + ), + explanation = "json.loads() (load string) converts a JSON string into a Python object. json.dumps() does the reverse operation (object to string)." + ), + 83 to ChallengeTextsEn( + language = "Python", + title = "Inheritance and super()", + question = "What is super() used for in Python?", + codeSnippet = null, + options = listOf( + "To call the parent class version of a method", + "To declare a class as higher in the hierarchy", + "To improve method performance", + "super() doesn't exist in Python" + ), + explanation = "super() returns a proxy object that delegates method calls to the parent class, following the MRO (Method Resolution Order)." + ), + 84 to ChallengeTextsEn( + language = "Python", + title = "Virtualenv", + question = "What is the purpose of a virtual environment (virtualenv)?", + codeSnippet = null, + options = listOf( + "To create a virtual machine to run Python", + "To isolate a Python project's dependencies from the global system", + "To simulate a production environment locally", + "To speed up Python code execution" + ), + explanation = "A virtual environment isolates each Python project's dependencies, avoiding conflicts between library versions." + ), + 85 to ChallengeTextsEn( + language = "Python", + title = "Type Hints", + question = "How do you indicate a function's return type with type hints?", + codeSnippet = null, + options = listOf( + "def sum(a: int, b: int) -> int:", + "def sum(int a, int b) returns int:", + "def sum(a: int, b: int) :: int", + "@typedef sum(a: int, b: int) -> int" + ), + explanation = "Type hints use ': type' for parameters and '-> type' for the return: def sum(a: int, b: int) -> int:" + ), + 88 to ChallengeTextsEn( + language = "Python", + title = "Exception in Division", + question = "Which exception does Python raise when dividing by zero?", + codeSnippet = "result = 10 / 0", + options = listOf( + "ValueError", + "ZeroDivisionError", + "TypeError", + "ArithmeticError (generic)" + ), + explanation = "Python raises ZeroDivisionError when you try to divide by zero. It's a subtype of ArithmeticError." + ), + 89 to ChallengeTextsEn( + language = "Python", + title = "Packages with pip", + question = "Which command installs a package from PyPI?", + codeSnippet = null, + options = listOf( + "python get package_name", + "pip install package_name", + "pip download package_name", + "python install package_name" + ), + explanation = "pip install package_name downloads and installs the package from PyPI (Python Package Index)." + ), + 90 to ChallengeTextsEn( + language = "Python", + title = "asyncio", + question = "Which keyword is used to define an async function in Python?", + codeSnippet = null, + options = listOf( + "async def", + "async function", + "coroutine def", + "def async" + ), + explanation = "Async functions are defined with 'async def'. Inside them, 'await' is used to call other async functions." + ), + ) +} diff --git a/app/src/main/java/com/tamagotchi/code/data/CodeCardsEn.kt b/app/src/main/java/com/tamagotchi/code/data/CodeCardsEn.kt new file mode 100644 index 0000000..49ce314 --- /dev/null +++ b/app/src/main/java/com/tamagotchi/code/data/CodeCardsEn.kt @@ -0,0 +1,47 @@ +package com.tamagotchi.code.data + +// English translations keyed by CodeCard.id. Jokes land in English. +object CodeCardsEn { + val texts: Map = mapOf( + 1 to "The first bug in history was an actual moth trapped in a relay of the Harvard Mark II in 1947.", + 2 to "Did you know the most expensive programming language in the world is APL? It required a special keyboard.", + 3 to "Tip: Use descriptive names for your variables. Future you will thank you.", + 4 to "The term 'debugging' comes from removing moths (bugs) from computers.", + 5 to "What is a cat's favorite programming language? Scratch.", + 6 to "In 1960 there were more than 2000 programming languages. Today about 250 survive.", + 7 to "The first compiler was written by Grace Hopper in 1952. She also popularized the term 'bug'.", + 8 to "Tip: Good code doesn't need comments. Excellent code has clear ones.", + 9 to "Did you know Python is named after Monty Python? Not the snake.", + 10 to "The {} symbol is called 'llave' in Spanish, 'curly brace' in English, and 'accolade' in French.", + 11 to "Why do programmers confuse Halloween with Christmas? Because Oct 31 == Dec 25.", + 12 to "There are 10 types of people: those who understand binary and those who don't.", + 13 to "Tip: Write tests before the code. Future you (and your team) will thank you.", + 14 to "The oldest programming language still in use today is Fortran (1957).", + 15 to "What did one bit say to the other? 'See you on the bus'.", + 16 to "Tip: One commit per logical change. Don't do 'fixed stuff' in a single giant commit.", + 17 to "The first computer virus was created in 1971 and was called 'Creeper'.", + 18 to "How does a programmer get out of the shower? With 'shower -exit'.", + 19 to "Tip: Learn a new keyboard shortcut every week. Small changes, big impact.", + 20 to "Java was originally called 'Oak'. It was renamed because a trademark already existed.", + 21 to "How many programmers does it take to change a light bulb? None, that's a hardware problem.", + 22 to "Tip: Code is written for people, not machines. Readability matters.", + 23 to "The first website in history is still online: info.cern.ch", + 24 to "Why did the programmer quit his job? Because he didn't get arrays.", + 25 to "Tip: If your code works but you don't know why, that's called 'programming by coincidence'.", + 26 to "The # symbol is called 'hash', 'pound' or 'number sign'. In programming it's 'sharp' or 'hashtag'.", + 27 to "What is a programmer's favorite drink? Java.", + 28 to "Tip: Technical debt is paid with interest. Better refactor today.", + 29 to "The CD-ROM was invented in 1982. It could store 650 MB, which today fits in a single photo.", + 30 to "Why do programmers hate nature? It has too many bugs.", + 31 to "Tip: Don't optimize prematurely. First make it work, then make it fast.", + 32 to "Linux has more than 27 million lines of code. And it keeps growing.", + 33 to "What is an algorithm? A word programmers use instead of saying 'I don't know how to do it'.", + 34 to "Tip: Use version control even if you work alone. Your past self will thank you.", + 35 to "The first programmer in history was Ada Lovelace, in 1843.", + 36 to "What does a programmer do when they're cold? Close all the windows except the terminal.", + 37 to "Tip: The best documentation is the code itself. Code lies less than comments.", + 38 to "The HTTP protocol has status code 418: 'I'm a teapot'.", + 39 to "What is a programmer's favorite animal? The moth (because of the bugs).", + 40 to "Tip: Divide and conquer. One big problem is just several small problems." + ) +} diff --git a/app/src/main/java/com/tamagotchi/code/data/CodeReviewDataEn.kt b/app/src/main/java/com/tamagotchi/code/data/CodeReviewDataEn.kt new file mode 100644 index 0000000..de21fbe --- /dev/null +++ b/app/src/main/java/com/tamagotchi/code/data/CodeReviewDataEn.kt @@ -0,0 +1,47 @@ +package com.tamagotchi.code.data + +// English translations keyed by CodeReviewSnippet.id. Code semantics kept identical. +object CodeReviewDataEn { + val translations: Map = mapOf( + 1 to ReviewTextsEn( + code = "fun sum(a: Int, b: Int): Int {\n return a - b\n}", + explanation = "The function should add, but it's subtracting." + ), + 2 to ReviewTextsEn( + code = "val list = listOf(1, 2, 3)\nval item = list[3]", + explanation = "IndexOutOfBoundsException: Index 3 does not exist in a list of 3 elements." + ), + 3 to ReviewTextsEn( + code = "fun greet(name: String?) {\n println(\"Hello \" + name.length)\n}", + explanation = "NullPointerException: Trying to access .length on a String that can be null." + ), + 4 to ReviewTextsEn( + code = "val x: Int = 10\nif (x == 10) {\n println(\"It's ten\")\n}", + explanation = "The code is correct." + ), + 5 to ReviewTextsEn( + code = "for (i in 0..10) {\n println(i)\n}", + explanation = "The loop iterates from 0 to 10 correctly." + ), + 6 to ReviewTextsEn( + code = "fun divide(a: Int, b: Int): Int {\n return a / b\n}", + explanation = "Missing handling of division by zero if b is 0." + ), + 7 to ReviewTextsEn( + code = "val map = mutableMapOf(\"a\" to 1)\nmap[\"b\"] = 2", + explanation = "Correct insertion into a mutable map." + ), + 8 to ReviewTextsEn( + code = "val name = \"Codey\"\nname = \"Buggy\"", + explanation = "A val cannot be reassigned." + ), + 9 to ReviewTextsEn( + code = "fun printAll(items: List) {\n items.forEach { println(it) }\n}", + explanation = "Correct use of forEach." + ), + 10 to ReviewTextsEn( + code = "lateinit var user: User\nprintln(user.name)", + explanation = "UninitializedPropertyAccessException if accessed before initialization." + ) + ) +} diff --git a/app/src/main/java/com/tamagotchi/code/data/LocalizedContent.kt b/app/src/main/java/com/tamagotchi/code/data/LocalizedContent.kt new file mode 100644 index 0000000..49e5883 --- /dev/null +++ b/app/src/main/java/com/tamagotchi/code/data/LocalizedContent.kt @@ -0,0 +1,43 @@ +package com.tamagotchi.code.data + +import java.util.Locale + +data class ChallengeTextsEn( + val language: String, + val title: String, + val question: String, + val codeSnippet: String? = null, + val options: List, + val explanation: String +) + +fun isEnglishContent(): Boolean = Locale.getDefault().language.equals("en", ignoreCase = true) + +fun CodingChallenge.localized(): CodingChallenge { + if (!isEnglishContent()) return this + val en = ChallengesDataEn.translations[id] + ?: SpecialChallengesDataEn.translations[id] + ?: return this + return copy( + language = en.language, + title = en.title, + question = en.question, + codeSnippet = en.codeSnippet ?: codeSnippet, + options = en.options, + explanation = en.explanation + ) +} + +data class ReviewTextsEn(val code: String, val explanation: String) + +fun CodeReviewSnippet.localized(): CodeReviewSnippet { + if (!isEnglishContent()) return this + val en = CodeReviewDataEn.translations[id] ?: return this + return copy(code = en.code, explanation = en.explanation) +} + +fun CodeCard.localized(): CodeCard { + if (!isEnglishContent()) return this + val en = CodeCardsEn.texts[id] ?: return this + return copy(text = en) +} diff --git a/app/src/main/java/com/tamagotchi/code/data/PersonalityMissions.kt b/app/src/main/java/com/tamagotchi/code/data/PersonalityMissions.kt index 4137e45..d691915 100644 --- a/app/src/main/java/com/tamagotchi/code/data/PersonalityMissions.kt +++ b/app/src/main/java/com/tamagotchi/code/data/PersonalityMissions.kt @@ -1,14 +1,15 @@ package com.tamagotchi.code.data -enum class QuestType(val displayName: String, val description: String, val rewardXp: Int, val rewardBytes: Int) { - STUDY_KOTLIN("Estudia Kotlin", "Hoy quiero estudiar Kotlin!", 100, 30), - GO_SHOP("Visita la tienda", "Llevame a la tienda a comprar algo!", 50, 20), - PLAY_BUG_HUNT("Juega Bug Hunt", "Juguemos Bug Hunt, hace tiempo que no!", 80, 40), - BUY_PIZZA("Compra pizza", "Tengo hambre, comprame una pizza!", 60, 0), - ACARICIAR("Acariciame 3 veces", "Hoy necesito mucho cariño!", 40, 15), - LIMPIAR("Limpia mi habitat", "Mi casa esta sucia, necesito que me limpies!", 50, 10), - FOCUS_25("Foco 25 minutos", "Pongamos musica de focus y trabajemos 25 minutos!", 120, 50), - COMPLETE_3_CHALLENGES("Completa 3 retos", "Hagamos ejercicios mentales!" , 150, 60) +// Display text for quests is resolved by enum name in the UI layer (R.string.quest_*) +enum class QuestType(val rewardXp: Int, val rewardBytes: Int) { + STUDY_KOTLIN(100, 30), + GO_SHOP(50, 20), + PLAY_BUG_HUNT(80, 40), + BUY_PIZZA(60, 0), + ACARICIAR(40, 15), + LIMPIAR(50, 10), + FOCUS_25(120, 50), + COMPLETE_3_CHALLENGES(150, 60) } data class ActiveQuest( diff --git a/app/src/main/java/com/tamagotchi/code/data/SpecialChallengesDataEn.kt b/app/src/main/java/com/tamagotchi/code/data/SpecialChallengesDataEn.kt new file mode 100644 index 0000000..ead317f --- /dev/null +++ b/app/src/main/java/com/tamagotchi/code/data/SpecialChallengesDataEn.kt @@ -0,0 +1,74 @@ +package com.tamagotchi.code.data + +// English translations keyed by CodingChallenge.id. Option ORDER matches the Spanish source. +object SpecialChallengesDataEn { + val translations: Map = mapOf( + 101 to ChallengeTextsEn( + language = "Special Logic", + title = "Find the Unique Element", + question = "Given an array of integers where every element appears three times except for one that appears exactly once. Find that unique element using O(1) extra memory and O(N) time.", + options = listOf( + "Use a hash table (Hash Map)", + "Sort the array and search linearly", + "Use bit counting modulo 3 at each position", + "XOR all the elements" + ), + explanation = "The optimal way to solve this is to count the number of set bits at each position i across all the numbers. If we take this sum modulo 3, we get the bit at position i of the unique number." + ), + 102 to ChallengeTextsEn( + language = "Special Logic", + title = "The Knapsack Problem", + question = "What is the classic approach to accurately solve the 0/1 Knapsack Problem?", + options = listOf( + "Greedy algorithm picking by highest value/weight ratio", + "Dynamic Programming with a 2D table or 1D vector", + "Binary Search", + "Topological Sorting" + ), + explanation = "The 0/1 Knapsack problem cannot be solved with a Greedy approach with guaranteed accuracy. It requires Dynamic Programming, evaluating subproblems of smaller capacities and available items." + ), + 103 to ChallengeTextsEn( + language = "Special Logic", + title = "Reverse a Linked List", + question = "How many pointers are typically needed (at minimum) to iteratively reverse a singly linked list in-place?", + options = listOf("1", "2", "3", "4"), + explanation = "You need 3 pointers: prev (to point to the previously processed node), current (the current node) and next (so you don't lose the rest of the list when breaking the link)." + ), + 104 to ChallengeTextsEn( + language = "Special Logic", + title = "CAP Theorem", + question = "In distributed systems, the CAP Theorem says you can only simultaneously guarantee 2 of 3 properties. Which ones?", + options = listOf( + "Consistency, Asynchrony, Partitioning", + "Consistency, Availability, Partition Tolerance", + "Control, Accessibility, Precision", + "Cache, API, Persistence" + ), + explanation = "CAP stands for Consistency, Availability, and Partition Tolerance. Because network partitions (P) are inevitable, a system must choose between C and A." + ), + 105 to ChallengeTextsEn( + language = "Special Logic", + title = "Detect a Cycle", + question = "Which two-pointer algorithm (one fast and one slow) is used to detect whether there is a cycle in a linked list?", + options = listOf( + "Dijkstra's Algorithm", + "Tortoise and Hare Algorithm (Floyd)", + "Depth-First Search (DFS)", + "A* Search" + ), + explanation = "Floyd's cycle-detection algorithm (Tortoise and Hare) uses two pointers advancing at different speeds. If there is a cycle, they will eventually meet." + ), + 106 to ChallengeTextsEn( + language = "Special Logic", + title = "AVL Trees", + question = "What condition must always hold in an AVL tree for it to be balanced?", + options = listOf( + "All leaf nodes must be at the same level", + "The height difference between the left and right subtrees of any node must be at most 1", + "The left subtree must always have more nodes than the right one", + "Node colors must not repeat red in sequence" + ), + explanation = "The balance factor in an AVL tree is strictly the height difference, which cannot exceed 1 or be less than -1." + ) + ) +} diff --git a/app/src/main/java/com/tamagotchi/code/data/repository/AchievementsRepository.kt b/app/src/main/java/com/tamagotchi/code/data/repository/AchievementsRepository.kt index 9040535..d701c6e 100644 --- a/app/src/main/java/com/tamagotchi/code/data/repository/AchievementsRepository.kt +++ b/app/src/main/java/com/tamagotchi/code/data/repository/AchievementsRepository.kt @@ -13,19 +13,20 @@ class AchievementsRepository(private val context: Context) { companion object { private val KEY_UNLOCKED_ACHIEVEMENTS = stringSetPreferencesKey("unlocked_achievements") + // Only stable ids are persisted; display text is resolved by id in the UI layer val ALL_ACHIEVEMENTS = listOf( - Achievement("primer_build", "Primer build", "Completá tu primera sesión Focus"), - Achievement("nivel_experto", "Nivel Experto", "Subí de nivel por primera vez"), - Achievement("cazador_de_bugs", "Cazador de bugs", "Puntuación perfecta en Bug Hunt"), - Achievement("git_sin_panico", "Git sin pánico", "Aprobá Git Rescue con 3+ aciertos"), - Achievement("racha_7", "Racha de 7 días", "Mantené una racha de estudio de 7 días"), - Achievement("racha_30", "Racha de 30 días", "Mantené una racha de estudio de 30 días"), - Achievement("coleccionista", "Coleccionista", "Desbloqueá 3 temas visuales"), - Achievement("completista", "Completista", "Desbloqueá los 12 temas visuales"), - Achievement("ahorrador", "Ahorrador", "Acumulá 1000 Bytes"), - Achievement("primer_acaricie", "Primer mimo", "Acariciá a tu mascota por primera vez"), - Achievement("duermevela", "Duermevela", "Poné a dormir a tu mascota"), - Achievement("limpiador", "Limpieza profunda", "Limpiá a tu mascota 10 veces"), + Achievement("primer_build"), + Achievement("nivel_experto"), + Achievement("cazador_de_bugs"), + Achievement("git_sin_panico"), + Achievement("racha_7"), + Achievement("racha_30"), + Achievement("coleccionista"), + Achievement("completista"), + Achievement("ahorrador"), + Achievement("primer_acaricie"), + Achievement("duermevela"), + Achievement("limpiador"), ) } @@ -48,6 +49,6 @@ class AchievementsRepository(private val context: Context) { data class Achievement( val id: String, - val name: String, - val description: String + val name: String = "", + val description: String = "" ) diff --git a/app/src/main/java/com/tamagotchi/code/data/repository/UserPreferencesRepository.kt b/app/src/main/java/com/tamagotchi/code/data/repository/UserPreferencesRepository.kt index dd17252..c4e5c97 100644 --- a/app/src/main/java/com/tamagotchi/code/data/repository/UserPreferencesRepository.kt +++ b/app/src/main/java/com/tamagotchi/code/data/repository/UserPreferencesRepository.kt @@ -7,6 +7,8 @@ import androidx.datastore.preferences.core.stringPreferencesKey import androidx.datastore.preferences.core.stringSetPreferencesKey import androidx.datastore.preferences.preferencesDataStore import com.tamagotchi.code.ui.theme.ThemeRegistry +import com.tamagotchi.code.util.DifficultyKey +import com.tamagotchi.code.util.TopicKey import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map @@ -68,11 +70,12 @@ class UserPreferencesRepository(private val context: Context) { } val selectedTopics: Flow> = context.dataStore.data.map { prefs -> - prefs[KEY_SELECTED_TOPICS] ?: setOf("Kotlin", "Estructuras de Datos", "Git") + (prefs[KEY_SELECTED_TOPICS] ?: setOf("Kotlin", "Estructuras de Datos", "Git")) + .map { TopicKey.normalize(it) }.toSet() } val difficulty: Flow = context.dataStore.data.map { prefs -> - prefs[KEY_DIFFICULTY] ?: "Inicial" + DifficultyKey.normalize(prefs[KEY_DIFFICULTY] ?: "Inicial") } val focusDurationDefault: Flow = context.dataStore.data.map { prefs -> diff --git a/app/src/main/java/com/tamagotchi/code/feature/games/BugHuntScreen.kt b/app/src/main/java/com/tamagotchi/code/feature/games/BugHuntScreen.kt index 127693d..57e48bf 100644 --- a/app/src/main/java/com/tamagotchi/code/feature/games/BugHuntScreen.kt +++ b/app/src/main/java/com/tamagotchi/code/feature/games/BugHuntScreen.kt @@ -36,7 +36,7 @@ private val snippetPool = listOf( humorRes = R.string.bug_humor_1 ), BugSnippet( - listOf("if (x = 5) {", "print(\"Cinco\")", "} else {", "print(\"Otro\")", "}"), + listOf("if (x = 5) {", "print(\"Five\")", "} else {", "print(\"Other\")", "}"), bugIndex = 0, explanationRes = R.string.bug_expl_2, humorRes = R.string.bug_humor_2 @@ -54,7 +54,7 @@ private val snippetPool = listOf( humorRes = R.string.bug_humor_4 ), BugSnippet( - listOf("fun main() {", "val msg = \"Hola\"", "println(msg)", "}//fin"), + listOf("fun main() {", "val msg = \"Hello\"", "println(msg)", "}//end"), bugIndex = 0, explanationRes = R.string.bug_expl_5, humorRes = R.string.bug_humor_5 @@ -84,7 +84,7 @@ private val snippetPool = listOf( humorRes = R.string.bug_humor_9 ), BugSnippet( - listOf("fun greet() {", "println(\"Hola\")", "", "", "", "}"), + listOf("fun greet() {", "println(\"Hello\")", "", "", "", "}"), bugIndex = 2, explanationRes = R.string.bug_expl_10, humorRes = R.string.bug_humor_10 diff --git a/app/src/main/java/com/tamagotchi/code/feature/games/CodeReviewScreen.kt b/app/src/main/java/com/tamagotchi/code/feature/games/CodeReviewScreen.kt index 8559148..dc9ed89 100644 --- a/app/src/main/java/com/tamagotchi/code/feature/games/CodeReviewScreen.kt +++ b/app/src/main/java/com/tamagotchi/code/feature/games/CodeReviewScreen.kt @@ -24,6 +24,7 @@ import androidx.compose.ui.unit.sp import com.tamagotchi.code.R import com.tamagotchi.code.data.CodeReviewData import com.tamagotchi.code.data.CodeReviewSnippet +import com.tamagotchi.code.data.localized import com.tamagotchi.code.ui.viewmodel.PetViewModel @Composable @@ -33,7 +34,7 @@ fun CodeReviewScreen( ) { var currentRound by remember { mutableIntStateOf(0) } var score by remember { mutableIntStateOf(0) } - val rounds = remember { CodeReviewData.snippets.shuffled().take(5) } + val rounds = remember { CodeReviewData.snippets.map { it.localized() }.shuffled().take(5) } var showResults by remember { mutableStateOf(false) } var selectedAnswer by remember { mutableStateOf(null) } var isCorrect by remember { mutableStateOf(null) } diff --git a/app/src/main/java/com/tamagotchi/code/feature/games/RefactorRushScreen.kt b/app/src/main/java/com/tamagotchi/code/feature/games/RefactorRushScreen.kt index 6fbdaf2..e6e3acc 100644 --- a/app/src/main/java/com/tamagotchi/code/feature/games/RefactorRushScreen.kt +++ b/app/src/main/java/com/tamagotchi/code/feature/games/RefactorRushScreen.kt @@ -54,8 +54,8 @@ private val puzzleBank = listOf( R.string.refactor_puzzle_3, listOf( "fun greet(name: String, age: Int): String {", - " val greeting = \"Hola, \$name\"", - " val ageMsg = if (age >= 18) \"Eres mayor\" else \"Eres menor\"", + " val greeting = \"Hello, \$name\"", + " val ageMsg = if (age >= 18) \"Adult\" else \"Minor\"", " return \"\$greeting. \$ageMsg\"", "}" ) diff --git a/app/src/main/java/com/tamagotchi/code/feature/learn/LearnScreen.kt b/app/src/main/java/com/tamagotchi/code/feature/learn/LearnScreen.kt index e330f47..3b6bad6 100644 --- a/app/src/main/java/com/tamagotchi/code/feature/learn/LearnScreen.kt +++ b/app/src/main/java/com/tamagotchi/code/feature/learn/LearnScreen.kt @@ -25,6 +25,7 @@ import androidx.compose.ui.unit.sp import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.tamagotchi.code.R import com.tamagotchi.code.data.CodingChallenge +import com.tamagotchi.code.data.localized import com.tamagotchi.code.data.database.PetStateEntity import com.tamagotchi.code.ui.viewmodel.PetViewModel @@ -438,7 +439,7 @@ fun QuizPanel(viewModel: PetViewModel) { @Composable fun SpecialChallengesPanel(viewModel: PetViewModel, state: PetStateEntity) { val unlockedThemes by viewModel.unlockedThemes.collectAsStateWithLifecycle() - var currentChallenge by remember { mutableStateOf(com.tamagotchi.code.data.SpecialChallengesData.challenges.random()) } + var currentChallenge by remember { mutableStateOf(com.tamagotchi.code.data.SpecialChallengesData.challenges.random().localized()) } var showFeedback by remember { mutableStateOf(null) } var unlockedThemeName by remember { mutableStateOf(null) } var isAnswered by remember { mutableStateOf(false) } @@ -612,7 +613,7 @@ fun SpecialChallengesPanel(viewModel: PetViewModel, state: PetStateEntity) { isAnswered = false showFeedback = null unlockedThemeName = null - currentChallenge = com.tamagotchi.code.data.SpecialChallengesData.challenges.random() + currentChallenge = com.tamagotchi.code.data.SpecialChallengesData.challenges.random().localized() }, colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.primary) ) { diff --git a/app/src/main/java/com/tamagotchi/code/feature/onboarding/OnboardingScreen.kt b/app/src/main/java/com/tamagotchi/code/feature/onboarding/OnboardingScreen.kt index 1e48ff8..8a9a96a 100644 --- a/app/src/main/java/com/tamagotchi/code/feature/onboarding/OnboardingScreen.kt +++ b/app/src/main/java/com/tamagotchi/code/feature/onboarding/OnboardingScreen.kt @@ -157,7 +157,7 @@ fun OnboardingScreen( ) ) { Text( - "Siguiente", + stringResource(R.string.onboarding_next), fontFamily = FontFamily.Monospace, fontWeight = FontWeight.Bold, fontSize = 15.sp @@ -373,21 +373,12 @@ private fun Step3Content( selectedTopics: Set, onTopicsChange: (Set) -> Unit ) { - val allTopics = listOf( - stringResource(R.string.topic_kotlin), - stringResource(R.string.topic_javascript), - stringResource(R.string.topic_python), - stringResource(R.string.topic_php), - stringResource(R.string.topic_sql), - stringResource(R.string.topic_git), - stringResource(R.string.topic_clean_code), - stringResource(R.string.topic_data_structures) - ) + val allTopics = com.tamagotchi.code.util.TopicKey.ALL.map { it to com.tamagotchi.code.util.TopicKey.displayRes(it) } val initialRoute = setOf( - stringResource(R.string.topic_kotlin), - stringResource(R.string.topic_git), - stringResource(R.string.topic_data_structures) + com.tamagotchi.code.util.TopicKey.KOTLIN, + com.tamagotchi.code.util.TopicKey.GIT, + com.tamagotchi.code.util.TopicKey.DATA_STRUCTURES ) Column( @@ -442,28 +433,29 @@ private fun Step3Content( horizontalArrangement = Arrangement.spacedBy(8.dp), verticalArrangement = Arrangement.spacedBy(8.dp) ) { - allTopics.forEach { topic -> + allTopics.forEach { (topicKey, labelRes) -> + val topicLabel = stringResource(labelRes) FilterChip( - selected = selectedTopics.contains(topic), + selected = selectedTopics.contains(topicKey), onClick = { val newSelection = selectedTopics.toMutableSet() - if (newSelection.contains(topic)) { + if (newSelection.contains(topicKey)) { if (newSelection.size > 1) { - newSelection.remove(topic) + newSelection.remove(topicKey) } } else { if (newSelection.size < 3) { - newSelection.add(topic) + newSelection.add(topicKey) } } if (newSelection.isNotEmpty()) { onTopicsChange(newSelection) } }, - label = { Text(topic, fontSize = 11.sp) }, + label = { Text(topicLabel, fontSize = 11.sp) }, leadingIcon = { com.tamagotchi.code.ui.components.TopicIcon( - topic = topic, + topic = topicKey, modifier = androidx.compose.ui.Modifier.size(18.dp) ) } diff --git a/app/src/main/java/com/tamagotchi/code/feature/settings/SettingsLanguageScreen.kt b/app/src/main/java/com/tamagotchi/code/feature/settings/SettingsLanguageScreen.kt index 19bf34e..92ab66b 100644 --- a/app/src/main/java/com/tamagotchi/code/feature/settings/SettingsLanguageScreen.kt +++ b/app/src/main/java/com/tamagotchi/code/feature/settings/SettingsLanguageScreen.kt @@ -28,16 +28,7 @@ fun SettingsLanguageScreen( val difficulty by viewModel.difficulty.collectAsStateWithLifecycle() val petState by viewModel.petState.collectAsStateWithLifecycle() - val allTopics = listOf( - stringResource(R.string.topic_kotlin), - stringResource(R.string.topic_javascript), - stringResource(R.string.topic_python), - stringResource(R.string.topic_php), - stringResource(R.string.topic_sql), - stringResource(R.string.topic_git), - stringResource(R.string.topic_clean_code), - stringResource(R.string.topic_data_structures) - ) + val allTopics = com.tamagotchi.code.util.TopicKey.ALL.map { it to com.tamagotchi.code.util.TopicKey.displayRes(it) } val allLanguages = listOf("Kotlin", "JavaScript", "Python", "PHP") @@ -104,16 +95,12 @@ fun SettingsLanguageScreen( Text(stringResource(R.string.settings_lang_difficulty), style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.primary) Spacer(modifier = Modifier.height(8.dp)) Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp)) { - listOf( - stringResource(R.string.settings_lang_diff_beginner), - stringResource(R.string.settings_lang_diff_initial), - stringResource(R.string.settings_lang_diff_intermediate), - stringResource(R.string.settings_lang_diff_mixed) - ).forEach { diffOption -> + com.tamagotchi.code.util.DifficultyKey.ALL.forEach { key -> + val label = stringResource(com.tamagotchi.code.util.DifficultyKey.displayRes(key)) FilterChip( - selected = difficulty == diffOption, - onClick = { viewModel.setDifficulty(diffOption) }, - label = { Text(diffOption) } + selected = difficulty == key, + onClick = { viewModel.setDifficulty(key) }, + label = { Text(label) } ) } } @@ -128,23 +115,24 @@ fun SettingsLanguageScreen( horizontalArrangement = Arrangement.spacedBy(8.dp), verticalArrangement = Arrangement.spacedBy(8.dp) ) { - allTopics.forEach { topic -> + allTopics.forEach { (topicKey, labelRes) -> + val topicLabel = stringResource(labelRes) FilterChip( - selected = selectedTopics.contains(topic), + selected = selectedTopics.contains(topicKey), onClick = { val newSelection = selectedTopics.toMutableSet() - if (newSelection.contains(topic)) { + if (newSelection.contains(topicKey)) { if (newSelection.size > 1) { - newSelection.remove(topic) + newSelection.remove(topicKey) } } else { - newSelection.add(topic) + newSelection.add(topicKey) } viewModel.setTopics(newSelection) }, - label = { Text(topic) }, + label = { Text(topicLabel) }, leadingIcon = { - TopicIcon(topic = topic, modifier = androidx.compose.ui.Modifier.size(18.dp)) + TopicIcon(topic = topicKey, modifier = androidx.compose.ui.Modifier.size(18.dp)) } ) } @@ -159,7 +147,11 @@ fun SettingsLanguageScreen( modifier = Modifier.fillMaxWidth() ) { Text( - text = stringResource(R.string.settings_lang_preview, selectedTopics.size, difficulty), + text = stringResource( + R.string.settings_lang_preview, + selectedTopics.size, + stringResource(com.tamagotchi.code.util.DifficultyKey.displayRes(difficulty)) + ), modifier = Modifier.padding(16.dp), style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant diff --git a/app/src/main/java/com/tamagotchi/code/feature/settings/SettingsScreen.kt b/app/src/main/java/com/tamagotchi/code/feature/settings/SettingsScreen.kt index fcdad4f..f888c75 100644 --- a/app/src/main/java/com/tamagotchi/code/feature/settings/SettingsScreen.kt +++ b/app/src/main/java/com/tamagotchi/code/feature/settings/SettingsScreen.kt @@ -31,6 +31,9 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.tamagotchi.code.R +import com.tamagotchi.code.ui.components.achievementDescription +import com.tamagotchi.code.ui.components.achievementName +import com.tamagotchi.code.ui.components.moodletLabel import com.tamagotchi.code.ui.theme.AppTheme import com.tamagotchi.code.ui.theme.ThemeRegistry import com.tamagotchi.code.ui.viewmodel.PetViewModel @@ -298,14 +301,14 @@ fun SettingsScreen( Spacer(modifier = Modifier.width(8.dp)) Column(modifier = Modifier.weight(1f)) { Text( - text = achievement.name, + text = achievementName(achievement.id, achievement.name), fontWeight = if (isUnlocked) FontWeight.Bold else FontWeight.Normal, fontSize = 14.sp, color = if (isUnlocked) MaterialTheme.colorScheme.onSurface else MaterialTheme.colorScheme.onSurfaceVariant ) Text( - text = achievement.description, + text = achievementDescription(achievement.id, achievement.description), fontSize = 11.sp, color = MaterialTheme.colorScheme.onSurfaceVariant ) @@ -335,6 +338,7 @@ fun SettingsScreen( ) if (moodlet != null) { + val moodletTypeLabel = moodletLabel(moodlet!!.moodletType) Spacer(modifier = Modifier.height(4.dp)) Surface( shape = RoundedCornerShape(6.dp), @@ -342,7 +346,7 @@ fun SettingsScreen( modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp) ) { Text( - text = stringResource(R.string.settings_moodlet, moodlet!!.moodletType), + text = stringResource(R.string.settings_moodlet, moodletTypeLabel), fontSize = 11.sp, color = MaterialTheme.colorScheme.tertiary, modifier = Modifier.padding(8.dp) diff --git a/app/src/main/java/com/tamagotchi/code/feature/skills/SkillTreeScreen.kt b/app/src/main/java/com/tamagotchi/code/feature/skills/SkillTreeScreen.kt index 8fb0230..4efbc63 100644 --- a/app/src/main/java/com/tamagotchi/code/feature/skills/SkillTreeScreen.kt +++ b/app/src/main/java/com/tamagotchi/code/feature/skills/SkillTreeScreen.kt @@ -25,6 +25,8 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.tamagotchi.code.R +import com.tamagotchi.code.ui.components.skillDescription +import com.tamagotchi.code.ui.components.skillName import com.tamagotchi.code.ui.viewmodel.PetViewModel import com.tamagotchi.code.ui.viewmodel.SkillNodeData @@ -86,6 +88,8 @@ fun SkillTreeScreen( } skills.forEach { skill -> + val skillNameText = skillName(skill.id, skill.name) + val skillDescText = skillDescription(skill.id, skill.description) val canAfford = xp >= (skill.currentTier + 1) * 100 val isMaxed = skill.currentTier >= skill.maxTier val progress = skill.currentTier.toFloat() / skill.maxTier @@ -109,7 +113,7 @@ fun SkillTreeScreen( ) { Icon( imageVector = icon, - contentDescription = skill.name, + contentDescription = skillNameText, tint = if (isMaxed) Color(0xFFFFD700) else Color(0xFF81C784), modifier = Modifier.size(24.dp) ) @@ -117,14 +121,14 @@ fun SkillTreeScreen( Column(modifier = Modifier.weight(1f)) { Text( - text = skill.name, + text = skillNameText, fontWeight = FontWeight.Bold, fontFamily = FontFamily.Monospace, fontSize = 12.sp, color = Color.White ) Text( - text = skill.description, + text = skillDescText, fontFamily = FontFamily.Monospace, fontSize = 10.sp, color = Color.Gray diff --git a/app/src/main/java/com/tamagotchi/code/feature/skills/WeeklyMissionsScreen.kt b/app/src/main/java/com/tamagotchi/code/feature/skills/WeeklyMissionsScreen.kt index 52a8931..b5e6e12 100644 --- a/app/src/main/java/com/tamagotchi/code/feature/skills/WeeklyMissionsScreen.kt +++ b/app/src/main/java/com/tamagotchi/code/feature/skills/WeeklyMissionsScreen.kt @@ -19,6 +19,8 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.tamagotchi.code.R +import com.tamagotchi.code.ui.components.weeklyMissionDescription +import com.tamagotchi.code.ui.components.weeklyMissionTitle import com.tamagotchi.code.ui.viewmodel.PetViewModel @Composable @@ -102,14 +104,14 @@ fun WeeklyMissionsScreen( Column(modifier = Modifier.weight(1f)) { Text( - text = mission.title, + text = weeklyMissionTitle(mission.id, mission.title), fontWeight = FontWeight.Bold, fontFamily = FontFamily.Monospace, fontSize = 12.sp, color = if (mission.completed) Color(0xFF81C784) else Color.White ) Text( - text = mission.description, + text = weeklyMissionDescription(mission.id, mission.description), fontFamily = FontFamily.Monospace, fontSize = 10.sp, color = Color.Gray diff --git a/app/src/main/java/com/tamagotchi/code/navigation/AppNavigation.kt b/app/src/main/java/com/tamagotchi/code/navigation/AppNavigation.kt index 0d80f9b..a66489f 100644 --- a/app/src/main/java/com/tamagotchi/code/navigation/AppNavigation.kt +++ b/app/src/main/java/com/tamagotchi/code/navigation/AppNavigation.kt @@ -10,9 +10,11 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import androidx.annotation.StringRes import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavDestination.Companion.hierarchy import androidx.navigation.NavGraph.Companion.findStartDestination @@ -20,6 +22,7 @@ import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.rememberNavController +import com.tamagotchi.code.R import com.tamagotchi.code.feature.focus.FocusScreen import com.tamagotchi.code.feature.home.HomeScreen import com.tamagotchi.code.feature.learn.LearnScreen @@ -51,16 +54,16 @@ object Routes { } data class BottomNavItem( - val label: String, + @StringRes val labelRes: Int, val icon: ImageVector, val route: String ) val bottomNavItems = listOf( - BottomNavItem("Inicio", Icons.Default.Home, Routes.HOME), - BottomNavItem("Aprender", Icons.Default.Code, Routes.LEARN), - BottomNavItem("Estudio", Icons.Default.Timer, Routes.FOCUS), - BottomNavItem("Tienda", Icons.Default.ShoppingBag, Routes.SHOP) + BottomNavItem(R.string.nav_home, Icons.Default.Home, Routes.HOME), + BottomNavItem(R.string.nav_learn, Icons.Default.Code, Routes.LEARN), + BottomNavItem(R.string.nav_focus, Icons.Default.Timer, Routes.FOCUS), + BottomNavItem(R.string.nav_shop, Icons.Default.ShoppingBag, Routes.SHOP) ) @OptIn(ExperimentalMaterial3Api::class) @@ -96,12 +99,12 @@ fun AppNavigation(viewModel: PetViewModel) { Row(verticalAlignment = Alignment.CenterVertically) { Icon( imageVector = Icons.Default.DeveloperMode, - contentDescription = "Code Tamagotchi Icon", + contentDescription = stringResource(R.string.cd_app_icon), tint = MaterialTheme.colorScheme.primary, modifier = Modifier.padding(end = 8.dp) ) Text( - text = "Code Tamagotchi", + text = stringResource(R.string.app_name), fontWeight = FontWeight.Bold, style = MaterialTheme.typography.titleMedium, fontSize = 20.sp @@ -118,7 +121,7 @@ fun AppNavigation(viewModel: PetViewModel) { ) { Icon( imageVector = Icons.Default.Settings, - contentDescription = "Configurar Mascota", + contentDescription = stringResource(R.string.cd_open_settings), tint = MaterialTheme.colorScheme.onSurfaceVariant ) } @@ -143,8 +146,8 @@ fun AppNavigation(viewModel: PetViewModel) { restoreState = true } }, - icon = { Icon(item.icon, contentDescription = item.label) }, - label = { Text(item.label, style = MaterialTheme.typography.labelSmall) } + icon = { Icon(item.icon, contentDescription = stringResource(item.labelRes)) }, + label = { Text(stringResource(item.labelRes), style = MaterialTheme.typography.labelSmall) } ) } } diff --git a/app/src/main/java/com/tamagotchi/code/ui/components/CodeCardDialog.kt b/app/src/main/java/com/tamagotchi/code/ui/components/CodeCardDialog.kt index 1b7e244..11392ac 100644 --- a/app/src/main/java/com/tamagotchi/code/ui/components/CodeCardDialog.kt +++ b/app/src/main/java/com/tamagotchi/code/ui/components/CodeCardDialog.kt @@ -17,11 +17,13 @@ import androidx.compose.ui.draw.rotate import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontFamily 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 com.tamagotchi.code.R import com.tamagotchi.code.data.CodeCard import com.tamagotchi.code.data.CardType @@ -59,11 +61,13 @@ fun CodeCardDialog( ) Spacer(modifier = Modifier.width(8.dp)) Text( - text = when (card.type) { - CardType.FACT -> "Dato Curioso" - CardType.JOKE -> "Chiste de Programador" - CardType.TIP -> "Tip de Codigo" - }, + text = stringResource( + when (card.type) { + CardType.FACT -> R.string.cc_title_fact + CardType.JOKE -> R.string.cc_title_joke + CardType.TIP -> R.string.cc_title_tip + } + ), fontFamily = FontFamily.Monospace, fontWeight = FontWeight.Bold, fontSize = 14.sp @@ -130,7 +134,7 @@ fun CodeCardDialog( modifier = Modifier.fillMaxWidth(), shape = RoundedCornerShape(8.dp) ) { - Text("Otra!", fontFamily = FontFamily.Monospace, fontWeight = FontWeight.Bold) + Text(stringResource(R.string.cc_another), fontFamily = FontFamily.Monospace, fontWeight = FontWeight.Bold) } } ) diff --git a/app/src/main/java/com/tamagotchi/code/ui/components/CodePetWidget.kt b/app/src/main/java/com/tamagotchi/code/ui/components/CodePetWidget.kt index ecbc4d9..e10dc6f 100644 --- a/app/src/main/java/com/tamagotchi/code/ui/components/CodePetWidget.kt +++ b/app/src/main/java/com/tamagotchi/code/ui/components/CodePetWidget.kt @@ -16,6 +16,7 @@ import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp @@ -65,13 +66,13 @@ fun CodePetWidget( ) { Column { Text( - text = petState?.name ?: "Codey", + text = petState?.name ?: stringResource(R.string.default_pet_name), color = Color.White, fontWeight = FontWeight.ExtraBold, fontSize = 18.sp ) Text( - text = "Nivel ${petState?.level ?: 1}", + text = stringResource(R.string.season_level_short, petState?.level ?: 1), color = Color(0xFF7C8CFB), fontWeight = FontWeight.Bold, fontSize = 12.sp @@ -105,7 +106,7 @@ fun CodePetWidget( ) { Image( painter = painterResource(id = petImageRes), - contentDescription = "Mascota Code Pet", + contentDescription = stringResource(R.string.cd_widget_pet), modifier = Modifier.size(100.dp), contentScale = ContentScale.Fit ) @@ -114,9 +115,9 @@ fun CodePetWidget( Spacer(modifier = Modifier.height(16.dp)) // Stats bars - StatBar(label = "Energía", value = (petState?.energy ?: 100f) / 100f, color = Color(0xFF4CAF50)) + StatBar(label = stringResource(R.string.meter_energy), value = (petState?.energy ?: 100f) / 100f, color = Color(0xFF4CAF50)) Spacer(modifier = Modifier.height(8.dp)) - StatBar(label = "Hambre", value = (petState?.hunger ?: 100f) / 100f, color = Color(0xFFFF9800)) + StatBar(label = stringResource(R.string.widget_hunger), value = (petState?.hunger ?: 100f) / 100f, color = Color(0xFFFF9800)) } } } diff --git a/app/src/main/java/com/tamagotchi/code/ui/components/DailyRewardDialog.kt b/app/src/main/java/com/tamagotchi/code/ui/components/DailyRewardDialog.kt index 0fe1c2a..adaac89 100644 --- a/app/src/main/java/com/tamagotchi/code/ui/components/DailyRewardDialog.kt +++ b/app/src/main/java/com/tamagotchi/code/ui/components/DailyRewardDialog.kt @@ -12,6 +12,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign @@ -19,6 +20,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties +import com.tamagotchi.code.R import com.tamagotchi.code.ui.viewmodel.PetViewModel import com.tamagotchi.code.ui.viewmodel.DailyReward import com.tamagotchi.code.ui.viewmodel.dailyRewardsList @@ -118,7 +120,7 @@ fun DailyRewardDialog( Spacer(modifier = Modifier.height(12.dp)) Text( - text = if (isClaimedToday) "¡Recompensa Reclamada!" else "Recompensas Diarias", + text = stringResource(if (isClaimedToday) R.string.dr_claimed_title else R.string.dr_dialog_title), fontFamily = FontFamily.Monospace, fontWeight = FontWeight.Bold, fontSize = 18.sp, @@ -126,8 +128,7 @@ fun DailyRewardDialog( ) Spacer(modifier = Modifier.height(4.dp)) Text( - text = if (isClaimedToday) "Vuelve mañana por más" - else "¡Vuelve cada día por más!", + text = stringResource(if (isClaimedToday) R.string.dr_claimed_sub else R.string.dr_open_sub), fontFamily = FontFamily.Monospace, fontSize = 12.sp, color = Color.White.copy(alpha = 0.5f) @@ -200,8 +201,7 @@ fun DailyRewardDialog( ) ) { Text( - text = if (isClaimedToday) "CERRAR" - else "RECLAMAR RECOMPENSA", + text = stringResource(if (isClaimedToday) R.string.dr_close_btn else R.string.dr_claim_btn), fontFamily = FontFamily.Monospace, fontWeight = FontWeight.Bold, fontSize = 14.sp @@ -249,7 +249,7 @@ private fun RewardCard( horizontalAlignment = Alignment.CenterHorizontally ) { Text( - text = "DÍA ${reward.day}", + text = stringResource(R.string.dr_day, reward.day), fontFamily = FontFamily.Monospace, fontWeight = FontWeight.Bold, fontSize = 11.sp, @@ -330,14 +330,14 @@ private fun MegaPackCard( Column(modifier = Modifier.weight(1f)) { Text( - text = "DÍA 7 — ¡MEGA PACK!", + text = stringResource(R.string.dr_day7_mega), fontFamily = FontFamily.Monospace, fontWeight = FontWeight.Bold, fontSize = 14.sp, color = accentColor.copy(alpha = alpha) ) Text( - text = reward.title, + text = stringResource(reward.titleRes), fontFamily = FontFamily.Monospace, fontSize = 12.sp, color = Color.White.copy(alpha = alpha * 0.7f) @@ -384,7 +384,7 @@ private fun MegaPackCard( ) Spacer(modifier = Modifier.width(4.dp)) Text( - text = "+${reward.healthRestore.toInt()} Salud", + text = stringResource(R.string.dr_health_amount, reward.healthRestore.toInt()), fontSize = 10.sp, fontFamily = FontFamily.Monospace, color = Color.White.copy(alpha = alpha * 0.6f) @@ -399,7 +399,7 @@ private fun MegaPackCard( ) Spacer(modifier = Modifier.width(4.dp)) Text( - text = "+${reward.energyRestore.toInt()} Energía", + text = stringResource(R.string.dr_energy_amount, reward.energyRestore.toInt()), fontSize = 10.sp, fontFamily = FontFamily.Monospace, color = Color.White.copy(alpha = alpha * 0.6f) diff --git a/app/src/main/java/com/tamagotchi/code/ui/components/LocalizedGameText.kt b/app/src/main/java/com/tamagotchi/code/ui/components/LocalizedGameText.kt new file mode 100644 index 0000000..a6322b8 --- /dev/null +++ b/app/src/main/java/com/tamagotchi/code/ui/components/LocalizedGameText.kt @@ -0,0 +1,131 @@ +package com.tamagotchi.code.ui.components + +import androidx.annotation.StringRes +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import com.tamagotchi.code.R + +@StringRes +fun achievementNameRes(id: String): Int? = when (id) { + "primer_build" -> R.string.ach_primer_build_name + "nivel_experto" -> R.string.ach_nivel_experto_name + "cazador_de_bugs" -> R.string.ach_cazador_de_bugs_name + "git_sin_panico" -> R.string.ach_git_sin_panico_name + "racha_7" -> R.string.ach_racha_7_name + "racha_30" -> R.string.ach_racha_30_name + "coleccionista" -> R.string.ach_coleccionista_name + "completista" -> R.string.ach_completista_name + "ahorrador" -> R.string.ach_ahorrador_name + "primer_acaricie" -> R.string.ach_primer_acaricie_name + "duermevela" -> R.string.ach_duermevela_name + "limpiador" -> R.string.ach_limpiador_name + else -> null +} + +@StringRes +fun achievementDescRes(id: String): Int? = when (id) { + "primer_build" -> R.string.ach_primer_build_desc + "nivel_experto" -> R.string.ach_nivel_experto_desc + "cazador_de_bugs" -> R.string.ach_cazador_de_bugs_desc + "git_sin_panico" -> R.string.ach_git_sin_panico_desc + "racha_7" -> R.string.ach_racha_7_desc + "racha_30" -> R.string.ach_racha_30_desc + "coleccionista" -> R.string.ach_coleccionista_desc + "completista" -> R.string.ach_completista_desc + "ahorrador" -> R.string.ach_ahorrador_desc + "primer_acaricie" -> R.string.ach_primer_acaricie_desc + "duermevela" -> R.string.ach_duermevela_desc + "limpiador" -> R.string.ach_limpiador_desc + else -> null +} + +@Composable +fun achievementName(id: String, fallback: String): String { + val res = achievementNameRes(id) ?: return fallback + return stringResource(res) +} + +@Composable +fun achievementDescription(id: String, fallback: String): String { + val res = achievementDescRes(id) ?: return fallback + return stringResource(res) +} + +@StringRes +fun skillNameRes(id: String): Int? = when (id) { + "double_xp" -> R.string.skill_double_xp_name + "slow_decay" -> R.string.skill_slow_decay_name + "shop_discount" -> R.string.skill_shop_discount_name + "minigame_bonus" -> R.string.skill_minigame_bonus_name + "offline_xp" -> R.string.skill_offline_xp_name + "extra_heart" -> R.string.skill_extra_heart_name + else -> null +} + +@StringRes +fun skillDescRes(id: String): Int? = when (id) { + "double_xp" -> R.string.skill_double_xp_desc + "slow_decay" -> R.string.skill_slow_decay_desc + "shop_discount" -> R.string.skill_shop_discount_desc + "minigame_bonus" -> R.string.skill_minigame_bonus_desc + "offline_xp" -> R.string.skill_offline_xp_desc + "extra_heart" -> R.string.skill_extra_heart_desc + else -> null +} + +@Composable +fun skillName(id: String, fallback: String): String { + val res = skillNameRes(id) ?: return fallback + return stringResource(res) +} + +@Composable +fun skillDescription(id: String, fallback: String): String { + val res = skillDescRes(id) ?: return fallback + return stringResource(res) +} + +@StringRes +fun weeklyMissionTitleRes(id: String): Int? = when (id) { + "wm1" -> R.string.wm_wm1_title + "wm2" -> R.string.wm_wm2_title + "wm3" -> R.string.wm_wm3_title + else -> null +} + +@StringRes +fun weeklyMissionDescRes(id: String): Int? = when (id) { + "wm1" -> R.string.wm_wm1_desc + "wm2" -> R.string.wm_wm2_desc + "wm3" -> R.string.wm_wm3_desc + else -> null +} + +@Composable +fun weeklyMissionTitle(id: String, fallback: String): String { + val res = weeklyMissionTitleRes(id) ?: return fallback + return stringResource(res) +} + +@Composable +fun weeklyMissionDescription(id: String, fallback: String): String { + val res = weeklyMissionDescRes(id) ?: return fallback + return stringResource(res) +} + +@StringRes +fun moodletLabelRes(type: String): Int? = when (type) { + "bug_prod" -> R.string.moodlet_bug_prod + "tabs" -> R.string.moodlet_tabs + "spaces" -> R.string.moodlet_spaces + "bad_commit" -> R.string.moodlet_bad_commit + "clean_code" -> R.string.moodlet_clean_code + "spilled_coffee" -> R.string.moodlet_spilled_coffee + else -> null +} + +@Composable +fun moodletLabel(type: String): String { + val res = moodletLabelRes(type) ?: return type + return stringResource(res) +} diff --git a/app/src/main/java/com/tamagotchi/code/ui/components/PairBuddy.kt b/app/src/main/java/com/tamagotchi/code/ui/components/PairBuddy.kt index b0adad6..129c83c 100644 --- a/app/src/main/java/com/tamagotchi/code/ui/components/PairBuddy.kt +++ b/app/src/main/java/com/tamagotchi/code/ui/components/PairBuddy.kt @@ -11,10 +11,13 @@ import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.pluralStringResource +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import com.tamagotchi.code.R import com.tamagotchi.code.ui.viewmodel.PairBuddyState @Composable @@ -40,21 +43,26 @@ fun PairBuddyOverlay( ) { Icon( imageVector = Icons.Default.BugReport, - contentDescription = "Buggy", + contentDescription = buddy.name, tint = Color(0xFFFFB74D), modifier = Modifier.size(24.dp) ) Spacer(modifier = Modifier.width(8.dp)) Column(modifier = Modifier.weight(1f)) { Text( - text = "${buddy.name} está aquí!", + text = stringResource(R.string.pair_buddy_here, buddy.name), fontFamily = FontFamily.Monospace, fontWeight = FontWeight.Bold, fontSize = 11.sp, color = Color(0xFFFFB74D) ) Text( - text = "XP x${buddy.xpMultiplier} por ${buddy.remainingChallenges} reto(s) más", + text = pluralStringResource( + R.plurals.pair_buddy_xp, + buddy.remainingChallenges, + buddy.remainingChallenges, + buddy.xpMultiplier + ), fontFamily = FontFamily.Monospace, fontSize = 10.sp, color = Color.LightGray diff --git a/app/src/main/java/com/tamagotchi/code/ui/components/ViewportCard.kt b/app/src/main/java/com/tamagotchi/code/ui/components/ViewportCard.kt index 67e9ef6..62e2f94 100644 --- a/app/src/main/java/com/tamagotchi/code/ui/components/ViewportCard.kt +++ b/app/src/main/java/com/tamagotchi/code/ui/components/ViewportCard.kt @@ -18,6 +18,9 @@ import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.pluralStringResource +import androidx.compose.ui.res.stringArrayResource +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp @@ -74,69 +77,18 @@ fun ViewportCard( else -> MaterialTheme.colorScheme.secondary } - val randomQuote = remember(state.currentStatus, state.xp) { - val quotes = when (state.currentStatus) { - "SLEEPING" -> listOf( - "Zzz... if (dream) { sleep() } else { repeat() }... Zzz", - "Cargando baterías... no interrumpas mi hilo principal.", - "Soñando con compiladores veloces y cero NullPointers...", - "Mi CPU está en modo ahorro. Vuelve en un ciclo de reloj.", - "Zzz... ¿viste ese commit? Fue... legendario..." - ) - "STUDYING" -> listOf( - "¡Shhh! Estoy optimizando algoritmos en mi cerebro.", - "Compilando... codeando a 1000 WPM.", - "Siento cómo se incrementa mi sinapsis neuronal binaria.", - "¿Sabías que el primer bug fue una polilla real? Yo prefiero los digitales.", - "Mi código es arte. Tu código... bueno, funciona.", - "Concentración total. No me hagas un force push ahora." - ) - "SICK" -> listOf( - "Error 500: Necesito desbuguear urgente. ¡Dame una píldora!", - "Demasiados bugs acumulados en mi stack... me siento mal.", - "Siento mi CPU sobrecalentada. ¿Podemos repasar un poco?", - "Mi recolector de basura no está funcionando. Me siento... sucio.", - "¿Me formateas? No, mejor dame cariño, es menos traumático." - ) - "SAD" -> listOf( - "Tengo flojera... me siento un poco depre.", - "Mi batería de motivación está por debajo del 20%.", - "¿Procrastinando otra vez? Mi código se llena de advertencias.", - "Siento que mi arquitectura se desmorona. Necesito un refactor emocional.", - "Ni siquiera un 'Hello World' me anima hoy." - ) - "HUNGRY" -> listOf( - "¡NullPointerException en mi estómago! Necesito bytes.", - "Mi caché de energía está vacía, ¿me das de comer?", - "Sin comida, mi rendimiento cae a O(n^2).", - "Mi estómago está haciendo un loop infinito de ruidos.", - "Aliméntame o empezaré a borrar tus archivos temporales. Es broma... ¿o no?" - ) - "EXCITED" -> listOf( - "¡Wiii! ¡Mi código es O(1) y mi corazón también!", - "¡Nivel de felicidad al MÁXIMO! Gracias por quererme.", - "¡Siento que podría compilar el kernel de Linux en 1 segundo!", - "¡Soy el root de tu corazón! ¡Wiiiii!", - "¡Todo compila a la primera! ¡Esto es magia negra!" - ) - "DEAD" -> listOf( - "R.I.P. Fue procesado por un segfault...", - "Aquí yace una mascota. Su código quedó sin mergear.", - "404: vida no encontrada. Revídame para continuar.", - "from grave import revive # ¿todavía estás a tiempo?" - ) - else -> listOf( - "¡Compilar sin advertencias es mi pasión!", - "¿Listo para tirar unas líneas de código limpias hoy?", - "¡Siento el poder de un refactor exitoso!", - "Me agradas, haces que mi arquitectura sea modular y sólida.", - "¿Has probado a apagarlo y volverlo a encender? A mí me funciona.", - "Tu código es tan limpio que puedo ver mi reflejo en él.", - "Oye, ¿has visto mis logs? Están llenos de amor por ti." - ) - } - quotes.random() + val quoteArrayRes = when (state.currentStatus) { + "SLEEPING" -> R.array.quote_sleeping + "STUDYING" -> R.array.quote_studying + "SICK" -> R.array.quote_sick + "SAD" -> R.array.quote_sad + "HUNGRY" -> R.array.quote_hungry + "EXCITED" -> R.array.quote_excited + "DEAD" -> R.array.quote_dead + else -> R.array.quote_happy } + val statusQuotes = stringArrayResource(quoteArrayRes) + val randomQuote = remember(quoteArrayRes, state.xp) { statusQuotes.random() } Card( shape = cardShape, @@ -170,7 +122,7 @@ fun ViewportCard( Spacer(modifier = Modifier.width(6.dp)) Icon( imageVector = Icons.Default.Edit, - contentDescription = "Renombrar", + contentDescription = stringResource(R.string.cd_rename), tint = statusColor, modifier = Modifier .size(16.dp) @@ -178,7 +130,7 @@ fun ViewportCard( ) } Text( - text = "Especialista: ${state.language}", + text = stringResource(R.string.viewport_specialist, state.language), fontSize = 12.sp, style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant @@ -193,7 +145,7 @@ fun ViewportCard( val isFilled = i <= currentHearts Icon( imageVector = if (isFilled) Icons.Default.Favorite else Icons.Default.FavoriteBorder, - contentDescription = "Corazón $i", + contentDescription = stringResource(R.string.cd_heart, i), tint = if (isFilled) MaterialTheme.colorScheme.error else Color.Gray, modifier = Modifier.size(16.dp) ) @@ -208,7 +160,7 @@ fun ViewportCard( modifier = Modifier.testTag("level_badge") ) { Text( - text = "LVL ${state.level}", + text = stringResource(R.string.viewport_level, state.level), fontWeight = FontWeight.Bold, style = MaterialTheme.typography.labelMedium, fontSize = 13.sp, @@ -278,7 +230,7 @@ fun ViewportCard( if (showHeart) { Icon( imageVector = Icons.Default.Favorite, - contentDescription = "Amor", + contentDescription = stringResource(R.string.cd_love), tint = MaterialTheme.colorScheme.error, modifier = Modifier .align(Alignment.Center) @@ -305,7 +257,7 @@ fun ViewportCard( ) { Icon( imageVector = Icons.Default.ChatBubbleOutline, - contentDescription = "Mensaje", + contentDescription = stringResource(R.string.cd_message), tint = statusColor, modifier = Modifier.size(20.dp) ) @@ -332,7 +284,7 @@ fun ViewportCard( horizontalArrangement = Arrangement.SpaceBetween ) { Text( - text = "XP: ${state.xp} / $currentLevelRequiredXp", + text = stringResource(R.string.viewport_xp_progress, state.xp, currentLevelRequiredXp), fontSize = 11.sp, style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant @@ -371,19 +323,19 @@ fun ViewportCard( horizontalArrangement = Arrangement.spacedBy(8.dp) ) { MeterItem( - label = "Vida", value = state.health, + label = stringResource(R.string.meter_health), value = state.health, icon = Icons.Default.Favorite, activeColor = MaterialTheme.colorScheme.error, trackColor = MaterialTheme.colorScheme.error.copy(alpha = 0.2f), modifier = Modifier.weight(1f).testTag("health_bar") ) MeterItem( - label = "Alimento", value = state.hunger, + label = stringResource(R.string.meter_food), value = state.hunger, icon = Icons.Default.Restaurant, activeColor = appTheme.accent, trackColor = appTheme.accent.copy(alpha = 0.2f), modifier = Modifier.weight(1f).testTag("hunger_bar") ) MeterItem( - label = "Energía", value = state.energy, + label = stringResource(R.string.meter_energy), value = state.energy, icon = Icons.Default.FlashOn, activeColor = MaterialTheme.colorScheme.tertiary, trackColor = MaterialTheme.colorScheme.tertiary.copy(alpha = 0.2f), modifier = Modifier.weight(1f).testTag("energy_bar") @@ -411,7 +363,7 @@ fun ViewportCard( Spacer(modifier = Modifier.width(16.dp)) Icon(Icons.Default.LocalFireDepartment, contentDescription = null, tint = MaterialTheme.colorScheme.error, modifier = Modifier.size(16.dp)) Spacer(modifier = Modifier.width(4.dp)) - Text("${state.streak} días", fontWeight = FontWeight.Bold, style = MaterialTheme.typography.labelMedium, fontSize = 13.sp, color = MaterialTheme.colorScheme.error) + Text(pluralStringResource(R.plurals.streak_days, state.streak, state.streak), fontWeight = FontWeight.Bold, style = MaterialTheme.typography.labelMedium, fontSize = 13.sp, color = MaterialTheme.colorScheme.error) } Button( onClick = { viewModel.toggleSleep() }, @@ -425,12 +377,12 @@ fun ViewportCard( ) { Icon( imageVector = if (state.currentStatus == "SLEEPING") Icons.Default.WbSunny else Icons.Default.NightsStay, - contentDescription = "Dormir/Despertar", + contentDescription = stringResource(R.string.cd_sleep_toggle), modifier = Modifier.size(14.dp) ) Spacer(modifier = Modifier.width(4.dp)) Text( - if (state.currentStatus == "SLEEPING") "Despertar" else "Dormir", + stringResource(if (state.currentStatus == "SLEEPING") R.string.viewport_wake else R.string.viewport_sleep), fontSize = 10.sp, fontWeight = FontWeight.Bold, style = MaterialTheme.typography.labelSmall ) } @@ -451,9 +403,9 @@ fun ViewportCard( contentPadding = PaddingValues(horizontal = 6.dp, vertical = 4.dp), modifier = Modifier.weight(1f).height(34.dp) ) { - Icon(Icons.Default.Pets, contentDescription = "Acariciar", modifier = Modifier.size(14.dp)) + Icon(Icons.Default.Pets, contentDescription = stringResource(R.string.action_pet), modifier = Modifier.size(14.dp)) Spacer(modifier = Modifier.width(3.dp)) - Text("Acariciar", fontSize = 10.sp, maxLines = 1, overflow = TextOverflow.Ellipsis, style = MaterialTheme.typography.labelSmall) + Text(stringResource(R.string.action_pet), fontSize = 10.sp, maxLines = 1, overflow = TextOverflow.Ellipsis, style = MaterialTheme.typography.labelSmall) } Button( onClick = { viewModel.cleanThePet(); viewModel.soundManager.playClick() }, @@ -462,9 +414,9 @@ fun ViewportCard( contentPadding = PaddingValues(horizontal = 6.dp, vertical = 4.dp), modifier = Modifier.weight(1f).height(34.dp) ) { - Icon(Icons.Default.CleaningServices, contentDescription = "Limpiar", modifier = Modifier.size(14.dp)) + Icon(Icons.Default.CleaningServices, contentDescription = stringResource(R.string.action_clean), modifier = Modifier.size(14.dp)) Spacer(modifier = Modifier.width(3.dp)) - Text("Limpiar", fontSize = 10.sp, maxLines = 1, overflow = TextOverflow.Ellipsis, style = MaterialTheme.typography.labelSmall) + Text(stringResource(R.string.action_clean), fontSize = 10.sp, maxLines = 1, overflow = TextOverflow.Ellipsis, style = MaterialTheme.typography.labelSmall) } Button( onClick = onPlayClick, @@ -473,9 +425,9 @@ fun ViewportCard( contentPadding = PaddingValues(horizontal = 6.dp, vertical = 4.dp), modifier = Modifier.weight(1f).height(34.dp) ) { - Icon(Icons.Default.SportsEsports, contentDescription = "Jugar", modifier = Modifier.size(14.dp)) + Icon(Icons.Default.SportsEsports, contentDescription = stringResource(R.string.action_play), modifier = Modifier.size(14.dp)) Spacer(modifier = Modifier.width(3.dp)) - Text("Jugar", fontWeight = FontWeight.Bold, fontSize = 10.sp, maxLines = 1, overflow = TextOverflow.Ellipsis, style = MaterialTheme.typography.labelSmall) + Text(stringResource(R.string.action_play), fontWeight = FontWeight.Bold, fontSize = 10.sp, maxLines = 1, overflow = TextOverflow.Ellipsis, style = MaterialTheme.typography.labelSmall) } } } diff --git a/app/src/main/java/com/tamagotchi/code/ui/viewmodel/PetViewModel.kt b/app/src/main/java/com/tamagotchi/code/ui/viewmodel/PetViewModel.kt index f145c9d..3d4b64e 100644 --- a/app/src/main/java/com/tamagotchi/code/ui/viewmodel/PetViewModel.kt +++ b/app/src/main/java/com/tamagotchi/code/ui/viewmodel/PetViewModel.kt @@ -8,6 +8,7 @@ import com.tamagotchi.code.data.CodeCard import com.tamagotchi.code.data.CodingChallenge import com.tamagotchi.code.data.QuestType import com.tamagotchi.code.data.codeCards +import com.tamagotchi.code.data.localized import com.tamagotchi.code.data.getTargetForQuest import com.tamagotchi.code.data.database.FocusSessionEntity import com.tamagotchi.code.data.database.PetStateEntity @@ -35,6 +36,8 @@ import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import android.content.Context import android.app.NotificationManager +import androidx.annotation.StringRes +import com.tamagotchi.code.R class PetViewModel( private val repository: PetRepository, @@ -84,7 +87,7 @@ class PetViewModel( } val selectedTopics = MutableStateFlow>(emptySet()) - val defaultInitialTopics = setOf("Kotlin", "Estructuras de Datos", "Git") + val defaultInitialTopics = setOf("Kotlin", "Data Structures", "Git") fun setTopics(topics: Set) { viewModelScope.launch { @@ -115,7 +118,7 @@ class PetViewModel( } } - val difficulty = MutableStateFlow("Inicial") + val difficulty = MutableStateFlow(com.tamagotchi.code.util.DifficultyKey.INITIAL) val focusDurationDefault = MutableStateFlow(25) val soundEnabled = MutableStateFlow(true) val vibrationEnabled = MutableStateFlow(true) @@ -133,10 +136,9 @@ class PetViewModel( viewModelScope.launch { userPreferences.setDifficulty(newDifficulty) difficulty.value = newDifficulty - + // Lógica para dificultad principiante - if (newDifficulty.contains("Principiante", ignoreCase = true) || - newDifficulty.contains("experiencia", ignoreCase = true)) { + if (newDifficulty == com.tamagotchi.code.util.DifficultyKey.BEGINNER) { // Podríamos cargar un set de retos ultra-básicos aquí } } @@ -188,7 +190,7 @@ class PetViewModel( repository.savePetState(defaultPet) userPreferences.setSelectedTopics(defaultInitialTopics) hasSeenOnboarding.value = false - userPreferences.setDifficulty("Inicial") + userPreferences.setDifficulty(com.tamagotchi.code.util.DifficultyKey.INITIAL) userPreferences.setFocusDurationDefault(25) } } @@ -498,7 +500,7 @@ class PetViewModel( fun loadChallengesForLanguage(language: String) { _selectedChallengeLanguage.value = language val filtered = ChallengesData.challenges.filter { it.language.equals(language, ignoreCase = true) } - _activeChallenges.value = filtered.shuffled().take(3) + _activeChallenges.value = filtered.map { it.localized() }.shuffled().take(3) _currentChallengeIndex.value = 0 _challengeFeedback.value = null } @@ -918,8 +920,6 @@ class PetViewModel( private set var showQuestCompletedDialog = mutableStateOf(false) private set - var questCompletedMessage = mutableStateOf("") - private set fun dismissDeathDialog() { showDeathDialog.value = false @@ -957,7 +957,7 @@ class PetViewModel( } val card = pool.random() userPreferences.markCardShown(card.id) - currentCodeCard.value = card + currentCodeCard.value = card.localized() showCodeCardDialog.value = true } } @@ -1011,7 +1011,6 @@ class PetViewModel( lastUpdated = System.currentTimeMillis() ) repository.savePetState(updated) - questCompletedMessage.value = "Mision completada!\n\n${currentType.displayName}\n\n+$rewardXp XP\n+$rewardBytes Bytes" showQuestCompletedDialog.value = true soundManager.playLevelUp() triggerCelebration() @@ -1157,15 +1156,8 @@ class PetViewModel( fun triggerRandomMoodlet() { viewModelScope.launch { - val events = listOf( - "bug_prod" to "Encontró un bug en producción 😱", - "tabs" to "Te vio usar tabs 😊", - "spaces" to "Te vio usar espacios 🤔", - "bad_commit" to "Commit sin descripción 😞", - "clean_code" to "Código limpio detectado 🎉", - "spilled_coffee" to "Café derramado 😰" - ) - val (type, message) = events.random() + val eventTypes = listOf("bug_prod", "tabs", "spaces", "bad_commit", "clean_code", "spilled_coffee") + val type = eventTypes.random() val duration = when (type) { "bug_prod" -> 2L "spilled_coffee" -> 1L @@ -1175,7 +1167,7 @@ class PetViewModel( } val expiry = System.currentTimeMillis() + duration * 60 * 60 * 1000 userPreferences.saveMoodlet(type, expiry) - moodletState.value = MoodletState(message, expiry) + moodletState.value = MoodletState(type, expiry) } } @@ -1422,10 +1414,11 @@ data class WeeklyMissionData( val completed: Boolean = false ) { companion object { + // Title/description are resolved by id at display time (see LocalizedGameText.kt) fun generateWeeklyMissions(): List = listOf( - WeeklyMissionData("wm1", "5 retos de código", "Completa 5 retos en Aprender", 100, 50), - WeeklyMissionData("wm2", "2h modo foco", "Estudia 2 horas en modo foco", 200, 100), - WeeklyMissionData("wm3", "Gana 3 Bug Hunt", "Gana 3 partidas de Bug Hunt", 150, 75) + WeeklyMissionData("wm1", "", "", 100, 50), + WeeklyMissionData("wm2", "", "", 200, 100), + WeeklyMissionData("wm3", "", "", 150, 75) ) } } @@ -1447,13 +1440,14 @@ data class SkillNodeData( ) object SkillTreeData { + // name/description are resolved by id at display time (see LocalizedGameText.kt) val DEFAULT_SKILLS = listOf( - SkillNodeData("double_xp", "Doble XP domingo", "XP x1.5/2/3 los domingos"), - SkillNodeData("slow_decay", "Decaimiento lento", "Decaimiento -10%/-20%/-30%"), - SkillNodeData("shop_discount", "Descuento tienda", "5%/10%/15% descuento"), - SkillNodeData("minigame_bonus", "Bonus minijuegos", "+10%/+20%/+30% recompensa"), - SkillNodeData("offline_xp", "XP offline", "1h/2h/4h de XP pasivo"), - SkillNodeData("extra_heart", "Corazón extra", "Máximo 6 corazones") + SkillNodeData("double_xp", "", ""), + SkillNodeData("slow_decay", "", ""), + SkillNodeData("shop_discount", "", ""), + SkillNodeData("minigame_bonus", "", ""), + SkillNodeData("offline_xp", "", ""), + SkillNodeData("extra_heart", "", "") ) } @@ -1463,15 +1457,15 @@ data class DailyReward( val xp: Int, val healthRestore: Float = 0f, val energyRestore: Float = 0f, - val title: String + @StringRes val titleRes: Int ) val dailyRewardsList = listOf( - DailyReward(1, 50, 15, title = "Hola, Mundo!"), - DailyReward(2, 100, 25, title = "Variables Inicializadas"), - DailyReward(3, 150, 35, energyRestore = 15f, title = "Café Double Shot"), - DailyReward(4, 200, 45, title = "Bucle Optimizado"), - DailyReward(5, 250, 55, healthRestore = 15f, title = "Bug Solucionado"), - DailyReward(6, 350, 70, title = "Compilación Limpia"), - DailyReward(7, 500, 100, healthRestore = 25f, energyRestore = 25f, title = "Despliegue Exitoso (PROD)") + DailyReward(1, 50, 15, titleRes = R.string.dr_1_title), + DailyReward(2, 100, 25, titleRes = R.string.dr_2_title), + DailyReward(3, 150, 35, energyRestore = 15f, titleRes = R.string.dr_3_title), + DailyReward(4, 200, 45, titleRes = R.string.dr_4_title), + DailyReward(5, 250, 55, healthRestore = 15f, titleRes = R.string.dr_5_title), + DailyReward(6, 350, 70, titleRes = R.string.dr_6_title), + DailyReward(7, 500, 100, healthRestore = 25f, energyRestore = 25f, titleRes = R.string.dr_7_title) ) diff --git a/app/src/main/java/com/tamagotchi/code/util/CommitWorker.kt b/app/src/main/java/com/tamagotchi/code/util/CommitWorker.kt index 9b9d08d..4e381e2 100644 --- a/app/src/main/java/com/tamagotchi/code/util/CommitWorker.kt +++ b/app/src/main/java/com/tamagotchi/code/util/CommitWorker.kt @@ -29,7 +29,7 @@ class CommitWorker( val prefs = get(UserPreferencesRepository::class.java) val activities = prefs.getDailyActivityLog() - val commit = DailyCommitGenerator.generateCommit(activities, petState.name) + val commit = DailyCommitGenerator.generateCommit(applicationContext, activities, petState.name) prefs.setPendingCommit(commit) prefs.clearDailyActivityLog() @@ -51,7 +51,7 @@ class CommitWorker( "pet_care_reminder" ) .setSmallIcon(R.drawable.ic_launcher_foreground) - .setContentTitle("${petState.name} hizo commit") + .setContentTitle(applicationContext.getString(R.string.notif_commit_title, petState.name)) .setContentText(commit) .setPriority(NotificationCompat.PRIORITY_DEFAULT) .setContentIntent(pendingIntent) diff --git a/app/src/main/java/com/tamagotchi/code/util/ContentKeys.kt b/app/src/main/java/com/tamagotchi/code/util/ContentKeys.kt new file mode 100644 index 0000000..2a3ac77 --- /dev/null +++ b/app/src/main/java/com/tamagotchi/code/util/ContentKeys.kt @@ -0,0 +1,68 @@ +package com.tamagotchi.code.util + +import com.tamagotchi.code.R + +// Canonical, locale-independent keys for user preferences that used to be stored as +// localized display text. Legacy localized values are normalized on read so existing +// installs keep working after switching device language. +object TopicKey { + const val KOTLIN = "Kotlin" + const val JAVASCRIPT = "JavaScript" + const val PYTHON = "Python" + const val PHP = "PHP" + const val SQL = "SQL" + const val GIT = "Git" + const val CLEAN_CODE = "Clean Code" + const val DATA_STRUCTURES = "Data Structures" + + val ALL = listOf(KOTLIN, JAVASCRIPT, PYTHON, PHP, SQL, GIT, CLEAN_CODE, DATA_STRUCTURES) + + fun normalize(raw: String): String = when (raw.trim().lowercase()) { + "kotlin" -> KOTLIN + "javascript" -> JAVASCRIPT + "python" -> PYTHON + "php" -> PHP + "sql" -> SQL + "git" -> GIT + "clean code", "clean_code" -> CLEAN_CODE + "data structures", "estructuras de datos", "data_structures" -> DATA_STRUCTURES + else -> raw.trim() + } + + fun displayRes(key: String): Int = when (normalize(key)) { + KOTLIN -> R.string.topic_kotlin + JAVASCRIPT -> R.string.topic_javascript + PYTHON -> R.string.topic_python + PHP -> R.string.topic_php + SQL -> R.string.topic_sql + GIT -> R.string.topic_git + CLEAN_CODE -> R.string.topic_clean_code + DATA_STRUCTURES -> R.string.topic_data_structures + else -> R.string.topic_kotlin + } +} + +object DifficultyKey { + const val BEGINNER = "BEGINNER" + const val INITIAL = "INITIAL" + const val INTERMEDIATE = "INTERMEDIATE" + const val MIXED = "MIXED" + + val ALL = listOf(BEGINNER, INITIAL, INTERMEDIATE, MIXED) + + fun normalize(raw: String): String = when (raw.trim().lowercase()) { + BEGINNER.lowercase(), "principiante" -> BEGINNER + INITIAL.lowercase(), "inicial" -> INITIAL + INTERMEDIATE.lowercase(), "intermedia" -> INTERMEDIATE + MIXED.lowercase(), "mixta" -> MIXED + else -> raw.trim() + } + + fun displayRes(key: String): Int = when (normalize(key)) { + BEGINNER -> R.string.settings_lang_diff_beginner + INITIAL -> R.string.settings_lang_diff_initial + INTERMEDIATE -> R.string.settings_lang_diff_intermediate + MIXED -> R.string.settings_lang_diff_mixed + else -> R.string.settings_lang_diff_initial + } +} diff --git a/app/src/main/java/com/tamagotchi/code/util/DailyCommitGenerator.kt b/app/src/main/java/com/tamagotchi/code/util/DailyCommitGenerator.kt index 55816fa..dcfba8e 100644 --- a/app/src/main/java/com/tamagotchi/code/util/DailyCommitGenerator.kt +++ b/app/src/main/java/com/tamagotchi/code/util/DailyCommitGenerator.kt @@ -1,19 +1,18 @@ package com.tamagotchi.code.util +import android.content.Context +import androidx.annotation.StringRes +import com.tamagotchi.code.R import kotlin.random.Random object DailyCommitGenerator { - fun generateCommit(activities: Set, petName: String): String { + fun generateCommit(context: Context, activities: Set, petName: String): String { if (activities.isEmpty()) { - return Random.nextCommitMsg( - listOf( - "docs: hoy no hice nada productivo", - "chore: día de procrastinación técnica", - "fix: sobreviví otro día sin crashear", - "style: no toqué nada pero me veo bien", - "refactor: moví archivos de lugar sin motivo aparente" - ) + return randomOf( + context, petName, + R.string.commit_idle_1, R.string.commit_idle_2, R.string.commit_idle_3, + R.string.commit_idle_4, R.string.commit_idle_5 ) } @@ -28,68 +27,44 @@ object DailyCommitGenerator { val activeCount = listOf(hasStudied, hasShopped, hasPlayed, hasSlept, hasPetted, hasClean, hasChallenge).count { it } return when { - activeCount >= 3 -> Random.nextCommitMsg( - listOf( - "feat: hoy fue un día completo, hasta hice merge", - "feat: commit masivo con varias funcionalidades", - "chore: $petName tuvo un día ocupado" - ) + activeCount >= 3 -> randomOf( + context, petName, + R.string.commit_busy_1, R.string.commit_busy_2, R.string.commit_busy_3 ) - hasStudied && hasPetted -> "feat: aprendí cosas nuevas mientras recibía cariño" - hasStudied -> Random.nextCommitMsg( - listOf( - "feat: aprendí cosas nuevas hoy", - "feat: $petName estudió y ganó XP", - "docs: sesión de estudio completada" - ) + hasStudied && hasPetted -> context.getString(R.string.commit_study_love) + hasStudied -> randomOf( + context, petName, + R.string.commit_study_1, R.string.commit_study_2, R.string.commit_study_3 ) - hasShopped -> Random.nextCommitMsg( - listOf( - "chore: gasté bytes en cosas innecesarias", - "chore: compras impulsivas en la tienda", - "feat: nuevos items adquiridos" - ) + hasShopped -> randomOf( + context, petName, + R.string.commit_shop_1, R.string.commit_shop_2, R.string.commit_shop_3 ) - hasPlayed -> Random.nextCommitMsg( - listOf( - "refactor: me distraje un rato", - "feat: sesión de juegos completada", - "test: probé mis reflejos en los minijuegos" - ) + hasPlayed -> randomOf( + context, petName, + R.string.commit_play_1, R.string.commit_play_2, R.string.commit_play_3 ) - hasSlept -> Random.nextCommitMsg( - listOf( - "fix: pausa activa para recargar baterías", - "fix: $petName durmió y recuperó energía", - "chore: modo ahorro de energía activado" - ) + hasSlept -> randomOf( + context, petName, + R.string.commit_sleep_1, R.string.commit_sleep_2, R.string.commit_sleep_3 ) - hasPetted -> Random.nextCommitMsg( - listOf( - "style: recibí cariño y eso mejora el código", - "feat: ++felicidad, --bugs", - "chore: mantenimiento emocional completado" - ) + hasPetted -> randomOf( + context, petName, + R.string.commit_pet_1, R.string.commit_pet_2, R.string.commit_pet_3 ) - hasClean -> "refactor: limpieza profunda del sistema" - hasChallenge -> Random.nextCommitMsg( - listOf( - "feat: desafío de código completado", - "fix: bugs eliminados con éxito", - "test: retos de programación superados" - ) + hasClean -> context.getString(R.string.commit_clean_1) + hasChallenge -> randomOf( + context, petName, + R.string.commit_challenge_1, R.string.commit_challenge_2, R.string.commit_challenge_3 ) - else -> Random.nextCommitMsg( - listOf( - "chore: día normal, nada especial", - "docs: $petName estuvo tranquilo hoy", - "style: sin cambios aparentes" - ) + else -> randomOf( + context, petName, + R.string.commit_normal_1, R.string.commit_normal_2, R.string.commit_normal_3 ) } } - private fun Random.nextCommitMsg(messages: List): String { - return messages[this.nextInt(messages.size)] + private fun randomOf(context: Context, petName: String, @StringRes vararg messages: Int): String { + return context.getString(messages[Random.nextInt(messages.size)], petName) } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/tamagotchi/code/util/PetCheckWorker.kt b/app/src/main/java/com/tamagotchi/code/util/PetCheckWorker.kt index 558c66b..3892042 100644 --- a/app/src/main/java/com/tamagotchi/code/util/PetCheckWorker.kt +++ b/app/src/main/java/com/tamagotchi/code/util/PetCheckWorker.kt @@ -51,22 +51,22 @@ class PetCheckWorker( } val title = when { - petState.health <= 0 -> "${petState.name} ha muerto" - petState.health < 5 -> "${petState.name} está al borde de la muerte" - petState.hunger < 5 -> "${petState.name} se muere de hambre" - petState.energy < 5 -> "${petState.name} está en las últimas" - else -> "${petState.name} te necesita" + petState.health <= 0 -> applicationContext.getString(R.string.notif_title_dead, petState.name) + petState.health < 5 -> applicationContext.getString(R.string.notif_title_dying, petState.name) + petState.hunger < 5 -> applicationContext.getString(R.string.notif_title_starving, petState.name) + petState.energy < 5 -> applicationContext.getString(R.string.notif_title_exhausted, petState.name) + else -> applicationContext.getString(R.string.notif_title_needs_you, petState.name) } val reason = when { - petState.health <= 0f -> "Me he ido al otro lado del compilador... ¿me rescatas?" - petState.health < 5f -> "Me duele hasta el último bit... ¿puedes revisarme? (Salud: ${petState.health.toInt()}%)" - petState.health < 20f -> "Noto que el Blue Screen of Death se acerca... (Salud: ${petState.health.toInt()}%)" - petState.hunger < 5f -> "Llevo horas sin comer, ¿crees que soy un microservicio? (Hambre: ${petState.hunger.toInt()}%)" - petState.hunger < 20f -> "Mi estómago está haciendo un loop infinito de ruidos. (Hambre: ${petState.hunger.toInt()}%)" - petState.energy < 5f -> "Mi batería está en rojo. Y no es una metáfora de código. (Energía: ${petState.energy.toInt()}%)" - petState.energy < 15f -> "Oye, ¿has visto mis logs? Están llenos de NullPointerException existenciales. (Energía: ${petState.energy.toInt()}%)" - else -> "¡Necesito atención!" + petState.health <= 0f -> applicationContext.getString(R.string.notif_reason_dead) + petState.health < 5f -> applicationContext.getString(R.string.notif_reason_health_pain, petState.health.toInt()) + petState.health < 20f -> applicationContext.getString(R.string.notif_reason_health_low, petState.health.toInt()) + petState.hunger < 5f -> applicationContext.getString(R.string.notif_reason_hunger_extreme, petState.hunger.toInt()) + petState.hunger < 20f -> applicationContext.getString(R.string.notif_reason_hunger_loop, petState.hunger.toInt()) + petState.energy < 5f -> applicationContext.getString(R.string.notif_reason_energy_red, petState.energy.toInt()) + petState.energy < 15f -> applicationContext.getString(R.string.notif_reason_energy_logs, petState.energy.toInt()) + else -> applicationContext.getString(R.string.notif_reason_general) } val intent = Intent(applicationContext, MainActivity::class.java).apply { diff --git a/app/src/main/java/com/tamagotchi/code/widget/CodePetWidgetProvider.kt b/app/src/main/java/com/tamagotchi/code/widget/CodePetWidgetProvider.kt index 0c3c575..04b7edf 100644 --- a/app/src/main/java/com/tamagotchi/code/widget/CodePetWidgetProvider.kt +++ b/app/src/main/java/com/tamagotchi/code/widget/CodePetWidgetProvider.kt @@ -35,22 +35,22 @@ class CodePetWidgetProvider : AppWidgetProvider() { if (petState != null) { views.setTextViewText(R.id.widget_title, petState.name) - views.setTextViewText(R.id.widget_level, "Nv.${petState.level}") + views.setTextViewText(R.id.widget_level, context.getString(R.string.widget_level, petState.level)) val effectiveStatus = if (petState.isDead) "DEAD" else petState.currentStatus - val subtitle = when (effectiveStatus) { - "HAPPY" -> "\u00a1Todo va genial!" - "SLEEPING" -> "Zzz... durmiendo" - "SICK" -> "No me siento bien" - "HUNGRY" -> "\u00a1Tengo hambre!" - "SAD" -> "Estoy triste" - "STUDYING" -> "\u00a1Modo estudio!" - "EXCITED" -> "\u00a1Muy emocionado!" - "DEAD" -> "Descansa en paz..." - else -> "\u00a1Todo va genial!" + val subtitleRes = when (effectiveStatus) { + "HAPPY" -> R.string.widget_status_happy + "SLEEPING" -> R.string.widget_status_sleeping + "SICK" -> R.string.widget_status_sick + "HUNGRY" -> R.string.widget_status_hungry + "SAD" -> R.string.widget_status_sad + "STUDYING" -> R.string.widget_status_studying + "EXCITED" -> R.string.widget_status_excited + "DEAD" -> R.string.widget_status_dead + else -> R.string.widget_status_happy } - views.setTextViewText(R.id.widget_subtitle, subtitle) + views.setTextViewText(R.id.widget_subtitle, context.getString(subtitleRes)) val imageRes = when (effectiveStatus) { "DEAD" -> R.drawable.mascota_dead @@ -74,19 +74,25 @@ class CodePetWidgetProvider : AppWidgetProvider() { views.setProgressBar(R.id.hunger_bar, 100, hunger, false) views.setTextViewText(R.id.energy_value, "${energy}%") views.setTextViewText(R.id.hunger_value, "${hunger}%") - views.setTextViewText(R.id.widget_bytes, "${petState.bytes} bytes") - views.setTextViewText(R.id.widget_streak, "Racha: ${petState.streak} d\u00edas") + views.setTextViewText(R.id.widget_bytes, context.getString(R.string.widget_bytes, petState.bytes)) + views.setTextViewText( + R.id.widget_streak, + context.resources.getQuantityString(R.plurals.widget_streak, petState.streak, petState.streak) + ) } else { - views.setTextViewText(R.id.widget_title, "CodePet") - views.setTextViewText(R.id.widget_level, "Nv.1") - views.setTextViewText(R.id.widget_subtitle, "\u00a1Inicia la app!") + views.setTextViewText(R.id.widget_title, context.getString(R.string.default_pet_name)) + views.setTextViewText(R.id.widget_level, context.getString(R.string.widget_level, 1)) + views.setTextViewText(R.id.widget_subtitle, context.getString(R.string.widget_start_app)) views.setImageViewResource(R.id.widget_pet_image, R.drawable.mascota_happy) views.setProgressBar(R.id.energy_bar, 100, 0, false) views.setProgressBar(R.id.hunger_bar, 100, 0, false) views.setTextViewText(R.id.energy_value, "0%") views.setTextViewText(R.id.hunger_value, "0%") - views.setTextViewText(R.id.widget_bytes, "0 bytes") - views.setTextViewText(R.id.widget_streak, "Racha: 0 d\u00edas") + views.setTextViewText(R.id.widget_bytes, context.getString(R.string.widget_bytes, 0)) + views.setTextViewText( + R.id.widget_streak, + context.resources.getQuantityString(R.plurals.widget_streak, 0, 0) + ) } val launchIntent = Intent(context, MainActivity::class.java) diff --git a/app/src/main/res/layout/code_pet_widget.xml b/app/src/main/res/layout/code_pet_widget.xml index 53a13e4..1f44562 100644 --- a/app/src/main/res/layout/code_pet_widget.xml +++ b/app/src/main/res/layout/code_pet_widget.xml @@ -1,5 +1,6 @@ @@ -106,7 +107,7 @@ android:id="@+id/energy_label" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:text="Energía" + android:text="@string/meter_energy" android:textColor="#B7C2D9" android:textSize="9sp" android:textStyle="bold" @@ -142,7 +143,7 @@ android:id="@+id/hunger_label" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:text="Hambre" + android:text="@string/widget_hunger" android:textColor="#B7C2D9" android:textSize="9sp" android:textStyle="bold" @@ -178,7 +179,7 @@ android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" - android:text="50 bytes" + tools:text="50 bytes" android:textColor="#FFD700" android:textSize="10sp" /> @@ -186,7 +187,7 @@ android:id="@+id/widget_streak" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:text="Racha: 0 d\u00edas" + tools:text="Racha: 0 días" android:textColor="#9BA3B2" android:textSize="10sp" /> diff --git a/app/src/main/res/values-en/strings.xml b/app/src/main/res/values-en/strings.xml index a87346e..0581009 100644 --- a/app/src/main/res/values-en/strings.xml +++ b/app/src/main/res/values-en/strings.xml @@ -106,7 +106,7 @@ Reorder the code so it makes sense before the build fails. Classic Arcade Old-school minigames to kill time. - On cooldown (Come back later for a bigger reward) + On cooldown (come back later for a bigger reward) Play @@ -232,7 +232,7 @@ You got %1$d of %2$d Explanation: %1$s The closing parenthesis is missing in println. - It was a fugitive parenthesis! It escaped without paying rent. + It was a fugitive parenthesis! It escaped without leaving a forwarding address. In Kotlin comparison is ==, not =. = is assignment. Codey: One = is assignment, double == is comparison! Don\'t confuse the compiler. The explicit return type is missing, it should be \': Int\'. @@ -265,7 +265,7 @@ git push --force Creating a new branch from main is the right way to start a feature. Codey: Never push to main without creating a branch first! You cowboy. - You made local changes and realize you broke everything. You want to go back to the last clean commit. + You committed a change that broke everything on your branch. You want to safely undo it while keeping the history intact. git reset --hard HEAD git revert HEAD git rm -rf . @@ -281,7 +281,7 @@ git pull origin main git merge feature main git branch -d main - git pull brings main\'s changes into your current branch. merge would do it the other way around. + git pull origin main brings main\'s changes into your current branch: it\'s fetch + merge in a single command. Codey: Don\'t delete main. Never delete main. It\'s like deleting the dictionary. You committed but forgot to include a file. How do you fix it? git add file && git commit --amend @@ -301,7 +301,7 @@ git commit --fixup Interactive rebase lets you squash, reorder and rename commits. Codey: \'fix\' and \'update\' messages are like writing \'thing\' on an exam. Be descriptive. - Your feature branch fell behind main and you need to update it without creating merge commits. + Your local branch is behind its remote counterpart and you want to incorporate those changes without creating merge commits. git rebase main git merge main git pull --rebase @@ -471,4 +471,285 @@ FULL BONUS: 500 XP + Exclusive hat! Complete the 3 weekly missions for a special bonus. + + 5 code challenges + Complete 5 challenges in Learn + 2h focus mode + Study for 2 hours in focus mode + 3 Bug Hunt wins + Win 3 rounds of Bug Hunt + + + Double XP Sunday + XP x1.5/2/3 on Sundays + Slow decay + Decay -10%/-20%/-30% + Shop discount + 5%/10%/15% off + Minigame bonus + +10%/+20%/+30% rewards + Offline XP + 1h/2h/4h of passive XP + Extra heart + Up to 6 hearts + + + First build + Complete your first Focus session + Expert level + Level up for the first time + Bug hunter + Perfect score in Bug Hunt + Git without panic + Pass Git Rescue with 3+ correct answers + 7-day streak + Keep a study streak of 7 days + 30-day streak + Keep a study streak of 30 days + Collector + Unlock 3 visual themes + Completionist + Unlock all 12 visual themes + Save-scanner + Accumulate 1000 Bytes + First pet + Pet your buddy for the first time + Dozing off + Put your pet to sleep + Deep clean + Clean your pet 10 times + + + Hello, World! + Variables Initialized + Double Shot Coffee + Optimized Loop + Bug Squashed + Clean Build + Successful Deploy (PROD)! + Daily Rewards + Reward Claimed! + Come back tomorrow for more + Come back every day for more! + DAY %1$d + DAY 7 — MEGA PACK! + +%1$d Health + +%1$d Energy + CLAIM REWARD + CLOSE + + + Study Kotlin + Let\'s study Kotlin today! + Visit the shop + Take me to the shop to buy something! + Play Bug Hunt + Let\'s play Bug Hunt, it\'s been a while! + Buy pizza + I\'m hungry, buy me a pizza! + Pet me 3 times + I need a lot of love today! + Clean my habitat + My home is dirty, I need you to clean me! + 25 minutes of Focus + Let\'s put on focus music and work for 25 minutes! + Complete 3 challenges + Let\'s do some mental exercises! + + + Found a bug in production 😱 + Saw you using tabs 😊 + Saw you using spaces 🤔 + Commit with no description 😞 + Clean code detected 🎉 + Spilled coffee 😰 + + + Rename + Specialist: %1$s + Heart %1$d + LVL %1$d + Level %1$d + Love + Message + XP: %1$d / %2$d + Health + Food + Energy + Sleep or wake up + Wake up + Sleep + Pet + Clean + Play + + + %1$d day + %1$d days + + + + %1$s is here! + + XP x%2$s for %1$d more challenge + XP x%2$s for %1$d more challenges + + + + Fun Fact + Programmer Joke + Coding Tip + Another! + + + Your programming companion + Home + Learn + Study + Shop + Code Tamagotchi icon + Open settings + + + + Zzz… if (dream) { sleep() } else { repeat() }… Zzz + Charging batteries… don\'t interrupt my main thread. + Dreaming of fast compilers and zero NullPointers… + My CPU is in power-saving mode. Come back in one clock cycle. + Zzz… did you see that commit? It was… legendary… + + + Shhh! I\'m optimizing algorithms in my brain. + Compiling… coding at 1000 WPM. + I can feel my binary neural synapses growing. + Did you know the first bug was an actual moth? I prefer the digital ones. + My code is art. Your code… well, it works. + Total concentration. Don\'t force-push on me right now. + + + Error 500: urgent debugging needed. Give me a pill! + Too many bugs piling up in my stack… I feel bad. + My CPU feels overheated. Can we go over some review questions? + My garbage collector isn\'t working. I feel… dirty. + Should I reformat? No, just give me some love — it\'s less traumatic. + + + Feeling lazy… I\'m a bit down. + My motivation battery is below 20%. + Procrastinating again? My code is filling up with warnings. + I feel my architecture crumbling. I need an emotional refactor. + Not even a \'Hello World\' cheers me up today. + + + NullPointerException in my stomach! I need bytes. + My energy cache is empty, can I get a snack? + Without food, my performance drops to O(n^2). + My stomach is running an infinite loop of growls. + Feed me or I\'ll start deleting your temp files. Kidding… or am I? + + + Wiii! My code is O(1) and so is my heart! + Happiness level at MAX! Thanks for loving me. + I feel like I could compile the Linux kernel in 1 second! + I am the root of your heart! Wiiiii! + Everything compiles on the first try! This is dark magic! + + + R.I.P. Killed by a segfault… + Here lies a pet. Its code was never merged. + 404: life not found. Revive me to continue. + from grave import revive # still in time? + + + Compiling without warnings is my passion! + Ready to write some clean code today? + I feel the power of a successful refactor! + I like you — you make my architecture modular and solid. + Have you tried turning it off and on again? Works for me. + Your code is so clean I can see my reflection in it. + Hey, have you seen my logs? They\'re full of love for you. + + + + %1$s has died + %1$s is on the verge of death + %1$s is starving + %1$s is running on fumes + %1$s needs you + I\'ve gone to the other side of the compiler… will you rescue me? + Even my last bit hurts… can you check on me? (Health: %1$d%%) + I feel the Blue Screen of Death approaching… (Health: %1$d%%) + Hours without eating — think I\'m a microservice? (Hunger: %1$d%%) + My stomach is running an infinite loop of growls. (Hunger: %1$d%%) + My battery is in the red. And it\'s not a code metaphor. (Energy: %1$d%%) + Hey, have you seen my logs? Full of existential NullPointerExceptions. (Energy: %1$d%%) + I need attention! + %1$s made a commit + + + Codey Care + Codey\'s hunger + Codey is hungry + Codey\'s health + Codey is sick + Codey\'s energy + Codey is tired + Codey in danger + Codey is in critical condition + Codey reminders + Reminders about your virtual pet\'s status + + + docs: did nothing productive today + chore: day of technical procrastination + fix: survived another day without crashing + style: touched nothing but still look good + refactor: moved files around for no apparent reason + feat: full day today, even did a merge + feat: massive commit with several features + chore: %1$s had a busy day + feat: learned new things while getting pets + feat: learned new things today + feat: %1$s studied and earned XP + docs: study session completed + chore: spent bytes on unnecessary things + chore: impulse buys at the shop + feat: new items acquired + refactor: got distracted for a while + feat: gaming session completed + test: tried my reflexes in the minigames + fix: active pause to recharge batteries + fix: %1$s slept and recovered energy + chore: power-saving mode enabled + style: got pets, which improves the code + feat: ++happiness, --bugs + chore: emotional maintenance completed + refactor: deep system cleanup + feat: code challenge completed + fix: bugs successfully eliminated + test: programming challenges passed + chore: regular day, nothing special + docs: %1$s stayed quiet today + style: no apparent changes + + + Lv.%1$d + All systems happy! + Zzz… sleeping + I don\'t feel well + I\'m hungry! + Feeling sad + Study mode on! + So excited! + Rest in peace… + %1$d bytes + Hunger + Open the app! + Code Pet mascot + + + Streak: %1$d day + Streak: %1$d days + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 5fbace7..c99a48b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -106,7 +106,7 @@ Ordena el código para que tenga sentido antes de que falle el build. Arcade Clásico Minijuegos antiguos para pasar el rato. - En enfriamiento (Vuelve más tarde para ganar más recompensa) + En enfriamiento (vuelve más tarde para ganar más recompensa) Jugar @@ -232,7 +232,7 @@ Acertaste %1$d de %2$d Explicación: %1$s Falta el paréntesis de cierre en println. - ¡Era un paréntesis fugitivo! Se escapó sin pagar el alquiler. + ¡Era un paréntesis fugitivo! Escapó sin dejar señas. En Kotlin la comparación es == no =. = es asignación. Codey: ¡Un solo = es asignación, doble == es comparación! No confundas al compilador. Falta el tipo de retorno explícito, debería ser \': Int\'. @@ -265,7 +265,7 @@ git push --force Crear una rama nueva desde main es la forma correcta de empezar un feature. Codey: ¡Nunca pushes a main sin antes crear una rama! Eres un cowboy. - Has hecho cambios locales y te das cuenta de que rompiste todo. Quieres volver al último commit limpio. + Hiciste un commit que rompió todo en tu rama. Quieres deshacerlo de forma segura, preservando el historial. git reset --hard HEAD git revert HEAD git rm -rf . @@ -281,7 +281,7 @@ git pull origin main git merge feature main git branch -d main - git pull trae los cambios de main a tu rama actual. merge lo haría al revés. + git pull origin main trae los cambios de main a tu rama actual: es un fetch + merge en un solo comando. Codey: No borres main. Nunca borres main. Es como borrar el diccionario. Hiciste un commit pero olvidaste incluir un archivo. ¿Cómo lo arreglas? git add archivo && git commit --amend @@ -301,7 +301,7 @@ git commit --fixup rebase interactivo te permite squash, reordenar y renombrar commits. Codey: Los mensajes \'fix\' y \'update\' son como decir \'cosa\' en un examen. Sé descriptivo. - Tu rama feature quedó atrás de main y necesitas actualizarla sin crear commits de merge. + Tu rama local quedó atrás respecto a su versión en el remoto y quieres incorporar esos cambios sin crear commits de merge. git rebase main git merge main git pull --rebase @@ -455,7 +455,7 @@ >>> PASE DE TEMPORADA - Gana XP de pase completando actividades. 20 niveles de recompensas! + Gana XP de pase completando actividades. ¡20 niveles de recompensas! NIVEL %1$d / 20 XP del pase: %1$d/100 PASE PREMIUM: Skins exclusivas + sombreros raros + XP boost @@ -464,11 +464,292 @@ Nivel 5: Sombrero básico Nivel 10: 200 XP Nivel 15: Carta de código rara - Nivel 20: Skin legendaria! + Nivel 20: ¡Skin legendaria! >>> MISIONES SEMANALES - BONUS COMPLETO: 500 XP + Sombrero exclusivo! + BONUS COMPLETO: 500 XP + ¡Sombrero exclusivo! Completa las 3 misiones semanales para un bonus especial. + + 5 retos de código + Completa 5 retos en Aprender + 2h modo foco + Estudia 2 horas en modo foco + Gana 3 Bug Hunt + Gana 3 partidas de Bug Hunt + + + Doble XP domingo + XP x1.5/2/3 los domingos + Decaimiento lento + Decaimiento -10%/-20%/-30% + Descuento tienda + 5%/10%/15% descuento + Bonus minijuegos + +10%/+20%/+30% recompensa + XP offline + 1h/2h/4h de XP pasivo + Corazón extra + Máximo 6 corazones + + + Primer build + Completa tu primera sesión Focus + Nivel Experto + Sube de nivel por primera vez + Cazador de bugs + Puntuación perfecta en Bug Hunt + Git sin pánico + Aprueba Git Rescue con 3+ aciertos + Racha de 7 días + Mantén una racha de estudio de 7 días + Racha de 30 días + Mantén una racha de estudio de 30 días + Coleccionista + Desbloquea 3 temas visuales + Completista + Desbloquea los 12 temas visuales + Ahorrador + Acumula 1000 Bytes + Primer mimo + Acaricia a tu mascota por primera vez + Duermevela + Pon a dormir a tu mascota + Limpieza profunda + Limpia a tu mascota 10 veces + + + ¡Hola, Mundo! + Variables Inicializadas + Café Doble Shot + Bucle Optimizado + Bug Solucionado + Compilación Limpia + ¡Despliegue Exitoso (PROD)! + Recompensas Diarias + ¡Recompensa Reclamada! + Vuelve mañana por más + ¡Vuelve cada día por más! + DÍA %1$d + DÍA 7 — ¡MEGA PACK! + +%1$d Salud + +%1$d Energía + RECLAMAR RECOMPENSA + CERRAR + + + Estudia Kotlin + ¡Hoy quiero estudiar Kotlin! + Visita la tienda + ¡Llévame a la tienda a comprar algo! + Juega Bug Hunt + ¡Juguemos Bug Hunt, hace tiempo que no! + Compra pizza + Tengo hambre, ¡cómprame una pizza! + Acaríciame 3 veces + ¡Hoy necesito mucho cariño! + Limpia mi hábitat + Mi casa está sucia, ¡necesito que me limpies! + Foco 25 minutos + ¡Pongamos música de foco y trabajemos 25 minutos! + Completa 3 retos + ¡Hagamos ejercicios mentales! + + + Encontró un bug en producción 😱 + Te vio usar tabs 😊 + Te vio usar espacios 🤔 + Commit sin descripción 😞 + Código limpio detectado 🎉 + Café derramado 😰 + + + Renombrar + Especialista: %1$s + Corazón %1$d + LVL %1$d + Nivel %1$d + Amor + Mensaje + XP: %1$d / %2$d + Vida + Alimento + Energía + Dormir o despertar + Despertar + Dormir + Acariciar + Limpiar + Jugar + + + %1$d día + %1$d días + + + + ¡%1$s está aquí! + + XP x%2$s por %1$d reto más + XP x%2$s por %1$d retos más + + + + Dato Curioso + Chiste de Programador + Tip de Código + ¡Otra! + + + Tu compañero de programación + Inicio + Aprender + Estudio + Tienda + Icono de Code Tamagotchi + Abrir configuración + + + + Zzz… if (dream) { sleep() } else { repeat() }… Zzz + Cargando baterías… no interrumpas mi hilo principal. + Soñando con compiladores veloces y cero NullPointers… + Mi CPU está en modo ahorro. Vuelve en un ciclo de reloj. + Zzz… ¿viste ese commit? Fue… legendario… + + + ¡Shhh! Estoy optimizando algoritmos en mi cerebro. + Compilando… codeando a 1000 WPM. + Siento cómo se incrementa mi sinapsis neuronal binaria. + ¿Sabías que el primer bug fue una polilla real? Yo prefiero los digitales. + Mi código es arte. Tu código… bueno, funciona. + Concentración total. No me hagas un force push ahora. + + + Error 500: Necesito desbuguear urgente. ¡Dame una píldora! + Demasiados bugs acumulados en mi stack… me siento mal. + Siento mi CPU sobrecalentada. ¿Podemos repasar un poco? + Mi recolector de basura no está funcionando. Me siento… sucio. + ¿Me formateas? No, mejor dame cariño, es menos traumático. + + + Tengo flojera… me siento un poco depre. + Mi batería de motivación está por debajo del 20%. + ¿Procrastinando otra vez? Mi código se llena de advertencias. + Siento que mi arquitectura se desmorona. Necesito un refactor emocional. + Ni siquiera un \'Hello World\' me anima hoy. + + + ¡NullPointerException en mi estómago! Necesito bytes. + Mi caché de energía está vacía, ¿me das de comer? + Sin comida, mi rendimiento cae a O(n^2). + Mi estómago está haciendo un loop infinito de ruidos. + Aliméntame o empezaré a borrar tus archivos temporales. Es broma… ¿o no? + + + ¡Wiii! ¡Mi código es O(1) y mi corazón también! + ¡Nivel de felicidad al MÁXIMO! Gracias por quererme. + ¡Siento que podría compilar el kernel de Linux en 1 segundo! + ¡Soy el root de tu corazón! ¡Wiiiii! + ¡Todo compila a la primera! ¡Esto es magia negra! + + + R.I.P. Fue procesado por un segfault… + Aquí yace una mascota. Su código quedó sin mergear. + 404: vida no encontrada. Revídame para continuar. + from grave import revive # ¿todavía estás a tiempo? + + + ¡Compilar sin advertencias es mi pasión! + ¿Listo para tirar unas líneas de código limpias hoy? + ¡Siento el poder de un refactor exitoso! + Me agradas, haces que mi arquitectura sea modular y sólida. + ¿Has probado a apagarlo y volverlo a encender? A mí me funciona. + Tu código es tan limpio que puedo ver mi reflejo en él. + Oye, ¿has visto mis logs? Están llenos de amor por ti. + + + + %1$s ha muerto + %1$s está al borde de la muerte + %1$s se muere de hambre + %1$s está en las últimas + %1$s te necesita + Me he ido al otro lado del compilador… ¿me rescatas? + Me duele hasta el último bit… ¿puedes revisarme? (Salud: %1$d%%) + Noto que el Blue Screen of Death se acerca… (Salud: %1$d%%) + Llevo horas sin comer, ¿crees que soy un microservicio? (Hambre: %1$d%%) + Mi estómago está haciendo un loop infinito de ruidos. (Hambre: %1$d%%) + Mi batería está en rojo. Y no es una metáfora de código. (Energía: %1$d%%) + Oye, ¿has visto mis logs? Están llenos de NullPointerException existenciales. (Energía: %1$d%%) + ¡Necesito atención! + %1$s hizo commit + + + Cuidado de Codey + Hambre de Codey + Codey tiene hambre + Salud de Codey + Codey está enfermo + Energía de Codey + Codey está cansado + Codey en peligro + Codey está en estado crítico + Recordatorios de Codey + Recordatorios sobre el estado de tu mascota virtual + + + docs: hoy no hice nada productivo + chore: día de procrastinación técnica + fix: sobreviví otro día sin crashear + style: no toqué nada pero me veo bien + refactor: moví archivos de lugar sin motivo aparente + feat: hoy fue un día completo, hasta hice merge + feat: commit masivo con varias funcionalidades + chore: %1$s tuvo un día ocupado + feat: aprendí cosas nuevas mientras recibía cariño + feat: aprendí cosas nuevas hoy + feat: %1$s estudió y ganó XP + docs: sesión de estudio completada + chore: gasté bytes en cosas innecesarias + chore: compras impulsivas en la tienda + feat: nuevos items adquiridos + refactor: me distraje un rato + feat: sesión de juegos completada + test: probé mis reflejos en los minijuegos + fix: pausa activa para recargar baterías + fix: %1$s durmió y recuperó energía + chore: modo ahorro de energía activado + style: recibí cariño y eso mejora el código + feat: ++felicidad, --bugs + chore: mantenimiento emocional completado + refactor: limpieza profunda del sistema + feat: desafío de código completado + fix: bugs eliminados con éxito + test: retos de programación superados + chore: día normal, nada especial + docs: %1$s estuvo tranquilo hoy + style: sin cambios aparentes + + + Nv.%1$d + ¡Todo va genial! + Zzz… durmiendo + No me siento bien + ¡Tengo hambre! + Estoy triste + ¡Modo estudio! + ¡Muy emocionado! + Descansa en paz… + %1$d bytes + Hambre + ¡Inicia la app! + Mascota Code Pet + + + Racha: %1$d día + Racha: %1$d días + + diff --git a/app/src/test/java/com/tamagotchi/code/LocalizedContentTest.kt b/app/src/test/java/com/tamagotchi/code/LocalizedContentTest.kt new file mode 100644 index 0000000..9f50090 --- /dev/null +++ b/app/src/test/java/com/tamagotchi/code/LocalizedContentTest.kt @@ -0,0 +1,58 @@ +package com.tamagotchi.code + +import com.tamagotchi.code.data.ChallengesData +import com.tamagotchi.code.data.ChallengesDataEn +import com.tamagotchi.code.data.CodeCardsEn +import com.tamagotchi.code.data.CodeReviewData +import com.tamagotchi.code.data.CodeReviewDataEn +import com.tamagotchi.code.data.SpecialChallengesData +import com.tamagotchi.code.data.SpecialChallengesDataEn +import com.tamagotchi.code.data.codeCards +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Test + +class LocalizedContentTest { + + @Test + fun everyChallengeHasEnglishTranslationWithMatchingOptions() { + val all = ChallengesData.challenges + SpecialChallengesData.challenges + assertTrue(all.isNotEmpty()) + all.forEach { es -> + val en = ChallengesDataEn.translations[es.id] + ?: SpecialChallengesDataEn.translations[es.id] + assertNotNull("Missing EN translation for challenge id=${es.id} (${es.title})", en) + assertEquals("EN options count mismatch for id=${es.id}", es.options.size, en!!.options.size) + assertTrue( + "correctAnswerIndex out of range for id=${es.id}", + es.correctAnswerIndex in en.options.indices + ) + assertTrue("Empty EN title for id=${es.id}", en.title.isNotBlank()) + assertTrue("Empty EN explanation for id=${es.id}", en.explanation.isNotBlank()) + } + } + + @Test + fun everyCodeReviewSnippetHasEnglishTranslation() { + CodeReviewData.snippets.forEach { es -> + val en = CodeReviewDataEn.translations[es.id] + assertNotNull("Missing EN translation for review id=${es.id}", en) + assertTrue("Empty EN review code for id=${es.id}", en!!.code.isNotBlank()) + assertTrue("Empty EN review explanation for id=${es.id}", en.explanation.isNotBlank()) + } + } + + @Test + fun everyCodeCardHasEnglishTranslation() { + codeCards.forEach { card -> + val text = CodeCardsEn.texts[card.id] + assertNotNull("Missing EN text for code card id=${card.id}", text) + assertTrue("Blank EN text for code card id=${card.id}", !text.isNullOrBlank()) + } + assertEquals(codeCards.size, CodeCardsEn.texts.size) + } + + private fun assertNotNull(message: String, value: Any?) { + if (value == null) throw AssertionError(message) + } +} From d82320ab72040991638ba2975912f14e96f2a721 Mon Sep 17 00:00:00 2001 From: openhands Date: Thu, 17 Sep 2026 18:56:47 -0400 Subject: [PATCH 3/3] docs: seccion Localizacion en ambos READMEs (estado ES/EN real, limitaciones de canales y temas) --- README.es.md | 14 ++++++++++++++ README.md | 14 ++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/README.es.md b/README.es.md index d4e2e5a..be728f9 100644 --- a/README.es.md +++ b/README.es.md @@ -43,6 +43,7 @@ [📦 Instalación](#-instalación) · [🎮 Cómo jugar](#-cómo-jugar) · [🎨 Temas visuales](#-temas-visuales) · + [🌍 Localización](#-localización) · [🗺️ Roadmap](#️-roadmap) · [🤝 Contribuir](#-contribuir) @@ -369,6 +370,19 @@ Code Tamagotchi incluye **12 temas premium** con personalidad única. Cada uno c --- +## 🌍 Localización + +Code Tamagotchi es totalmente bilingüe (**español + inglés**) y sigue el **idioma del sistema** automáticamente — no hay selector dentro de la app: + +- ✅ UI, diálogos, textos de juegos, notificaciones y widget: `values/` (es) + `values-en/` (en) +- ✅ Contenido educativo: 90 retos de código, 6 desafíos de algoritmos, 10 snippets de Code Review y 40 code cards traducidos al inglés (emparejados por id estable, verificado en `LocalizedContentTest`) +- ⚠️ Las etiquetas de los **canales de notificación** (Ajustes de Android) se crean una sola vez con el idioma activo en la primera instalación +- ⚠️ Los nombres de los temas se muestran en español por diseño (son identificadores estables persistidos en preferencias) + +Las traducciones viven junto a los datos originales: `ChallengesDataEn.kt`, `SpecialChallengesDataEn.kt`, `CodeReviewDataEn.kt`, `CodeCardsEn.kt`. PRs para mejorar redacción (en cualquier idioma) son muy bienvenidas. + +--- + ## 🗺️ Roadmap - [x] 🐾 Mascota virtual con estados emocionales y decaimiento diff --git a/README.md b/README.md index 574ea35..8633315 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,7 @@ [⚡ Quick Start](#-quick-start) · [🎮 How to Play](#-how-to-play) · [🎨 Themes](#-themes) · + [🌍 Localization](#-localization) · [🗺️ Roadmap](#️-roadmap) · [🤝 Contributing](#-contributing) @@ -328,6 +329,19 @@ Code Tamagotchi ships with **12 premium themes**. Each configures colors, typogr --- +## 🌍 Localization + +Code Tamagotchi is fully bilingual (**English + Spanish**) and follows your **system language** automatically — there is no in-app switcher: + +- ✅ UI, dialogs, game texts, notifications and widget: `values/` (es) + `values-en/` (en) +- ✅ Learning content: 90 code challenges, 6 special algorithm challenges, 10 code-review snippets and 40 code cards translated to English (matched by stable id, order-checked in `LocalizedContentTest`) +- ⚠️ Notification **channel labels** (Android Settings) are created once with the language active at first install +- ⚠️ Theme names are shown in Spanish by design (they are stable identifiers stored in preferences) + +Translations live next to the original data: `ChallengesDataEn.kt`, `SpecialChallengesDataEn.kt`, `CodeReviewDataEn.kt`, `CodeCardsEn.kt`. PRs to fix wording (either language) are very welcome. + +--- + ## 🗺️ Roadmap - [x] 🐾 Virtual pet with emotional states and decay