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:
Bruno Deanoz 2026-06-21 15:02:16 +02:00
parent 94c0f85fb7
commit 0237eae1e6
7 changed files with 269 additions and 65 deletions

View file

@ -52,6 +52,8 @@ import dev.kaizen.app.net.SessionViewModel
import dev.kaizen.app.ui.theme.Amber
import dev.kaizen.app.ui.theme.AmberDeep
import dev.kaizen.app.ui.theme.OnAmber
import androidx.compose.ui.res.stringResource
import dev.kaizen.app.R
import kotlinx.coroutines.launch
/**
@ -64,6 +66,7 @@ import kotlinx.coroutines.launch
fun LoginScreen(session: SessionViewModel, modifier: Modifier = Modifier) {
val cs = MaterialTheme.colorScheme
val scope = rememberCoroutineScope()
val ctx = androidx.compose.ui.platform.LocalContext.current
var baseUrl by remember { mutableStateOf(DEFAULT_BASE_URL) }
var email by remember { mutableStateOf("") }
@ -81,9 +84,9 @@ fun LoginScreen(session: SessionViewModel, modifier: Modifier = Modifier) {
scope.launch {
when (val result = session.login(baseUrl, email, password, deviceName)) {
is LoginResult.Success -> Unit // session.config flips → MainActivity routes to chat
LoginResult.InvalidCredentials -> error = "E-Mail oder Passwort ist falsch."
LoginResult.RateLimited -> error = "Zu viele Versuche. Bitte kurz warten."
is LoginResult.Error -> error = "Verbindung fehlgeschlagen. Server-Adresse prüfen."
LoginResult.InvalidCredentials -> error = ctx.getString(R.string.login_error_credentials)
LoginResult.RateLimited -> error = ctx.getString(R.string.login_error_rate_limited)
is LoginResult.Error -> error = ctx.getString(R.string.login_error_connection)
}
loading = false
}
@ -102,10 +105,10 @@ fun LoginScreen(session: SessionViewModel, modifier: Modifier = Modifier) {
Spacer(Modifier.size(64.dp))
KaizenOrb(96.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))
Text(
"Melde dich bei deiner Kaizen-Instanz an",
stringResource(R.string.login_subtitle),
color = cs.onSurfaceVariant.copy(alpha = 0.8f),
fontSize = 14.sp,
)
@ -115,7 +118,7 @@ fun LoginScreen(session: SessionViewModel, modifier: Modifier = Modifier) {
GlassField(
value = baseUrl,
onValueChange = { baseUrl = it; error = null },
placeholder = "https://deine-instanz.de",
placeholder = stringResource(R.string.login_url_placeholder),
keyboardType = KeyboardType.Uri,
imeAction = ImeAction.Next,
)
@ -123,7 +126,7 @@ fun LoginScreen(session: SessionViewModel, modifier: Modifier = Modifier) {
GlassField(
value = email,
onValueChange = { email = it; error = null },
placeholder = "E-Mail",
placeholder = stringResource(R.string.login_email_placeholder),
keyboardType = KeyboardType.Email,
imeAction = ImeAction.Next,
)
@ -131,7 +134,7 @@ fun LoginScreen(session: SessionViewModel, modifier: Modifier = Modifier) {
GlassField(
value = password,
onValueChange = { password = it; error = null },
placeholder = "Passwort",
placeholder = stringResource(R.string.login_password_placeholder),
keyboardType = KeyboardType.Password,
imeAction = ImeAction.Done,
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))
} else {
Text(
"Anmelden",
stringResource(R.string.login_submit),
color = if (enabled) OnAmber else cs.onSurfaceVariant,
fontSize = 16.sp,
fontWeight = FontWeight.SemiBold,

View file

@ -67,6 +67,8 @@ import dev.kaizen.app.ui.shape.KaizenShapes
import dev.kaizen.app.ui.theme.Amber
import dev.kaizen.app.ui.theme.AmberDeep
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.format.DateTimeFormatter
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))
}
// DESIGN.md §11.1: no suggestion chips on mobile — redundant with mode pills
Column(
modifier = modifier
.fillMaxSize()
@ -135,7 +136,7 @@ fun EmptyHero(userName: String, onPick: (String) -> Unit, modifier: Modifier = M
Spacer(Modifier.height(24.dp))
Text("${greeting(now.hour)}, $userName", color = cs.onSurfaceVariant, fontSize = 17.sp)
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))
Text(dateLine, color = cs.onSurfaceVariant.copy(alpha = 0.7f), fontSize = 13.sp)
Spacer(Modifier.height(142.dp))
@ -143,7 +144,7 @@ fun EmptyHero(userName: String, onPick: (String) -> Unit, modifier: Modifier = M
}
@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 isDark = isSystemInDarkTheme()
@ -163,7 +164,8 @@ fun ModePillsRow(selected: String, onSelect: (String) -> Unit, modifier: Modifie
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
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)
else if (isDark) Color(0x12FFFFFF) else Color(0x82FFFFFF)
val borderBrush = if (active) glassBorderActive else glassBorderInactive
@ -173,13 +175,13 @@ fun ModePillsRow(selected: String, onSelect: (String) -> Unit, modifier: Modifie
.clip(KaizenShapes.pill)
.background(backgroundFill)
.border(1.2.dp, borderBrush, KaizenShapes.pill)
.clickable { onSelect(mode.label) }
.clickable { onSelect(mode.labelRes) }
.padding(horizontal = 13.dp, vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Icon(mode.icon, null, tint = if (active) Amber else cs.onSurfaceVariant, modifier = Modifier.size(16.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) {
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) {
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(
value = value, onValueChange = onValueChange,
textStyle = TextStyle(color = cs.onBackground, fontSize = 16.sp, lineHeight = 22.sp),

View file

@ -8,7 +8,10 @@ import androidx.compose.material.icons.rounded.Language
import androidx.compose.material.icons.rounded.Movie
import androidx.compose.material.icons.rounded.MusicNote
import androidx.compose.material.icons.rounded.Tune
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import dev.kaizen.app.R
import dev.kaizen.app.net.Attachment
enum class Role { User, Assistant }
@ -23,31 +26,30 @@ data class Message(
val attachments: List<Attachment> = emptyList(),
)
// Empty-state suggestion pills (mirrors the web hero)
data class Suggestion(val label: String, val prompt: String, val icon: ImageVector)
data class Suggestion(val labelRes: Int, val promptRes: Int, val icon: ImageVector)
val suggestions = listOf(
Suggestion("Brainstormen", "Lass uns zu einem Thema brainstormen.", Icons.Rounded.AutoAwesome),
Suggestion("Bild generieren", "Generiere ein Bild von einem Bergsee bei Sonnenaufgang.", Icons.Rounded.Image),
Suggestion("Video erstellen", "Erstelle ein kurzes Video von einer Stadt bei Nacht.", Icons.Rounded.Movie),
Suggestion("Web durchsuchen", "Durchsuche das Web nach den neuesten KI-Nachrichten.", Icons.Rounded.Language),
Suggestion(R.string.suggest_brainstorm, R.string.suggest_brainstorm_prompt, Icons.Rounded.AutoAwesome),
Suggestion(R.string.suggest_image, R.string.suggest_image_prompt, Icons.Rounded.Image),
Suggestion(R.string.suggest_video, R.string.suggest_video_prompt, Icons.Rounded.Movie),
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 label: String, val icon: ImageVector)
data class ChatMode(val labelRes: Int, val icon: ImageVector)
val chatModes = listOf(
ChatMode("Standard", Icons.Rounded.Adjust),
ChatMode("Sampling", Icons.Rounded.Tune),
ChatMode("Bild", Icons.Rounded.Image),
ChatMode("Suche", Icons.Rounded.Language),
ChatMode("Video", Icons.Rounded.Movie),
ChatMode("Audio", Icons.Rounded.MusicNote),
ChatMode(R.string.mode_standard, Icons.Rounded.Adjust),
ChatMode(R.string.mode_sampling, Icons.Rounded.Tune),
ChatMode(R.string.mode_image, Icons.Rounded.Image),
ChatMode(R.string.mode_search, Icons.Rounded.Language),
ChatMode(R.string.mode_video, Icons.Rounded.Movie),
ChatMode(R.string.mode_audio, Icons.Rounded.MusicNote),
)
@Composable
fun greeting(hour: Int): String = when (hour) {
in 5..11 -> "Guten Morgen"
in 12..17 -> "Guten Tag"
in 18..22 -> "Guten Abend"
else -> "Hallo"
in 5..11 -> stringResource(R.string.greeting_morning)
in 12..17 -> stringResource(R.string.greeting_day)
in 18..22 -> stringResource(R.string.greeting_evening)
else -> stringResource(R.string.greeting_night)
}

View file

@ -54,6 +54,7 @@ import dev.kaizen.app.ui.effect.GlassSurface
import dev.kaizen.app.ui.effect.GlassTiers
import dev.kaizen.app.ui.effect.KaizenShadows
import dev.kaizen.app.ui.shape.KaizenShapes
import dev.kaizen.app.R
import dev.kaizen.app.db.MessageEntity
import dev.kaizen.app.db.toEntity
import dev.kaizen.app.haptics.rememberHaptics
@ -93,7 +94,7 @@ fun ChatScreen(
var input by remember { mutableStateOf("") }
var isStreaming by remember { mutableStateOf(false) }
var nextId by remember { mutableStateOf(0L) }
var selectedMode by remember { mutableStateOf("Standard") }
var selectedMode by remember { mutableStateOf(R.string.mode_standard) }
// Navigation State
var currentScreen by remember { mutableStateOf(AppScreen.Chat) }
@ -315,11 +316,11 @@ fun ChatScreen(
val idx = messages.indexOfLast { it.id == assistantId }
if (idx >= 0) {
val existing = messages[idx].content
val errorSuffix = "\n\n${chatErrorText(e)}"
val errorSuffix = "\n\n${chatErrorText(e, context)}"
messages[idx] = messages[idx].copy(
streaming = false,
thinking = false,
content = if (existing.isNotBlank()) existing + errorSuffix else chatErrorText(e),
content = if (existing.isNotBlank()) existing + errorSuffix else chatErrorText(e, context),
)
}
} finally {
@ -437,7 +438,7 @@ fun ChatScreen(
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Icon(
imageVector = Icons.Rounded.Menu,
contentDescription = "Menü öffnen",
contentDescription = context.getString(R.string.chat_open_menu),
tint = MaterialTheme.colorScheme.onBackground,
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): String = when {
e is ChatHttpException && e.code == 401 -> "Sitzung abgelaufen. Bitte erneut anmelden."
e is ChatHttpException && e.code == 402 -> "Guthaben aufgebraucht."
e is ChatHttpException && e.code == 429 -> "Zu viele Anfragen. Kurz warten."
e is ChatHttpException -> "Fehler vom Server (${e.code})."
else -> "Keine Verbindung zum Server."
private fun chatErrorText(e: Throwable, ctx: android.content.Context): String = when {
e is ChatHttpException && e.code == 401 -> ctx.getString(R.string.error_session_expired)
e is ChatHttpException && e.code == 402 -> ctx.getString(R.string.error_credits_exhausted)
e is ChatHttpException && e.code == 429 -> ctx.getString(R.string.error_rate_limited)
e is ChatHttpException -> ctx.getString(R.string.error_server, e.code)
else -> ctx.getString(R.string.error_no_connection)
}

View file

@ -45,29 +45,33 @@ import dev.kaizen.app.ui.effect.GlassSurface
import dev.kaizen.app.ui.effect.GlassTiers
import dev.kaizen.app.ui.effect.KaizenShadows
import dev.kaizen.app.ui.shape.KaizenShapes
import dev.kaizen.app.R
import dev.kaizen.app.ui.theme.Amber
import androidx.compose.ui.res.stringResource
import java.time.Instant
import java.time.LocalDate
import java.time.ZoneId
import java.time.format.DateTimeFormatter
import java.util.Locale
private fun dateLabel(updatedAt: String?): String {
val instant = runCatching { Instant.parse(updatedAt) }.getOrNull() ?: return "FRÜHER"
private data class DateLabels(val today: String, val yesterday: String, val earlier: String, val pinned: String)
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 today = LocalDate.now()
return when (date) {
today -> "HEUTE"
today.minusDays(1) -> "GESTERN"
else -> date.format(DateTimeFormatter.ofPattern("dd. MMM yyyy", Locale.GERMAN)).uppercase()
today -> labels.today
today.minusDays(1) -> labels.yesterday
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>>()
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 {
groups.getOrPut(dateLabel(it.updatedAt)) { mutableListOf() }.add(it)
groups.getOrPut(dateLabel(it.updatedAt, labels)) { mutableListOf() }.add(it)
}
return groups
}
@ -109,7 +113,7 @@ fun KaizenSidebar(
horizontalArrangement = Arrangement.SpaceBetween,
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(
modifier = Modifier
.size(36.dp)
@ -118,7 +122,7 @@ fun KaizenSidebar(
.clickable { onNewChat() },
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))
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))
@ -145,13 +149,19 @@ fun KaizenSidebar(
Box(Modifier.weight(1f).fillMaxWidth()) {
if (conversations.isEmpty()) {
Text(
"Noch keine Chats",
stringResource(R.string.chat_no_conversations),
color = cs.onSurfaceVariant.copy(alpha = 0.5f),
fontSize = 14.sp,
modifier = Modifier.padding(start = 12.dp, top = 12.dp)
)
} 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(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.spacedBy(8.dp)
@ -207,7 +217,7 @@ fun KaizenSidebar(
Column(Modifier.weight(1f)) {
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))
@ -221,9 +231,9 @@ fun KaizenSidebar(
.padding(horizontal = 12.dp, vertical = 10.dp),
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))
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))
}
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,
fontSize = 14.sp,
fontWeight = FontWeight.Medium,
@ -269,7 +279,7 @@ private fun HistoryItemRow(item: ConversationSummary, selected: Boolean, onClick
)
if (item.locked) {
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))
}
}
}

View 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 &amp; ACCOUNT</string>
<string name="settings_section_ai">AI PROFILE &amp; 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 &amp; Facts</string>
<string name="settings_appearance_light">Light</string>
<string name="settings_appearance_dark">Dark</string>
<string name="settings_appearance_system">System</string>
</resources>

View file

@ -1,3 +1,96 @@
<resources>
<string name="app_name">Kaizen</string>
</resources>
<!-- 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 &amp; KONTO</string>
<string name="settings_section_ai">KI-PROFIL &amp; 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 &amp; Fakten</string>
<string name="settings_appearance_light">Hell</string>
<string name="settings_appearance_dark">Dunkel</string>
<string name="settings_appearance_system">System</string>
</resources>