feat: i18n — extract all UI strings to resources (de + en)
German (default) in values/strings.xml, English in values-en/strings.xml. Matches web app's next-intl de/en setup. Migrated: greeting, hero headline, chat input, error messages, mode pills, suggestion pills, sidebar (brand, search, dates, logout, settings), model sheet, login screen (welcome, fields, errors, submit button), conversation labels (locked, pinned). ChatModels: Suggestion/ChatMode now use resource IDs instead of hardcoded strings. greeting() is now @Composable (uses stringResource). ModePillsRow: selected state uses resource ID (Int) instead of String. Date labels (HEUTE/GESTERN/FRÜHER) localized via DateLabels data class.
This commit is contained in:
parent
94c0f85fb7
commit
0237eae1e6
7 changed files with 269 additions and 65 deletions
|
|
@ -52,6 +52,8 @@ import dev.kaizen.app.net.SessionViewModel
|
||||||
import dev.kaizen.app.ui.theme.Amber
|
import dev.kaizen.app.ui.theme.Amber
|
||||||
import dev.kaizen.app.ui.theme.AmberDeep
|
import dev.kaizen.app.ui.theme.AmberDeep
|
||||||
import dev.kaizen.app.ui.theme.OnAmber
|
import dev.kaizen.app.ui.theme.OnAmber
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import dev.kaizen.app.R
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -64,6 +66,7 @@ import kotlinx.coroutines.launch
|
||||||
fun LoginScreen(session: SessionViewModel, modifier: Modifier = Modifier) {
|
fun LoginScreen(session: SessionViewModel, modifier: Modifier = Modifier) {
|
||||||
val cs = MaterialTheme.colorScheme
|
val cs = MaterialTheme.colorScheme
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
|
val ctx = androidx.compose.ui.platform.LocalContext.current
|
||||||
|
|
||||||
var baseUrl by remember { mutableStateOf(DEFAULT_BASE_URL) }
|
var baseUrl by remember { mutableStateOf(DEFAULT_BASE_URL) }
|
||||||
var email by remember { mutableStateOf("") }
|
var email by remember { mutableStateOf("") }
|
||||||
|
|
@ -81,9 +84,9 @@ fun LoginScreen(session: SessionViewModel, modifier: Modifier = Modifier) {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
when (val result = session.login(baseUrl, email, password, deviceName)) {
|
when (val result = session.login(baseUrl, email, password, deviceName)) {
|
||||||
is LoginResult.Success -> Unit // session.config flips → MainActivity routes to chat
|
is LoginResult.Success -> Unit // session.config flips → MainActivity routes to chat
|
||||||
LoginResult.InvalidCredentials -> error = "E-Mail oder Passwort ist falsch."
|
LoginResult.InvalidCredentials -> error = ctx.getString(R.string.login_error_credentials)
|
||||||
LoginResult.RateLimited -> error = "Zu viele Versuche. Bitte kurz warten."
|
LoginResult.RateLimited -> error = ctx.getString(R.string.login_error_rate_limited)
|
||||||
is LoginResult.Error -> error = "Verbindung fehlgeschlagen. Server-Adresse prüfen."
|
is LoginResult.Error -> error = ctx.getString(R.string.login_error_connection)
|
||||||
}
|
}
|
||||||
loading = false
|
loading = false
|
||||||
}
|
}
|
||||||
|
|
@ -102,10 +105,10 @@ fun LoginScreen(session: SessionViewModel, modifier: Modifier = Modifier) {
|
||||||
Spacer(Modifier.size(64.dp))
|
Spacer(Modifier.size(64.dp))
|
||||||
KaizenOrb(96.dp)
|
KaizenOrb(96.dp)
|
||||||
Spacer(Modifier.size(24.dp))
|
Spacer(Modifier.size(24.dp))
|
||||||
Text("Willkommen", color = cs.onBackground, fontSize = 30.sp, fontWeight = FontWeight.Bold)
|
Text(stringResource(R.string.login_welcome), color = cs.onBackground, fontSize = 30.sp, fontWeight = FontWeight.Bold)
|
||||||
Spacer(Modifier.size(6.dp))
|
Spacer(Modifier.size(6.dp))
|
||||||
Text(
|
Text(
|
||||||
"Melde dich bei deiner Kaizen-Instanz an",
|
stringResource(R.string.login_subtitle),
|
||||||
color = cs.onSurfaceVariant.copy(alpha = 0.8f),
|
color = cs.onSurfaceVariant.copy(alpha = 0.8f),
|
||||||
fontSize = 14.sp,
|
fontSize = 14.sp,
|
||||||
)
|
)
|
||||||
|
|
@ -115,7 +118,7 @@ fun LoginScreen(session: SessionViewModel, modifier: Modifier = Modifier) {
|
||||||
GlassField(
|
GlassField(
|
||||||
value = baseUrl,
|
value = baseUrl,
|
||||||
onValueChange = { baseUrl = it; error = null },
|
onValueChange = { baseUrl = it; error = null },
|
||||||
placeholder = "https://deine-instanz.de",
|
placeholder = stringResource(R.string.login_url_placeholder),
|
||||||
keyboardType = KeyboardType.Uri,
|
keyboardType = KeyboardType.Uri,
|
||||||
imeAction = ImeAction.Next,
|
imeAction = ImeAction.Next,
|
||||||
)
|
)
|
||||||
|
|
@ -123,7 +126,7 @@ fun LoginScreen(session: SessionViewModel, modifier: Modifier = Modifier) {
|
||||||
GlassField(
|
GlassField(
|
||||||
value = email,
|
value = email,
|
||||||
onValueChange = { email = it; error = null },
|
onValueChange = { email = it; error = null },
|
||||||
placeholder = "E-Mail",
|
placeholder = stringResource(R.string.login_email_placeholder),
|
||||||
keyboardType = KeyboardType.Email,
|
keyboardType = KeyboardType.Email,
|
||||||
imeAction = ImeAction.Next,
|
imeAction = ImeAction.Next,
|
||||||
)
|
)
|
||||||
|
|
@ -131,7 +134,7 @@ fun LoginScreen(session: SessionViewModel, modifier: Modifier = Modifier) {
|
||||||
GlassField(
|
GlassField(
|
||||||
value = password,
|
value = password,
|
||||||
onValueChange = { password = it; error = null },
|
onValueChange = { password = it; error = null },
|
||||||
placeholder = "Passwort",
|
placeholder = stringResource(R.string.login_password_placeholder),
|
||||||
keyboardType = KeyboardType.Password,
|
keyboardType = KeyboardType.Password,
|
||||||
imeAction = ImeAction.Done,
|
imeAction = ImeAction.Done,
|
||||||
password = true,
|
password = true,
|
||||||
|
|
@ -227,7 +230,7 @@ private fun SignInButton(enabled: Boolean, loading: Boolean, onClick: () -> Unit
|
||||||
CircularProgressIndicator(color = OnAmber, strokeWidth = 2.4.dp, modifier = Modifier.size(22.dp))
|
CircularProgressIndicator(color = OnAmber, strokeWidth = 2.4.dp, modifier = Modifier.size(22.dp))
|
||||||
} else {
|
} else {
|
||||||
Text(
|
Text(
|
||||||
"Anmelden",
|
stringResource(R.string.login_submit),
|
||||||
color = if (enabled) OnAmber else cs.onSurfaceVariant,
|
color = if (enabled) OnAmber else cs.onSurfaceVariant,
|
||||||
fontSize = 16.sp,
|
fontSize = 16.sp,
|
||||||
fontWeight = FontWeight.SemiBold,
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
|
|
||||||
|
|
@ -67,6 +67,8 @@ import dev.kaizen.app.ui.shape.KaizenShapes
|
||||||
import dev.kaizen.app.ui.theme.Amber
|
import dev.kaizen.app.ui.theme.Amber
|
||||||
import dev.kaizen.app.ui.theme.AmberDeep
|
import dev.kaizen.app.ui.theme.AmberDeep
|
||||||
import dev.kaizen.app.ui.theme.OnAmber
|
import dev.kaizen.app.ui.theme.OnAmber
|
||||||
|
import dev.kaizen.app.R
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
import java.time.LocalDateTime
|
import java.time.LocalDateTime
|
||||||
import java.time.format.DateTimeFormatter
|
import java.time.format.DateTimeFormatter
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
|
@ -122,7 +124,6 @@ fun EmptyHero(userName: String, onPick: (String) -> Unit, modifier: Modifier = M
|
||||||
now.format(DateTimeFormatter.ofPattern("EEE, d. MMMM · HH:mm", Locale.GERMAN))
|
now.format(DateTimeFormatter.ofPattern("EEE, d. MMMM · HH:mm", Locale.GERMAN))
|
||||||
}
|
}
|
||||||
|
|
||||||
// DESIGN.md §11.1: no suggestion chips on mobile — redundant with mode pills
|
|
||||||
Column(
|
Column(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
|
|
@ -135,7 +136,7 @@ fun EmptyHero(userName: String, onPick: (String) -> Unit, modifier: Modifier = M
|
||||||
Spacer(Modifier.height(24.dp))
|
Spacer(Modifier.height(24.dp))
|
||||||
Text("${greeting(now.hour)}, $userName", color = cs.onSurfaceVariant, fontSize = 17.sp)
|
Text("${greeting(now.hour)}, $userName", color = cs.onSurfaceVariant, fontSize = 17.sp)
|
||||||
Spacer(Modifier.height(6.dp))
|
Spacer(Modifier.height(6.dp))
|
||||||
Text("Was liegt an?", color = cs.onBackground, fontSize = 32.sp, fontWeight = FontWeight.Bold)
|
Text(stringResource(R.string.hero_headline), color = cs.onBackground, fontSize = 32.sp, fontWeight = FontWeight.Bold)
|
||||||
Spacer(Modifier.height(8.dp))
|
Spacer(Modifier.height(8.dp))
|
||||||
Text(dateLine, color = cs.onSurfaceVariant.copy(alpha = 0.7f), fontSize = 13.sp)
|
Text(dateLine, color = cs.onSurfaceVariant.copy(alpha = 0.7f), fontSize = 13.sp)
|
||||||
Spacer(Modifier.height(142.dp))
|
Spacer(Modifier.height(142.dp))
|
||||||
|
|
@ -143,7 +144,7 @@ fun EmptyHero(userName: String, onPick: (String) -> Unit, modifier: Modifier = M
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ModePillsRow(selected: String, onSelect: (String) -> Unit, modifier: Modifier = Modifier) {
|
fun ModePillsRow(selected: Int, onSelect: (Int) -> Unit, modifier: Modifier = Modifier) {
|
||||||
val cs = MaterialTheme.colorScheme
|
val cs = MaterialTheme.colorScheme
|
||||||
val isDark = isSystemInDarkTheme()
|
val isDark = isSystemInDarkTheme()
|
||||||
|
|
||||||
|
|
@ -163,7 +164,8 @@ fun ModePillsRow(selected: String, onSelect: (String) -> Unit, modifier: Modifie
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
) {
|
) {
|
||||||
chatModes.forEach { mode ->
|
chatModes.forEach { mode ->
|
||||||
val active = mode.label == selected
|
val label = stringResource(mode.labelRes)
|
||||||
|
val active = mode.labelRes == selected
|
||||||
val backgroundFill = if (active) Amber.copy(alpha = 0.16f)
|
val backgroundFill = if (active) Amber.copy(alpha = 0.16f)
|
||||||
else if (isDark) Color(0x12FFFFFF) else Color(0x82FFFFFF)
|
else if (isDark) Color(0x12FFFFFF) else Color(0x82FFFFFF)
|
||||||
val borderBrush = if (active) glassBorderActive else glassBorderInactive
|
val borderBrush = if (active) glassBorderActive else glassBorderInactive
|
||||||
|
|
@ -173,13 +175,13 @@ fun ModePillsRow(selected: String, onSelect: (String) -> Unit, modifier: Modifie
|
||||||
.clip(KaizenShapes.pill)
|
.clip(KaizenShapes.pill)
|
||||||
.background(backgroundFill)
|
.background(backgroundFill)
|
||||||
.border(1.2.dp, borderBrush, KaizenShapes.pill)
|
.border(1.2.dp, borderBrush, KaizenShapes.pill)
|
||||||
.clickable { onSelect(mode.label) }
|
.clickable { onSelect(mode.labelRes) }
|
||||||
.padding(horizontal = 13.dp, vertical = 8.dp),
|
.padding(horizontal = 13.dp, vertical = 8.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
Icon(mode.icon, null, tint = if (active) Amber else cs.onSurfaceVariant, modifier = Modifier.size(16.dp))
|
Icon(mode.icon, null, tint = if (active) Amber else cs.onSurfaceVariant, modifier = Modifier.size(16.dp))
|
||||||
Spacer(Modifier.width(6.dp))
|
Spacer(Modifier.width(6.dp))
|
||||||
Text(mode.label, color = if (active) cs.onBackground else cs.onSurfaceVariant, fontSize = 13.sp, fontWeight = FontWeight.Medium)
|
Text(label, color = if (active) cs.onBackground else cs.onSurfaceVariant, fontSize = 13.sp, fontWeight = FontWeight.Medium)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -296,10 +298,10 @@ fun ChatInput(
|
||||||
}
|
}
|
||||||
Row(Modifier.padding(horizontal = 8.dp, vertical = 8.dp), verticalAlignment = Alignment.CenterVertically) {
|
Row(Modifier.padding(horizontal = 8.dp, vertical = 8.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||||
Box(Modifier.size(40.dp).clip(KaizenShapes.circle).clickable(onClick = onAddClick), contentAlignment = Alignment.Center) {
|
Box(Modifier.size(40.dp).clip(KaizenShapes.circle).clickable(onClick = onAddClick), contentAlignment = Alignment.Center) {
|
||||||
Icon(Icons.Rounded.Add, "Anhang", tint = cs.onSurfaceVariant, modifier = Modifier.size(22.dp))
|
Icon(Icons.Rounded.Add, stringResource(R.string.chat_attachment), tint = cs.onSurfaceVariant, modifier = Modifier.size(22.dp))
|
||||||
}
|
}
|
||||||
Box(Modifier.weight(1f).heightIn(min = 36.dp).padding(horizontal = 4.dp), contentAlignment = Alignment.CenterStart) {
|
Box(Modifier.weight(1f).heightIn(min = 36.dp).padding(horizontal = 4.dp), contentAlignment = Alignment.CenterStart) {
|
||||||
if (value.isEmpty()) Text("Nachricht eingeben …", color = cs.onSurfaceVariant, fontSize = 16.sp)
|
if (value.isEmpty()) Text(stringResource(R.string.chat_input_placeholder), color = cs.onSurfaceVariant, fontSize = 16.sp)
|
||||||
BasicTextField(
|
BasicTextField(
|
||||||
value = value, onValueChange = onValueChange,
|
value = value, onValueChange = onValueChange,
|
||||||
textStyle = TextStyle(color = cs.onBackground, fontSize = 16.sp, lineHeight = 22.sp),
|
textStyle = TextStyle(color = cs.onBackground, fontSize = 16.sp, lineHeight = 22.sp),
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,10 @@ import androidx.compose.material.icons.rounded.Language
|
||||||
import androidx.compose.material.icons.rounded.Movie
|
import androidx.compose.material.icons.rounded.Movie
|
||||||
import androidx.compose.material.icons.rounded.MusicNote
|
import androidx.compose.material.icons.rounded.MusicNote
|
||||||
import androidx.compose.material.icons.rounded.Tune
|
import androidx.compose.material.icons.rounded.Tune
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import dev.kaizen.app.R
|
||||||
import dev.kaizen.app.net.Attachment
|
import dev.kaizen.app.net.Attachment
|
||||||
|
|
||||||
enum class Role { User, Assistant }
|
enum class Role { User, Assistant }
|
||||||
|
|
@ -23,31 +26,30 @@ data class Message(
|
||||||
val attachments: List<Attachment> = emptyList(),
|
val attachments: List<Attachment> = emptyList(),
|
||||||
)
|
)
|
||||||
|
|
||||||
// Empty-state suggestion pills (mirrors the web hero)
|
data class Suggestion(val labelRes: Int, val promptRes: Int, val icon: ImageVector)
|
||||||
data class Suggestion(val label: String, val prompt: String, val icon: ImageVector)
|
|
||||||
|
|
||||||
val suggestions = listOf(
|
val suggestions = listOf(
|
||||||
Suggestion("Brainstormen", "Lass uns zu einem Thema brainstormen.", Icons.Rounded.AutoAwesome),
|
Suggestion(R.string.suggest_brainstorm, R.string.suggest_brainstorm_prompt, Icons.Rounded.AutoAwesome),
|
||||||
Suggestion("Bild generieren", "Generiere ein Bild von einem Bergsee bei Sonnenaufgang.", Icons.Rounded.Image),
|
Suggestion(R.string.suggest_image, R.string.suggest_image_prompt, Icons.Rounded.Image),
|
||||||
Suggestion("Video erstellen", "Erstelle ein kurzes Video von einer Stadt bei Nacht.", Icons.Rounded.Movie),
|
Suggestion(R.string.suggest_video, R.string.suggest_video_prompt, Icons.Rounded.Movie),
|
||||||
Suggestion("Web durchsuchen", "Durchsuche das Web nach den neuesten KI-Nachrichten.", Icons.Rounded.Language),
|
Suggestion(R.string.suggest_web, R.string.suggest_web_prompt, Icons.Rounded.Language),
|
||||||
)
|
)
|
||||||
|
|
||||||
// Mode pills above the input (visual only in Phase 0)
|
data class ChatMode(val labelRes: Int, val icon: ImageVector)
|
||||||
data class ChatMode(val label: String, val icon: ImageVector)
|
|
||||||
|
|
||||||
val chatModes = listOf(
|
val chatModes = listOf(
|
||||||
ChatMode("Standard", Icons.Rounded.Adjust),
|
ChatMode(R.string.mode_standard, Icons.Rounded.Adjust),
|
||||||
ChatMode("Sampling", Icons.Rounded.Tune),
|
ChatMode(R.string.mode_sampling, Icons.Rounded.Tune),
|
||||||
ChatMode("Bild", Icons.Rounded.Image),
|
ChatMode(R.string.mode_image, Icons.Rounded.Image),
|
||||||
ChatMode("Suche", Icons.Rounded.Language),
|
ChatMode(R.string.mode_search, Icons.Rounded.Language),
|
||||||
ChatMode("Video", Icons.Rounded.Movie),
|
ChatMode(R.string.mode_video, Icons.Rounded.Movie),
|
||||||
ChatMode("Audio", Icons.Rounded.MusicNote),
|
ChatMode(R.string.mode_audio, Icons.Rounded.MusicNote),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@Composable
|
||||||
fun greeting(hour: Int): String = when (hour) {
|
fun greeting(hour: Int): String = when (hour) {
|
||||||
in 5..11 -> "Guten Morgen"
|
in 5..11 -> stringResource(R.string.greeting_morning)
|
||||||
in 12..17 -> "Guten Tag"
|
in 12..17 -> stringResource(R.string.greeting_day)
|
||||||
in 18..22 -> "Guten Abend"
|
in 18..22 -> stringResource(R.string.greeting_evening)
|
||||||
else -> "Hallo"
|
else -> stringResource(R.string.greeting_night)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,7 @@ import dev.kaizen.app.ui.effect.GlassSurface
|
||||||
import dev.kaizen.app.ui.effect.GlassTiers
|
import dev.kaizen.app.ui.effect.GlassTiers
|
||||||
import dev.kaizen.app.ui.effect.KaizenShadows
|
import dev.kaizen.app.ui.effect.KaizenShadows
|
||||||
import dev.kaizen.app.ui.shape.KaizenShapes
|
import dev.kaizen.app.ui.shape.KaizenShapes
|
||||||
|
import dev.kaizen.app.R
|
||||||
import dev.kaizen.app.db.MessageEntity
|
import dev.kaizen.app.db.MessageEntity
|
||||||
import dev.kaizen.app.db.toEntity
|
import dev.kaizen.app.db.toEntity
|
||||||
import dev.kaizen.app.haptics.rememberHaptics
|
import dev.kaizen.app.haptics.rememberHaptics
|
||||||
|
|
@ -93,7 +94,7 @@ fun ChatScreen(
|
||||||
var input by remember { mutableStateOf("") }
|
var input by remember { mutableStateOf("") }
|
||||||
var isStreaming by remember { mutableStateOf(false) }
|
var isStreaming by remember { mutableStateOf(false) }
|
||||||
var nextId by remember { mutableStateOf(0L) }
|
var nextId by remember { mutableStateOf(0L) }
|
||||||
var selectedMode by remember { mutableStateOf("Standard") }
|
var selectedMode by remember { mutableStateOf(R.string.mode_standard) }
|
||||||
|
|
||||||
// Navigation State
|
// Navigation State
|
||||||
var currentScreen by remember { mutableStateOf(AppScreen.Chat) }
|
var currentScreen by remember { mutableStateOf(AppScreen.Chat) }
|
||||||
|
|
@ -315,11 +316,11 @@ fun ChatScreen(
|
||||||
val idx = messages.indexOfLast { it.id == assistantId }
|
val idx = messages.indexOfLast { it.id == assistantId }
|
||||||
if (idx >= 0) {
|
if (idx >= 0) {
|
||||||
val existing = messages[idx].content
|
val existing = messages[idx].content
|
||||||
val errorSuffix = "\n\n⚠ ${chatErrorText(e)}"
|
val errorSuffix = "\n\n⚠ ${chatErrorText(e, context)}"
|
||||||
messages[idx] = messages[idx].copy(
|
messages[idx] = messages[idx].copy(
|
||||||
streaming = false,
|
streaming = false,
|
||||||
thinking = false,
|
thinking = false,
|
||||||
content = if (existing.isNotBlank()) existing + errorSuffix else chatErrorText(e),
|
content = if (existing.isNotBlank()) existing + errorSuffix else chatErrorText(e, context),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
|
|
@ -437,7 +438,7 @@ fun ChatScreen(
|
||||||
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Rounded.Menu,
|
imageVector = Icons.Rounded.Menu,
|
||||||
contentDescription = "Menü öffnen",
|
contentDescription = context.getString(R.string.chat_open_menu),
|
||||||
tint = MaterialTheme.colorScheme.onBackground,
|
tint = MaterialTheme.colorScheme.onBackground,
|
||||||
modifier = Modifier.size(20.dp)
|
modifier = Modifier.size(20.dp)
|
||||||
)
|
)
|
||||||
|
|
@ -511,11 +512,10 @@ fun ChatScreen(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Maps a streaming failure to a short, human German message shown in the bubble. */
|
private fun chatErrorText(e: Throwable, ctx: android.content.Context): String = when {
|
||||||
private fun chatErrorText(e: Throwable): String = when {
|
e is ChatHttpException && e.code == 401 -> ctx.getString(R.string.error_session_expired)
|
||||||
e is ChatHttpException && e.code == 401 -> "Sitzung abgelaufen. Bitte erneut anmelden."
|
e is ChatHttpException && e.code == 402 -> ctx.getString(R.string.error_credits_exhausted)
|
||||||
e is ChatHttpException && e.code == 402 -> "Guthaben aufgebraucht."
|
e is ChatHttpException && e.code == 429 -> ctx.getString(R.string.error_rate_limited)
|
||||||
e is ChatHttpException && e.code == 429 -> "Zu viele Anfragen. Kurz warten."
|
e is ChatHttpException -> ctx.getString(R.string.error_server, e.code)
|
||||||
e is ChatHttpException -> "Fehler vom Server (${e.code})."
|
else -> ctx.getString(R.string.error_no_connection)
|
||||||
else -> "Keine Verbindung zum Server."
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -45,29 +45,33 @@ import dev.kaizen.app.ui.effect.GlassSurface
|
||||||
import dev.kaizen.app.ui.effect.GlassTiers
|
import dev.kaizen.app.ui.effect.GlassTiers
|
||||||
import dev.kaizen.app.ui.effect.KaizenShadows
|
import dev.kaizen.app.ui.effect.KaizenShadows
|
||||||
import dev.kaizen.app.ui.shape.KaizenShapes
|
import dev.kaizen.app.ui.shape.KaizenShapes
|
||||||
|
import dev.kaizen.app.R
|
||||||
import dev.kaizen.app.ui.theme.Amber
|
import dev.kaizen.app.ui.theme.Amber
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import java.time.LocalDate
|
import java.time.LocalDate
|
||||||
import java.time.ZoneId
|
import java.time.ZoneId
|
||||||
import java.time.format.DateTimeFormatter
|
import java.time.format.DateTimeFormatter
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
|
||||||
private fun dateLabel(updatedAt: String?): String {
|
private data class DateLabels(val today: String, val yesterday: String, val earlier: String, val pinned: String)
|
||||||
val instant = runCatching { Instant.parse(updatedAt) }.getOrNull() ?: return "FRÜHER"
|
|
||||||
|
private fun dateLabel(updatedAt: String?, labels: DateLabels): String {
|
||||||
|
val instant = runCatching { Instant.parse(updatedAt) }.getOrNull() ?: return labels.earlier
|
||||||
val date = instant.atZone(ZoneId.systemDefault()).toLocalDate()
|
val date = instant.atZone(ZoneId.systemDefault()).toLocalDate()
|
||||||
val today = LocalDate.now()
|
val today = LocalDate.now()
|
||||||
return when (date) {
|
return when (date) {
|
||||||
today -> "HEUTE"
|
today -> labels.today
|
||||||
today.minusDays(1) -> "GESTERN"
|
today.minusDays(1) -> labels.yesterday
|
||||||
else -> date.format(DateTimeFormatter.ofPattern("dd. MMM yyyy", Locale.GERMAN)).uppercase()
|
else -> date.format(DateTimeFormatter.ofPattern("dd. MMM yyyy", Locale.getDefault())).uppercase()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun groupConversations(conversations: List<ConversationSummary>): Map<String, List<ConversationSummary>> {
|
private fun groupConversations(conversations: List<ConversationSummary>, labels: DateLabels): Map<String, List<ConversationSummary>> {
|
||||||
val groups = LinkedHashMap<String, MutableList<ConversationSummary>>()
|
val groups = LinkedHashMap<String, MutableList<ConversationSummary>>()
|
||||||
conversations.filter { it.pinned }.takeIf { it.isNotEmpty() }?.let { groups["★ ANGEHEFTET"] = it.toMutableList() }
|
conversations.filter { it.pinned }.takeIf { it.isNotEmpty() }?.let { groups[labels.pinned] = it.toMutableList() }
|
||||||
conversations.filterNot { it.pinned }.forEach {
|
conversations.filterNot { it.pinned }.forEach {
|
||||||
groups.getOrPut(dateLabel(it.updatedAt)) { mutableListOf() }.add(it)
|
groups.getOrPut(dateLabel(it.updatedAt, labels)) { mutableListOf() }.add(it)
|
||||||
}
|
}
|
||||||
return groups
|
return groups
|
||||||
}
|
}
|
||||||
|
|
@ -109,7 +113,7 @@ fun KaizenSidebar(
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
Text("kaizen", color = cs.onBackground, fontSize = 20.sp, fontWeight = FontWeight.Bold)
|
Text(stringResource(R.string.sidebar_brand), color = cs.onBackground, fontSize = 20.sp, fontWeight = FontWeight.Bold)
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(36.dp)
|
.size(36.dp)
|
||||||
|
|
@ -118,7 +122,7 @@ fun KaizenSidebar(
|
||||||
.clickable { onNewChat() },
|
.clickable { onNewChat() },
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
Icon(Icons.Rounded.Edit, "Neuer Chat", tint = cs.onBackground, modifier = Modifier.size(18.dp))
|
Icon(Icons.Rounded.Edit, stringResource(R.string.chat_new), tint = cs.onBackground, modifier = Modifier.size(18.dp))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -136,7 +140,7 @@ fun KaizenSidebar(
|
||||||
) {
|
) {
|
||||||
Icon(Icons.Rounded.Search, null, tint = cs.onSurfaceVariant.copy(alpha = 0.6f), modifier = Modifier.size(18.dp))
|
Icon(Icons.Rounded.Search, null, tint = cs.onSurfaceVariant.copy(alpha = 0.6f), modifier = Modifier.size(18.dp))
|
||||||
Spacer(Modifier.width(8.dp))
|
Spacer(Modifier.width(8.dp))
|
||||||
Text("Suchen...", color = cs.onSurfaceVariant.copy(alpha = 0.6f), fontSize = 14.sp)
|
Text(stringResource(R.string.chat_search), color = cs.onSurfaceVariant.copy(alpha = 0.6f), fontSize = 14.sp)
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(Modifier.height(24.dp))
|
Spacer(Modifier.height(24.dp))
|
||||||
|
|
@ -145,13 +149,19 @@ fun KaizenSidebar(
|
||||||
Box(Modifier.weight(1f).fillMaxWidth()) {
|
Box(Modifier.weight(1f).fillMaxWidth()) {
|
||||||
if (conversations.isEmpty()) {
|
if (conversations.isEmpty()) {
|
||||||
Text(
|
Text(
|
||||||
"Noch keine Chats",
|
stringResource(R.string.chat_no_conversations),
|
||||||
color = cs.onSurfaceVariant.copy(alpha = 0.5f),
|
color = cs.onSurfaceVariant.copy(alpha = 0.5f),
|
||||||
fontSize = 14.sp,
|
fontSize = 14.sp,
|
||||||
modifier = Modifier.padding(start = 12.dp, top = 12.dp)
|
modifier = Modifier.padding(start = 12.dp, top = 12.dp)
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
val grouped = remember(conversations) { groupConversations(conversations) }
|
val labels = DateLabels(
|
||||||
|
today = stringResource(R.string.sidebar_today),
|
||||||
|
yesterday = stringResource(R.string.sidebar_yesterday),
|
||||||
|
earlier = stringResource(R.string.sidebar_earlier),
|
||||||
|
pinned = stringResource(R.string.sidebar_pinned),
|
||||||
|
)
|
||||||
|
val grouped = remember(conversations, labels) { groupConversations(conversations, labels) }
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
|
@ -207,7 +217,7 @@ fun KaizenSidebar(
|
||||||
Column(Modifier.weight(1f)) {
|
Column(Modifier.weight(1f)) {
|
||||||
Text(userName, color = cs.onBackground, fontSize = 15.sp, fontWeight = FontWeight.SemiBold)
|
Text(userName, color = cs.onBackground, fontSize = 15.sp, fontWeight = FontWeight.SemiBold)
|
||||||
}
|
}
|
||||||
Icon(Icons.Rounded.Settings, "Einstellungen", tint = cs.onSurfaceVariant.copy(alpha = 0.7f), modifier = Modifier.size(20.dp))
|
Icon(Icons.Rounded.Settings, stringResource(R.string.sidebar_settings), tint = cs.onSurfaceVariant.copy(alpha = 0.7f), modifier = Modifier.size(20.dp))
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(Modifier.height(10.dp))
|
Spacer(Modifier.height(10.dp))
|
||||||
|
|
@ -221,9 +231,9 @@ fun KaizenSidebar(
|
||||||
.padding(horizontal = 12.dp, vertical = 10.dp),
|
.padding(horizontal = 12.dp, vertical = 10.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
Icon(Icons.AutoMirrored.Rounded.Logout, "Abmelden", tint = cs.onSurfaceVariant.copy(alpha = 0.7f), modifier = Modifier.size(18.dp))
|
Icon(Icons.AutoMirrored.Rounded.Logout, stringResource(R.string.sidebar_logout), tint = cs.onSurfaceVariant.copy(alpha = 0.7f), modifier = Modifier.size(18.dp))
|
||||||
Spacer(Modifier.width(10.dp))
|
Spacer(Modifier.width(10.dp))
|
||||||
Text("Abmelden", color = cs.onSurfaceVariant.copy(alpha = 0.85f), fontSize = 14.sp, fontWeight = FontWeight.Medium)
|
Text(stringResource(R.string.sidebar_logout), color = cs.onSurfaceVariant.copy(alpha = 0.85f), fontSize = 14.sp, fontWeight = FontWeight.Medium)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -260,7 +270,7 @@ private fun HistoryItemRow(item: ConversationSummary, selected: Boolean, onClick
|
||||||
Spacer(Modifier.width(8.dp))
|
Spacer(Modifier.width(8.dp))
|
||||||
}
|
}
|
||||||
Text(
|
Text(
|
||||||
text = if (item.locked) "Gesperrter Chat" else item.title,
|
text = if (item.locked) stringResource(R.string.chat_locked) else item.title,
|
||||||
color = if (item.locked) cs.onSurfaceVariant.copy(alpha = 0.5f) else cs.onBackground,
|
color = if (item.locked) cs.onSurfaceVariant.copy(alpha = 0.5f) else cs.onBackground,
|
||||||
fontSize = 14.sp,
|
fontSize = 14.sp,
|
||||||
fontWeight = FontWeight.Medium,
|
fontWeight = FontWeight.Medium,
|
||||||
|
|
@ -269,7 +279,7 @@ private fun HistoryItemRow(item: ConversationSummary, selected: Boolean, onClick
|
||||||
)
|
)
|
||||||
if (item.locked) {
|
if (item.locked) {
|
||||||
Spacer(Modifier.width(8.dp))
|
Spacer(Modifier.width(8.dp))
|
||||||
Icon(Icons.Rounded.Lock, "Gesperrt", tint = Amber, modifier = Modifier.size(15.dp))
|
Icon(Icons.Rounded.Lock, stringResource(R.string.chat_locked_badge), tint = Amber, modifier = Modifier.size(15.dp))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
94
app/src/main/res/values-en/strings.xml
Normal file
94
app/src/main/res/values-en/strings.xml
Normal file
|
|
@ -0,0 +1,94 @@
|
||||||
|
<resources>
|
||||||
|
<!-- Greeting -->
|
||||||
|
<string name="greeting_morning">Good Morning</string>
|
||||||
|
<string name="greeting_day">Good Afternoon</string>
|
||||||
|
<string name="greeting_evening">Good Evening</string>
|
||||||
|
<string name="greeting_night">Hello</string>
|
||||||
|
<string name="hero_headline">What\'s up?</string>
|
||||||
|
|
||||||
|
<!-- Chat -->
|
||||||
|
<string name="chat_input_placeholder">Type a message …</string>
|
||||||
|
<string name="chat_no_conversations">No chats yet</string>
|
||||||
|
<string name="chat_locked">Locked Chat</string>
|
||||||
|
<string name="chat_locked_badge">Locked</string>
|
||||||
|
<string name="chat_new">New Chat</string>
|
||||||
|
<string name="chat_search">Search…</string>
|
||||||
|
<string name="chat_attachment">Attachment</string>
|
||||||
|
<string name="chat_call">Call</string>
|
||||||
|
<string name="chat_remove">Remove</string>
|
||||||
|
<string name="chat_error">Error</string>
|
||||||
|
<string name="chat_open_menu">Open menu</string>
|
||||||
|
|
||||||
|
<!-- Chat errors -->
|
||||||
|
<string name="error_session_expired">Session expired. Please sign in again.</string>
|
||||||
|
<string name="error_credits_exhausted">Credits exhausted.</string>
|
||||||
|
<string name="error_rate_limited">Too many requests. Please wait.</string>
|
||||||
|
<string name="error_server">Server error (%1$d).</string>
|
||||||
|
<string name="error_no_connection">No connection to server.</string>
|
||||||
|
|
||||||
|
<!-- Mode pills -->
|
||||||
|
<string name="mode_standard">Standard</string>
|
||||||
|
<string name="mode_sampling">Sampling</string>
|
||||||
|
<string name="mode_image">Image</string>
|
||||||
|
<string name="mode_search">Search</string>
|
||||||
|
<string name="mode_video">Video</string>
|
||||||
|
<string name="mode_audio">Audio</string>
|
||||||
|
|
||||||
|
<!-- Suggestion pills -->
|
||||||
|
<string name="suggest_brainstorm">Brainstorm</string>
|
||||||
|
<string name="suggest_brainstorm_prompt">Let\'s brainstorm on a topic.</string>
|
||||||
|
<string name="suggest_image">Generate Image</string>
|
||||||
|
<string name="suggest_image_prompt">Generate an image of a mountain lake at sunrise.</string>
|
||||||
|
<string name="suggest_video">Create Video</string>
|
||||||
|
<string name="suggest_video_prompt">Create a short video of a city at night.</string>
|
||||||
|
<string name="suggest_web">Search the Web</string>
|
||||||
|
<string name="suggest_web_prompt">Search the web for the latest AI news.</string>
|
||||||
|
|
||||||
|
<!-- Sidebar -->
|
||||||
|
<string name="sidebar_brand">kaizen</string>
|
||||||
|
<string name="sidebar_pinned">★ PINNED</string>
|
||||||
|
<string name="sidebar_today">TODAY</string>
|
||||||
|
<string name="sidebar_yesterday">YESTERDAY</string>
|
||||||
|
<string name="sidebar_earlier">EARLIER</string>
|
||||||
|
<string name="sidebar_logout">Sign Out</string>
|
||||||
|
<string name="sidebar_settings">Settings</string>
|
||||||
|
|
||||||
|
<!-- Model sheet -->
|
||||||
|
<string name="model_sheet_title">Select Model</string>
|
||||||
|
<string name="model_search_placeholder">Search models…</string>
|
||||||
|
<string name="model_search_clear">Clear</string>
|
||||||
|
<string name="model_connecting">Connecting…</string>
|
||||||
|
<string name="model_no_results">No matches</string>
|
||||||
|
<string name="model_favorite">Favorite</string>
|
||||||
|
<string name="model_selected">Selected</string>
|
||||||
|
<string name="model_favorites_group">★ Favorites</string>
|
||||||
|
|
||||||
|
<!-- Login -->
|
||||||
|
<string name="login_welcome">Welcome</string>
|
||||||
|
<string name="login_subtitle">Sign in to your Kaizen instance</string>
|
||||||
|
<string name="login_url_placeholder">https://your-instance.com</string>
|
||||||
|
<string name="login_email_placeholder">Email</string>
|
||||||
|
<string name="login_password_placeholder">Password</string>
|
||||||
|
<string name="login_submit">Sign In</string>
|
||||||
|
<string name="login_error_credentials">Invalid email or password.</string>
|
||||||
|
<string name="login_error_rate_limited">Too many attempts. Please wait.</string>
|
||||||
|
<string name="login_error_connection">Connection failed. Check server address.</string>
|
||||||
|
|
||||||
|
<!-- Settings -->
|
||||||
|
<string name="settings_title">Settings</string>
|
||||||
|
<string name="settings_back">Back</string>
|
||||||
|
<string name="settings_section_appearance">APPEARANCE</string>
|
||||||
|
<string name="settings_section_profile">PROFILE & ACCOUNT</string>
|
||||||
|
<string name="settings_section_ai">AI PROFILE & CONTEXT</string>
|
||||||
|
<string name="settings_accent_title">Accent Color</string>
|
||||||
|
<string name="settings_accent_desc">Choose the app color scheme</string>
|
||||||
|
<string name="settings_appearance_title">Appearance</string>
|
||||||
|
<string name="settings_appearance_desc">Set light/dark appearance</string>
|
||||||
|
<string name="settings_name_title">Display Name</string>
|
||||||
|
<string name="settings_name_desc">Change your display name</string>
|
||||||
|
<string name="settings_email_title">Email Address</string>
|
||||||
|
<string name="settings_ai_title">System Prompt & Facts</string>
|
||||||
|
<string name="settings_appearance_light">Light</string>
|
||||||
|
<string name="settings_appearance_dark">Dark</string>
|
||||||
|
<string name="settings_appearance_system">System</string>
|
||||||
|
</resources>
|
||||||
|
|
@ -1,3 +1,96 @@
|
||||||
<resources>
|
<resources>
|
||||||
<string name="app_name">Kaizen</string>
|
<string name="app_name">Kaizen</string>
|
||||||
|
|
||||||
|
<!-- Greeting -->
|
||||||
|
<string name="greeting_morning">Guten Morgen</string>
|
||||||
|
<string name="greeting_day">Guten Tag</string>
|
||||||
|
<string name="greeting_evening">Guten Abend</string>
|
||||||
|
<string name="greeting_night">Hallo</string>
|
||||||
|
<string name="hero_headline">Was liegt an?</string>
|
||||||
|
|
||||||
|
<!-- Chat -->
|
||||||
|
<string name="chat_input_placeholder">Nachricht eingeben …</string>
|
||||||
|
<string name="chat_no_conversations">Noch keine Chats</string>
|
||||||
|
<string name="chat_locked">Gesperrter Chat</string>
|
||||||
|
<string name="chat_locked_badge">Gesperrt</string>
|
||||||
|
<string name="chat_new">Neuer Chat</string>
|
||||||
|
<string name="chat_search">Suchen…</string>
|
||||||
|
<string name="chat_attachment">Anhang</string>
|
||||||
|
<string name="chat_call">Anruf</string>
|
||||||
|
<string name="chat_remove">Entfernen</string>
|
||||||
|
<string name="chat_error">Fehler</string>
|
||||||
|
<string name="chat_open_menu">Menü öffnen</string>
|
||||||
|
|
||||||
|
<!-- Chat errors -->
|
||||||
|
<string name="error_session_expired">Sitzung abgelaufen. Bitte erneut anmelden.</string>
|
||||||
|
<string name="error_credits_exhausted">Guthaben aufgebraucht.</string>
|
||||||
|
<string name="error_rate_limited">Zu viele Anfragen. Kurz warten.</string>
|
||||||
|
<string name="error_server">Fehler vom Server (%1$d).</string>
|
||||||
|
<string name="error_no_connection">Keine Verbindung zum Server.</string>
|
||||||
|
|
||||||
|
<!-- Mode pills -->
|
||||||
|
<string name="mode_standard">Standard</string>
|
||||||
|
<string name="mode_sampling">Sampling</string>
|
||||||
|
<string name="mode_image">Bild</string>
|
||||||
|
<string name="mode_search">Suche</string>
|
||||||
|
<string name="mode_video">Video</string>
|
||||||
|
<string name="mode_audio">Audio</string>
|
||||||
|
|
||||||
|
<!-- Suggestion pills -->
|
||||||
|
<string name="suggest_brainstorm">Brainstormen</string>
|
||||||
|
<string name="suggest_brainstorm_prompt">Lass uns zu einem Thema brainstormen.</string>
|
||||||
|
<string name="suggest_image">Bild generieren</string>
|
||||||
|
<string name="suggest_image_prompt">Generiere ein Bild von einem Bergsee bei Sonnenaufgang.</string>
|
||||||
|
<string name="suggest_video">Video erstellen</string>
|
||||||
|
<string name="suggest_video_prompt">Erstelle ein kurzes Video von einer Stadt bei Nacht.</string>
|
||||||
|
<string name="suggest_web">Web durchsuchen</string>
|
||||||
|
<string name="suggest_web_prompt">Durchsuche das Web nach den neuesten KI-Nachrichten.</string>
|
||||||
|
|
||||||
|
<!-- Sidebar -->
|
||||||
|
<string name="sidebar_brand">kaizen</string>
|
||||||
|
<string name="sidebar_pinned">★ ANGEHEFTET</string>
|
||||||
|
<string name="sidebar_today">HEUTE</string>
|
||||||
|
<string name="sidebar_yesterday">GESTERN</string>
|
||||||
|
<string name="sidebar_earlier">FRÜHER</string>
|
||||||
|
<string name="sidebar_logout">Abmelden</string>
|
||||||
|
<string name="sidebar_settings">Einstellungen</string>
|
||||||
|
|
||||||
|
<!-- Model sheet -->
|
||||||
|
<string name="model_sheet_title">Modell auswählen</string>
|
||||||
|
<string name="model_search_placeholder">Modell suchen…</string>
|
||||||
|
<string name="model_search_clear">Löschen</string>
|
||||||
|
<string name="model_connecting">Verbindung wird hergestellt…</string>
|
||||||
|
<string name="model_no_results">Keine Treffer</string>
|
||||||
|
<string name="model_favorite">Favorit</string>
|
||||||
|
<string name="model_selected">Ausgewählt</string>
|
||||||
|
<string name="model_favorites_group">★ Favoriten</string>
|
||||||
|
|
||||||
|
<!-- Login -->
|
||||||
|
<string name="login_welcome">Willkommen</string>
|
||||||
|
<string name="login_subtitle">Melde dich bei deiner Kaizen-Instanz an</string>
|
||||||
|
<string name="login_url_placeholder">https://deine-instanz.de</string>
|
||||||
|
<string name="login_email_placeholder">E-Mail</string>
|
||||||
|
<string name="login_password_placeholder">Passwort</string>
|
||||||
|
<string name="login_submit">Anmelden</string>
|
||||||
|
<string name="login_error_credentials">E-Mail oder Passwort ist falsch.</string>
|
||||||
|
<string name="login_error_rate_limited">Zu viele Versuche. Bitte kurz warten.</string>
|
||||||
|
<string name="login_error_connection">Verbindung fehlgeschlagen. Server-Adresse prüfen.</string>
|
||||||
|
|
||||||
|
<!-- Settings -->
|
||||||
|
<string name="settings_title">Einstellungen</string>
|
||||||
|
<string name="settings_back">Zurück</string>
|
||||||
|
<string name="settings_section_appearance">ERSCHEINUNGSBILD</string>
|
||||||
|
<string name="settings_section_profile">PROFIL & KONTO</string>
|
||||||
|
<string name="settings_section_ai">KI-PROFIL & KONTEXT</string>
|
||||||
|
<string name="settings_accent_title">Akzentfarbe</string>
|
||||||
|
<string name="settings_accent_desc">Wähle das Farbschema der App</string>
|
||||||
|
<string name="settings_appearance_title">Helligkeitsmodus</string>
|
||||||
|
<string name="settings_appearance_desc">Passe das Hell/Dunkel-Erscheinungsbild an</string>
|
||||||
|
<string name="settings_name_title">Benutzername</string>
|
||||||
|
<string name="settings_name_desc">Ändere deinen Anzeigenamen</string>
|
||||||
|
<string name="settings_email_title">E-Mail-Adresse</string>
|
||||||
|
<string name="settings_ai_title">System-Prompt & Fakten</string>
|
||||||
|
<string name="settings_appearance_light">Hell</string>
|
||||||
|
<string name="settings_appearance_dark">Dunkel</string>
|
||||||
|
<string name="settings_appearance_system">System</string>
|
||||||
</resources>
|
</resources>
|
||||||
Loading…
Reference in a new issue