// =========================================================================
// ex21/Ex21Activity.kt - une application en deux langues
// -------------------------------------------------------------------------
// La méthode d'Android :
// - les TEXTES de l'application sont des RESSOURCES : un fichier
// res/values/strings.xml (la langue par défaut, ici le français) et un
// fichier par traduction (res/values-en/strings.xml). Chaque texte a un
// nom ; le code ne connaît que ce nom (R.string.ex21_bonjour), et
// Android choisit le fichier selon la langue de l'application ;
// - stringResource(R.string.ex21_bonjour, "Marie") : le texte, dont les
// paramètres (%1$s) sont remplacés ; pluralStringResource : un texte au
// pluriel, dont la forme suit les règles de la langue ;
// - la langue de l'application : celle du téléphone... ou celle que
// l'application choisit elle-même, AppCompatDelegate.setApplicationLocales.
// Android RECRÉE alors l'activité, dans la nouvelle langue : les textes,
// les dates, les montants, et les textes d'Android lui-même (le
// calendrier, ses boutons).
// Le serveur RdvMedecins, lui, ne renvoie que des CLÉS ("ERRORS.LOGIN_TAKEN") :
// on les associe aux ressources par une table (clesServeur).
// L'activité est une AppCompatActivity : la classe d'activité qui sait
// appliquer la langue choisie, même sur les versions d'Android antérieures
// à la 13.
// =========================================================================
@file:OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class)
package fr.stahe.exemples.ex21
import android.os.Bundle
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.DateRange
import androidx.compose.material3.Card
import androidx.compose.material3.DatePicker
import androidx.compose.material3.DatePickerDialog
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.SegmentedButton
import androidx.compose.material3.SegmentedButtonDefaults
import androidx.compose.material3.SingleChoiceSegmentedButtonRow
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.rememberDatePickerState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalConfiguration
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 androidx.core.os.LocaleListCompat
import fr.stahe.exemples.R
import fr.stahe.exemples.commun.PageExemple
import fr.stahe.exemples.commun.ThemeExemples
import java.text.NumberFormat
import java.time.LocalDate
import java.time.format.DateTimeFormatter
import java.time.format.FormatStyle
import java.util.Currency
val langues = listOf("fr", "en")
// ce que renverrait le serveur RdvMedecins (cf. chapitre sur la boîte noire)
data class ReponseServeur(val statusCode: Int, val cle: String, val params: Map<String, String>)
val reponsesServeur = listOf(
ReponseServeur(409, "ERRORS.LOGIN_TAKEN", emptyMap()),
ReponseServeur(429, "ERRORS.TOO_MANY_ATTEMPTS", mapOf("minutes" to "15")),
)
// la clé du serveur -> la ressource qui la traduit
val clesServeur = mapOf(
"ERRORS.LOGIN_TAKEN" to R.string.errors_login_taken,
"ERRORS.TOO_MANY_ATTEMPTS" to R.string.errors_too_many_attempts,
)
class Ex21Activity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent { ThemeExemples { PageTraduite() } }
}
}
// choisir la langue de l'application : Android la mémorise, et recrée l'activité
fun changerLangue(langue: String) {
AppCompatDelegate.setApplicationLocales(LocaleListCompat.forLanguageTags(langue))
}
@Composable
fun PageTraduite() {
// rememberSaveable : le nombre survit à la recréation de l'activité
var nombre by rememberSaveable { mutableIntStateOf(2) }
var calendrierOuvert by rememberSaveable { mutableStateOf(false) }
// la langue COURANTE : celle de la configuration de l'activité
val locale = LocalConfiguration.current.locales[0]
// des formateurs dans la langue courante
val date = LocalDate.now().format(DateTimeFormatter.ofLocalizedDate(FormatStyle.FULL).withLocale(locale))
val prix = NumberFormat.getCurrencyInstance(locale).apply { currency = Currency.getInstance("EUR") }.format(1234.5)
PageExemple(
titre = stringResource(R.string.ex21_titre),
// les langues : des boutons "segmentés", dans la barre du haut
actions = {
SingleChoiceSegmentedButtonRow(Modifier.padding(end = 8.dp).width(120.dp)) {
langues.forEachIndexed { i, l ->
SegmentedButton(
selected = locale.language == l,
onClick = { changerLangue(l) },
shape = SegmentedButtonDefaults.itemShape(i, langues.size),
icon = {},
) { Text(l.uppercase()) }
}
}
},
) {
Card(Modifier.fillMaxWidth()) {
Column(Modifier.padding(12.dp)) {
Text(stringResource(R.string.ex21_bonjour, "Marie"))
Text(stringResource(R.string.ex21_aujourdhui, date))
Text(stringResource(R.string.ex21_prix, prix))
Spacer(Modifier.height(8.dp))
// le pluriel : la catégorie "zero" n'existe ni en français ni en
// anglais (0 est "one" en français, "other" en anglais) : on
// traite 0 à part
Text(
if (nombre == 0) stringResource(R.string.ex21_aucun_rdv) else pluralStringResource(R.plurals.ex21_rendez_vous, nombre, nombre),
fontWeight = FontWeight.Bold,
)
FlowRow(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
TextButton(onClick = { nombre++ }) { Text(stringResource(R.string.ex21_ajouter)) }
TextButton(onClick = { nombre-- }, enabled = nombre > 0) { Text(stringResource(R.string.ex21_retirer)) }
}
// le calendrier de Material : ses textes suivent la langue de l'application
OutlinedButton(onClick = { calendrierOuvert = true }) {
Icon(Icons.Default.DateRange, contentDescription = null)
Spacer(Modifier.width(8.dp))
Text(stringResource(R.string.ex21_calendrier))
}
}
}
Card(Modifier.fillMaxWidth()) {
Column(Modifier.padding(12.dp)) {
Text(stringResource(R.string.ex21_serveur_titre), style = MaterialTheme.typography.titleSmall)
Text(stringResource(R.string.ex21_serveur_explication))
Spacer(Modifier.height(8.dp))
for (r in reponsesServeur) {
Text("${r.statusCode} ${r.cle}", fontFamily = FontFamily.Monospace, fontSize = 12.sp)
// la ressource de la clé ; ses paramètres, dans l'ordre (*: "étale" le tableau)
val id = clesServeur[r.cle]
Text("→ " + if (id == null) r.cle else stringResource(id, *r.params.values.toTypedArray()))
Spacer(Modifier.height(6.dp))
}
}
}
}
if (calendrierOuvert) {
val etat = rememberDatePickerState()
DatePickerDialog(
onDismissRequest = { calendrierOuvert = false },
// les textes OK / Annuler d'Android lui-même, dans la langue de l'application
confirmButton = { TextButton(onClick = { calendrierOuvert = false }) { Text(stringResource(android.R.string.ok)) } },
dismissButton = { TextButton(onClick = { calendrierOuvert = false }) { Text(stringResource(android.R.string.cancel)) } },
) {
DatePicker(state = etat)
}
}
}