languagemanager update

This commit is contained in:
oxidiert
2025-09-18 12:02:47 +02:00
parent fe2b05c0fd
commit ac1fbb515d
5 changed files with 647 additions and 345 deletions

View File

@ -1,11 +1,5 @@
package com.dano.test1
import android.content.ContentValues
import android.media.MediaScannerConnection
import android.net.Uri
import android.os.Build
import android.os.Environment
import android.provider.MediaStore
import android.util.Log
import android.view.View
import android.widget.*
@ -13,25 +7,21 @@ import com.dano.test1.data.Client
import com.dano.test1.data.Question
import com.dano.test1.data.Questionnaire
import kotlinx.coroutines.*
import org.json.JSONArray
import java.io.File
import java.nio.charset.Charset
import kotlin.math.roundToInt
import org.apache.poi.ss.usermodel.Row
import org.apache.poi.xssf.usermodel.XSSFWorkbook
import org.json.JSONArray
class DatabaseButtonHandler(
private val activity: MainActivity,
private val databaseButton: Button,
private val onClose: () -> Unit,
// Aktuelle UI-Sprache für die Bildschirm-Anzeige (header-Tabelle)
private val languageIDProvider: () -> String = { "GERMAN" }
) {
private val uiScope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
private val tag = "DatabaseButtonHandler"
// Cache für geladene IDs aus assets/header_order.json oder header_order.xlsx
private var orderedIdsCache: List<String>? = null
// Services (wie bei dir)
private val headerRepo = HeaderOrderRepository(activity)
private val exporter = ExcelExportService(activity, headerRepo)
fun setup() {
databaseButton.text = "Datenbank"
@ -44,12 +34,21 @@ class DatabaseButtonHandler(
private fun openDatabaseScreen() {
activity.setContentView(R.layout.database_screen)
val lang = safeLang() // ✨
val titleTv: TextView = requireView(R.id.title, "title") // ✨
val table: TableLayout = requireView(R.id.tableClients, "tableClients")
val progress: ProgressBar = requireView(R.id.progressBar, "progressBar")
val emptyView: TextView = requireView(R.id.emptyView, "emptyView")
val backButton: Button = requireView(R.id.backButton, "backButton")
val btnDownloadHeader: Button = requireView(R.id.btnDownloadHeader, "btnDownloadHeader")
// ✨ statische Texte lokalisieren
titleTv.text = t(lang, "database_clients_title") ?: "Datenbank Clients"
emptyView.text = t(lang, "no_clients_available") ?: "Keine Clients vorhanden."
backButton.text = t(lang, "previous") ?: "Zurück"
btnDownloadHeader.text = t(lang, "download_header") ?: "Download Header"
backButton.setOnClickListener { onClose() }
btnDownloadHeader.setOnClickListener { onDownloadHeadersClicked(progress) }
@ -57,7 +56,8 @@ class DatabaseButtonHandler(
emptyView.visibility = View.GONE
table.removeAllViews()
addHeaderRow(table, listOf("#", "Client-Code"))
// ✨ Spaltenkopf lokalisieren
addHeaderRow(table, listOf("#", t(lang, "client_code") ?: "Client-Code"))
uiScope.launch {
val clients: List<Client> = withContext(Dispatchers.IO) {
@ -88,17 +88,18 @@ class DatabaseButtonHandler(
uiScope.launch {
try {
progress.visibility = View.VISIBLE
val savedUri = exportHeadersForAllClients() // -> speichert NUR in Downloads
val savedUri = exporter.exportHeadersForAllClients()
progress.visibility = View.GONE
val lang = safeLang() // ✨
if (savedUri != null) {
Toast.makeText(
activity,
"Export erfolgreich: Downloads/ClientHeaders.xlsx",
t(lang, "export_success_downloads") ?: "Export erfolgreich: Downloads/ClientHeaders.xlsx",
Toast.LENGTH_LONG
).show()
} else {
Toast.makeText(activity, "Export fehlgeschlagen.", Toast.LENGTH_LONG).show()
Toast.makeText(activity, t(lang, "export_failed") ?: "Export fehlgeschlagen.", Toast.LENGTH_LONG).show()
}
} catch (e: Exception) {
progress.visibility = View.GONE
@ -108,165 +109,27 @@ class DatabaseButtonHandler(
}
}
/**
* Erzeugt eine Excel-Datei (ClientHeaders.xlsx) mit:
* - Zeile 1: Spalten-IDs (header_order.json / header_order.xlsx)
* - Zeile 2: ENGLISCHE Fragen/Beschriftungen zu jeder ID (rein EN, keine IDs mehr)
* - Ab Zeile 3: pro Client die Werte ("Done"/"Not Done"/Antwort oder "None")
* - Speichert AUSSCHLIESSLICH in den öffentlichen Geräte-Ordner "Downloads"
*/
private suspend fun exportHeadersForAllClients(): Uri? {
val orderedIds = loadOrderedIds()
if (orderedIds.isEmpty()) return null
val clients = MyApp.database.clientDao().getAllClients()
val questionnaires = MyApp.database.questionnaireDao().getAll()
val questionnaireIdSet = questionnaires.map { it.id }.toSet()
val wb = XSSFWorkbook()
val sheet = wb.createSheet("Headers")
sheet.setColumnWidth(0, 8 * 256) // #
for (i in 1..orderedIds.size) sheet.setColumnWidth(i, 36 * 256)
// Zeile 1: IDs
var col = 0
val headerRow: Row = sheet.createRow(0)
headerRow.createCell(col++).setCellValue("#")
orderedIds.forEach { id -> headerRow.createCell(col++).setCellValue(id) }
// Zeile 2: Fragen/Titel konsequent auf EN
val questionRow: Row = sheet.createRow(1)
var qc = 0
questionRow.createCell(qc++).setCellValue("Question (EN)")
for (id in orderedIds) {
val englishQuestion = englishQuestionForId(id, questionnaireIdSet)
questionRow.createCell(qc++).setCellValue(englishQuestion)
}
// Ab Zeile 3: Werte je Client
clients.forEachIndexed { rowIdx, client ->
val row: Row = sheet.createRow(rowIdx + 2)
var c = 0
row.createCell(c++).setCellValue((rowIdx + 1).toDouble())
val completedForClient = MyApp.database.completedQuestionnaireDao().getAllForClient(client.clientCode)
val statusMap = completedForClient.associate { it.questionnaireId to it.isDone }
val answers = MyApp.database.answerDao().getAnswersForClient(client.clientCode)
val answerMap = answers.associate { it.questionId to it.answerValue }
orderedIds.forEach { id ->
val raw = when {
id == "client_code" -> client.clientCode
id in questionnaireIdSet -> if (statusMap[id] == true) "Done" else "Not Done"
else -> answerMap[id]?.takeIf { it.isNotBlank() } ?: "None"
}
val out = localizeForExportEn(id, raw) // Export immer EN
row.createCell(c++).setCellValue(out)
}
}
// Bytes erzeugen
val bytes = java.io.ByteArrayOutputStream().use { bos ->
wb.write(bos); bos.toByteArray()
}
wb.close()
// -> nur in Downloads speichern (Q+ via MediaStore, sonst Public Downloads)
return saveToDownloads(
filename = "ClientHeaders.xlsx",
mimeType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
bytes = bytes
)
}
/** Speichert Bytes nach "Downloads". */
private fun saveToDownloads(filename: String, mimeType: String, bytes: ByteArray): Uri? {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
// Android 10+ (Scoped Storage)
val resolver = activity.contentResolver
val values = ContentValues().apply {
put(MediaStore.MediaColumns.DISPLAY_NAME, filename)
put(MediaStore.MediaColumns.MIME_TYPE, mimeType)
put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS)
// optional: put(MediaStore.MediaColumns.IS_PENDING, 1)
}
val collection = MediaStore.Downloads.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
val uri = resolver.insert(collection, values)
if (uri != null) {
resolver.openOutputStream(uri)?.use { it.write(bytes) } ?: return null
// optional: values.clear(); values.put(MediaStore.MediaColumns.IS_PENDING, 0); resolver.update(uri, values, null, null)
uri
} else null
} else {
// Android 9 und älter public Downloads (WRITE_EXTERNAL_STORAGE kann nötig sein)
val downloadsDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
if (!downloadsDir.exists()) downloadsDir.mkdirs()
val outFile = File(downloadsDir, filename)
outFile.writeBytes(bytes)
// im System sichtbar machen
MediaScannerConnection.scanFile(
activity,
arrayOf(outFile.absolutePath),
arrayOf(mimeType),
null
)
Uri.fromFile(outFile)
}
}
// Liefert einen englischen, lesbaren Fragetext/Titel zu einer ID (nur EN, keine IDs).
private suspend fun englishQuestionForId(id: String, questionnaireIdSet: Set<String>): String {
if (id == "client_code") return "Client code"
if (id in questionnaireIdSet && !id.contains('-')) return "Questionnaire status"
localizeEnglishNoBrackets(id)?.let { lm ->
if (!looksLikeId(lm, id)) return lm
}
val fieldPart = id.substringAfterLast('-', id)
localizeEnglishNoBrackets(fieldPart)?.let { lm ->
if (!looksLikeId(lm, fieldPart)) return lm
}
val pretty = humanizeIdToEnglish(fieldPart)
if (pretty.isNotBlank()) return pretty
return "Question"
}
private fun looksLikeId(text: String, originalId: String): Boolean {
val normText = text.lowercase().replace(Regex("[^a-z0-9]+"), "_").trim('_')
val normId = originalId.lowercase().replace(Regex("[^a-z0-9]+"), "_").trim('_')
return normText == normId
}
private fun humanizeIdToEnglish(source: String): String {
val s = source
.replace(Regex("^questionnaire_\\d+_"), "")
.replace('_', ' ')
.trim()
if (s.isBlank()) return s
return s.split(Regex("\\s+")).joinToString(" ") { it.lowercase().replaceFirstChar { c -> c.titlecase() } }
}
// ---------------------------
// SCREEN 2: Fragebogen-Übersicht + "header"-Liste für einen Client
// SCREEN 2: Fragebogen-Übersicht + "header"-Liste
// ---------------------------
private fun openClientOverviewScreen(clientCode: String) {
activity.setContentView(R.layout.client_overview_screen)
val lang = safeLang() // ✨
val title: TextView = requireView(R.id.titleClientOverview, "titleClientOverview")
val tableQ: TableLayout = requireView(R.id.tableQuestionnaires, "tableQuestionnaires")
val progress: ProgressBar = requireView(R.id.progressBarClient, "progressBarClient")
val emptyView: TextView = requireView(R.id.emptyViewClient, "emptyViewClient")
val backButton: Button = requireView(R.id.backButtonClient, "backButtonClient")
// "header" Sektion
val headerLabel: TextView = requireView(R.id.headerLabel, "headerLabel")
val tableOrdered: TableLayout = requireView(R.id.tableOrdered, "tableOrdered")
title.text = "Client: $clientCode Fragebögen"
headerLabel.text = "header"
// ✨
title.text = "${t(lang, "client") ?: "Client"}: $clientCode ${t(lang, "questionnaires") ?: "Fragebögen"}"
headerLabel.text = t(lang, "headers") ?: "header"
backButton.text = t(lang, "previous") ?: "Zurück"
backButton.setOnClickListener { openDatabaseScreen() }
progress.visibility = View.VISIBLE
@ -274,8 +137,15 @@ class DatabaseButtonHandler(
tableQ.removeAllViews()
tableOrdered.removeAllViews()
addHeaderRow(tableQ, listOf("#", "Fragebogen-ID", "Status"))
addHeaderRow(tableOrdered, listOf("#", "ID", "Wert"))
// ✨ Tabellenköpfe lokalisieren
addHeaderRow(
tableQ,
listOf("#", t(lang, "questionnaire_id") ?: "Fragebogen-ID", t(lang, "status") ?: "Status")
)
addHeaderRow(
tableOrdered,
listOf("#", t(lang, "id") ?: "ID", t(lang, "value") ?: "Wert")
)
uiScope.launch {
val result = withContext(Dispatchers.IO) {
@ -292,7 +162,7 @@ class DatabaseButtonHandler(
val allAnswersForClient = result.third
if (allQuestionnaires.isEmpty()) {
emptyView.text = "Keine Fragebögen vorhanden."
emptyView.text = t(lang, "no_questionnaires") ?: "Keine Fragebögen vorhanden."
emptyView.visibility = View.VISIBLE
}
@ -300,7 +170,7 @@ class DatabaseButtonHandler(
val questionnaireIdSet = allQuestionnaires.map { it.id }.toSet()
val answerMap = allAnswersForClient.associate { it.questionId to it.answerValue }
// Tabelle 1 (nur Statusfarbe als Textfarbe)
// Tabelle 1 (Status)
allQuestionnaires
.sortedWith(compareBy({ extractQuestionnaireNumber(it.id) ?: Int.MAX_VALUE }, { it.id }))
.forEachIndexed { idx, q ->
@ -313,52 +183,44 @@ class DatabaseButtonHandler(
table = tableQ,
cells = listOf((idx + 1).toString(), q.id, statusText),
onClick = { openQuestionnaireDetailScreen(clientCode, q.id) },
colorOverrides = mapOf(2 to statusTextColor),
cellBgOverrides = emptyMap()
colorOverrides = mapOf(2 to statusTextColor)
)
} else {
addDisabledRow(
table = tableQ,
cells = listOf((idx + 1).toString(), q.id, statusText),
colorOverrides = mapOf(2 to statusTextColor),
cellBgOverrides = emptyMap()
colorOverrides = mapOf(2 to statusTextColor)
)
}
}
// Farben (Material-like)
val lightGreen = 0xFFC8E6C9.toInt() // beantwortet (nicht-Fragebogen)
val lightRed = 0xFFFFCDD2.toInt() // unbeantwortet (nicht-Fragebogen)
val doneGreen = 0xFF4CAF50.toInt() // Fragebogen: Done (BLEIBT)
val notRed = 0xFFF44336.toInt() // Fragebogen: Not Done (BLEIBT)
// Farben
val lightGreen = 0xFFC8E6C9.toInt()
val lightRed = 0xFFFFCDD2.toInt()
val doneGreen = 0xFF4CAF50.toInt()
val notRed = 0xFFF44336.toInt()
// Tabelle 2 (Header-Liste)
val orderedIds = loadOrderedIds()
val orderedIds = headerRepo.loadOrderedIds()
orderedIds.forEachIndexed { idx, id ->
var rowBgColor: Int? = null
val raw: String
val cellBgForQuestionnaire: Int?
if (id == "client_code") {
// client_code bleibt unmarkiert
raw = clientCode
cellBgForQuestionnaire = null
} else if (id in questionnaireIdSet) {
// Fragebogenstatus: wie bisher grün/rot NUR für #, ID, Wert
raw = if (statusMap[id] == true) "Done" else "Not Done"
cellBgForQuestionnaire = if (raw == "Done") doneGreen else notRed
} else {
// Normale Frage:
raw = answerMap[id]?.takeIf { it.isNotBlank() } ?: "None"
cellBgForQuestionnaire = null
// NEU: hellgrün wenn beantwortet, hellrot wenn None
rowBgColor = if (raw == "None") lightRed else lightGreen
}
val display = localizeHeaderValue(id, raw)
val display = localizeHeaderValue(id, raw, lang) // ✨
// Für Fragebögen: "#"(0), ID(1), Wert(2) farbig hinterlegen wie gehabt
val cellBgOverrides =
if (cellBgForQuestionnaire != null)
mapOf(0 to cellBgForQuestionnaire, 1 to cellBgForQuestionnaire, 2 to cellBgForQuestionnaire)
@ -367,34 +229,39 @@ class DatabaseButtonHandler(
addRow(
table = tableOrdered,
cells = listOf((idx + 1).toString(), id, display),
colorOverrides = emptyMap(),
rowBgColor = rowBgColor, // greift für normale Fragen (ganze Zeile inkl. #)
cellBgOverrides = cellBgOverrides // greift für Fragebögen (nur 3 Zellen)
rowBgColor = rowBgColor,
cellBgOverrides = cellBgOverrides
)
}
}
}
// ---------------------------
// SCREEN 3: Fragen & Antworten zu einem Fragebogen
// SCREEN 3: Fragen & Antworten eines Fragebogens
// ---------------------------
private fun openQuestionnaireDetailScreen(clientCode: String, questionnaireId: String) {
activity.setContentView(R.layout.questionnaire_detail_screen)
val lang = safeLang() // ✨
val title: TextView = requireView(R.id.titleQuestionnaireDetail, "titleQuestionnaireDetail")
val table: TableLayout = requireView(R.id.tableQA, "tableQA")
val progress: ProgressBar = requireView(R.id.progressBarQA, "progressBarQA")
val emptyView: TextView = requireView(R.id.emptyViewQA, "emptyViewQA")
val backButton: Button = requireView(R.id.backButtonQA, "backButtonQA")
title.text = "Client: $clientCode Fragebogen: $questionnaireId"
// ✨ Titel + Back + Empty übersetzen
title.text = "${t(lang, "client") ?: "Client"}: $clientCode ${t(lang, "questionnaire") ?: "Fragebogen"}: $questionnaireId"
backButton.text = t(lang, "previous") ?: "Zurück"
emptyView.text = t(lang, "no_questions_available") ?: "Keine Fragen vorhanden."
backButton.setOnClickListener { openClientOverviewScreen(clientCode) }
progress.visibility = View.VISIBLE
emptyView.visibility = View.GONE
table.removeAllViews()
addHeaderRow(table, listOf("#", "Frage", "Antwort"))
// ✨ Kopf: # | Frage | Antwort
addHeaderRow(table, listOf("#", t(lang, "question") ?: "Frage", t(lang, "answer") ?: "Antwort"))
uiScope.launch {
val (questions, answersForClient) = withContext(Dispatchers.IO) {
@ -407,7 +274,6 @@ class DatabaseButtonHandler(
progress.visibility = View.GONE
if (questions.isEmpty()) {
emptyView.text = "Keine Fragen vorhanden."
emptyView.visibility = View.VISIBLE
return@launch
}
@ -415,148 +281,68 @@ class DatabaseButtonHandler(
val answerMap = answersForClient.associate { it.questionId to it.answerValue }
questions.forEachIndexed { idx, q: Question ->
val qText = q.question.takeIf { it.isNotBlank() } ?: q.questionId
val aText = answerMap[q.questionId]?.takeIf { it.isNotBlank() } ?: ""
val baseId = q.questionId.substringAfterLast('-', q.questionId) // z.B. consent_signed, gender_male
val qText = localizeQuestionLabel(q.questionId, q.question, lang) // ✨
val raw = answerMap[q.questionId]?.takeIf { it.isNotBlank() } ?: ""
val aText = localizeAnswerValue(baseId, raw, lang) // ✨
addRow(table, listOf((idx + 1).toString(), qText, aText))
}
}
}
// ---------------------------
// ordered_ids aus XLSX/JSON laden (mit Cache)
// Lokalisierungshilfen (Anzeige)
// ---------------------------
private fun loadOrderedIds(): List<String> {
orderedIdsCache?.let { return it }
val fromXlsx = runCatching { loadOrderedIdsFromExcel("header_order.xlsx") }
.onFailure { e -> Log.w(tag, "header_order.xlsx konnte nicht gelesen werden: ${e.message}") }
.getOrElse { emptyList() }
private fun safeLang(): String = try { languageIDProvider() } catch (_: Exception) { "GERMAN" }
if (fromXlsx.isNotEmpty()) {
orderedIdsCache = fromXlsx
return fromXlsx
}
return try {
val stream = activity.assets.open("header_order.json")
val json = stream.readBytes().toString(Charset.forName("UTF-8"))
val arr = JSONArray(json)
val list = MutableList(arr.length()) { i -> arr.getString(i) }
orderedIdsCache = list
list
} catch (e: Exception) {
Log.e(tag, "Weder header_order.xlsx noch header_order.json verfügbar/gültig: ${e.message}")
Toast.makeText(activity, "Keine Header-Vorlage gefunden", Toast.LENGTH_LONG).show()
emptyList()
}
private fun stripBrackets(s: String): String {
val m = Regex("^\\[(.*)]$").matchEntire(s.trim())
return m?.groupValues?.get(1) ?: s
}
private fun loadOrderedIdsFromExcel(assetFileName: String): List<String> {
activity.assets.open(assetFileName).use { input ->
XSSFWorkbook(input).use { wb ->
val sheet = wb.getSheetAt(0) ?: return emptyList()
val row = sheet.getRow(0) ?: return emptyList()
val first = row.firstCellNum.toInt()
val last = row.lastCellNum.toInt() // exklusiv
val out = mutableListOf<String>()
for (i in first until last) {
val cell = row.getCell(i) ?: continue
val value = getCellAsString(cell).trim()
if (value.isEmpty()) continue
if (i == first && value == "#") continue // „#“ in Spalte 0 ignorieren
out.add(value)
}
return out
}
}
private fun t(lang: String, key: String): String? {
val txt = try { LanguageManager.getText(lang, key) } catch (_: Exception) { null } ?: return null
val out = stripBrackets(txt).trim()
return if (out.equals(key, true) || out.isBlank()) null else out
}
private fun getCellAsString(cell: org.apache.poi.ss.usermodel.Cell): String =
when (cell.cellType) {
org.apache.poi.ss.usermodel.CellType.STRING -> cell.stringCellValue
org.apache.poi.ss.usermodel.CellType.NUMERIC ->
if (org.apache.poi.ss.usermodel.DateUtil.isCellDateFormatted(cell))
cell.dateCellValue.time.toString()
else {
val n = cell.numericCellValue
if (n % 1.0 == 0.0) n.toLong().toString() else n.toString()
}
org.apache.poi.ss.usermodel.CellType.BOOLEAN -> cell.booleanCellValue.toString()
org.apache.poi.ss.usermodel.CellType.FORMULA -> cell.richStringCellValue.string
else -> ""
}
// ---------------------------
// Hilfsfunktionen (Lokalisierung & UI)
// ---------------------------
private fun extractQuestionnaireNumber(id: String): Int? {
val m = Regex("^questionnaire_(\\d+)").find(id.lowercase())
return m?.groupValues?.get(1)?.toIntOrNull()
// ✨ Frage-Label für die UI
private fun localizeQuestionLabel(questionId: String, fallbackQuestionText: String?, lang: String): String {
val field = questionId.substringAfterLast('-', questionId)
t(lang, field)?.let { return it } // z.B. consent_instruction
t(lang, questionId)?.let { return it } // kompletter key
fallbackQuestionText?.takeIf { it.isNotBlank() }?.let { return it }
return field.replace('_', ' ').replaceFirstChar { it.titlecase() }
}
// Anzeige im Header (App) aktuelle UI-Sprache
private fun localizeHeaderValue(id: String, raw: String): String {
if (id == "client_code") return raw
val lang = try { languageIDProvider() } catch (_: Exception) { "GERMAN" }
fun strip(s: String): String {
val m = Regex("^\\[(.*)]$").matchEntire(s.trim())
return m?.groupValues?.get(1) ?: s
}
fun tryKey(key: String): String? {
val t = try { LanguageManager.getText(lang, key) } catch (_: Exception) { key }
val out = strip(t)
if (out.isBlank() || out.equals(key, ignoreCase = true)) return null
return out
}
// ✨ Antwort-Wert für die UI
private fun localizeAnswerValue(fieldId: String, raw: String, lang: String): String {
// Zahlen/Datum/Strich beibehalten
if (raw == "") return raw
if (raw.matches(Regex("^\\d{1,4}([./-]\\d{1,2}([./-]\\d{1,4})?)?\$"))) return raw
// 1) direkter Key
t(lang, raw)?.let { return it }
// 2) normalisiert
val norm = raw.lowercase().replace(Regex("[^a-z0-9]+"), "_").trim('_')
val candidates = buildList {
when (raw) { "Done" -> add("done"); "Not Done" -> add("not_done"); "None" -> add("none") }
add(raw)
if (norm.isNotBlank() && norm != raw) add(norm)
if (norm.isNotBlank()) { add("${id}_$norm"); add("${id}-$norm") }
if (norm.isNotBlank()) t(lang, norm)?.let { return it }
// 3) kombiniere Feld + Wert (häufiges Muster)
if (norm.isNotBlank()) {
t(lang, "${fieldId}_$norm")?.let { return it }
t(lang, "${fieldId}-${norm}")?.let { return it }
}
for (key in candidates) tryKey(key)?.let { return it }
return raw
}
private fun localizeEnglishNoBrackets(key: String): String? {
val t = try { LanguageManager.getText("ENGLISH", key) } catch (_: Exception) { null }
val m = Regex("^\\[(.*)]$").matchEntire(t?.trim() ?: "")
val stripped = m?.groupValues?.get(1) ?: t
if (stripped == null || stripped.isBlank() || stripped.equals(key, ignoreCase = true)) return null
return stripped
}
// Englisch für Export; belässt Done/Not Done/None.
private fun localizeForExportEn(id: String, raw: String): String {
if (id == "client_code") return raw
if (raw == "Done" || raw == "Not Done" || raw == "None") return raw
val norm = raw.lowercase().replace(Regex("[^a-z0-9]+"), "_").trim('_')
val candidates = buildList {
add(raw)
if (norm.isNotBlank() && norm != raw) add(norm)
if (norm.isNotBlank()) { add("${id}_$norm"); add("${id}-$norm") }
}
for (key in candidates) localizeEnglishNoBrackets(key)?.let { return it }
return raw
// Klammern entfernen, falls aus LanguageManager so geliefert
return stripBrackets(raw)
}
// ---------------------------
// UI-Helfer
// Anzeige-Helfer
// ---------------------------
private fun addHeaderRow(table: TableLayout, labels: List<String>) {
val row = TableRow(activity)
labels.forEach { label -> row.addView(makeHeaderCell(label)) }
table.addView(row)
addDivider(table)
table.addView(row); addDivider(table)
}
private fun addRow(
@ -569,11 +355,9 @@ class DatabaseButtonHandler(
val row = TableRow(activity)
rowBgColor?.let { row.setBackgroundColor(it) }
cells.forEachIndexed { index, text ->
val tv = makeBodyCell(text, colorOverrides[index], cellBgOverrides[index])
row.addView(tv)
val tv = makeBodyCell(text, colorOverrides[index], cellBgOverrides[index]); row.addView(tv)
}
table.addView(row)
addDivider(table)
table.addView(row); addDivider(table)
}
private fun addClickableRow(
@ -590,11 +374,9 @@ class DatabaseButtonHandler(
setOnClickListener { onClick() }
}
cells.forEachIndexed { index, text ->
val tv = makeBodyCell(text, colorOverrides[index], cellBgOverrides[index])
row.addView(tv)
val tv = makeBodyCell(text, colorOverrides[index], cellBgOverrides[index]); row.addView(tv)
}
table.addView(row)
addDivider(table)
table.addView(row); addDivider(table)
}
private fun addDisabledRow(
@ -603,25 +385,16 @@ class DatabaseButtonHandler(
colorOverrides: Map<Int, Int> = emptyMap(),
cellBgOverrides: Map<Int, Int> = emptyMap()
) {
val row = TableRow(activity).apply {
isClickable = false
isEnabled = false
alpha = 0.6f
}
val row = TableRow(activity).apply { isClickable = false; isEnabled = false; alpha = 0.6f }
cells.forEachIndexed { index, text ->
val tv = makeBodyCell(text, colorOverrides[index], cellBgOverrides[index])
row.addView(tv)
val tv = makeBodyCell(text, colorOverrides[index], cellBgOverrides[index]); row.addView(tv)
}
table.addView(row)
addDivider(table)
table.addView(row); addDivider(table)
}
private fun addDivider(table: TableLayout) {
val divider = View(activity)
val params = TableLayout.LayoutParams(
TableLayout.LayoutParams.MATCH_PARENT,
dp(1)
)
val params = TableLayout.LayoutParams(TableLayout.LayoutParams.MATCH_PARENT, dp(1))
divider.layoutParams = params
divider.setBackgroundColor(0xFFDDDDDD.toInt())
table.addView(divider)
@ -663,4 +436,25 @@ class DatabaseButtonHandler(
}
return v
}
private fun extractQuestionnaireNumber(id: String): Int? {
val m = Regex("^questionnaire_(\\d+)").find(id.lowercase())
return m?.groupValues?.get(1)?.toIntOrNull()
}
// ✨ alte localizeHeaderValue erweitert: nimmt jetzt lang mit
private fun localizeHeaderValue(id: String, raw: String, lang: String): String {
if (id == "client_code") return raw
fun norm(s: String) = s.lowercase().replace(Regex("[^a-z0-9]+"), "_").trim('_')
val candidates = buildList {
when (raw) { "Done" -> add("done"); "Not Done" -> add("not_done"); "None" -> add("none") }
add(raw)
val n = norm(raw)
if (n.isNotBlank() && n != raw) add(n)
if (n.isNotBlank()) { add("${id}_$n"); add("${id}-$n") }
}
for (k in candidates) t(lang, k)?.let { return it }
return stripBrackets(raw)
}
}

View File

@ -0,0 +1,163 @@
package com.dano.test1
import android.content.ContentValues
import android.content.Context
import android.media.MediaScannerConnection
import android.net.Uri
import android.os.Build
import android.os.Environment
import android.provider.MediaStore
import org.apache.poi.ss.usermodel.Row
import org.apache.poi.xssf.usermodel.XSSFWorkbook
class ExcelExportService(
private val context: Context,
private val headerRepo: HeaderOrderRepository
) {
/** Baut die Excel-Datei und speichert sie ausschließlich unter "Downloads". */
suspend fun exportHeadersForAllClients(): Uri? {
val orderedIds = headerRepo.loadOrderedIds()
if (orderedIds.isEmpty()) return null
val clients = MyApp.database.clientDao().getAllClients()
val questionnaires = MyApp.database.questionnaireDao().getAll()
val questionnaireIdSet = questionnaires.map { it.id }.toSet()
val wb = XSSFWorkbook()
val sheet = wb.createSheet("Headers")
sheet.setColumnWidth(0, 8 * 256)
for (i in 1..orderedIds.size) sheet.setColumnWidth(i, 36 * 256)
// Row 1: IDs
var col = 0
val headerRow: Row = sheet.createRow(0)
headerRow.createCell(col++).setCellValue("#")
orderedIds.forEach { id -> headerRow.createCell(col++).setCellValue(id) }
// Row 2: Questions (EN)
val questionRow: Row = sheet.createRow(1)
var qc = 0
questionRow.createCell(qc++).setCellValue("Question (EN)")
for (id in orderedIds) {
val englishQuestion = englishQuestionForId(id, questionnaireIdSet)
questionRow.createCell(qc++).setCellValue(englishQuestion)
}
// Rows 3+: Values per client
clients.forEachIndexed { rowIdx, client ->
val row: Row = sheet.createRow(rowIdx + 2)
var c = 0
row.createCell(c++).setCellValue((rowIdx + 1).toDouble())
val completedForClient = MyApp.database.completedQuestionnaireDao().getAllForClient(client.clientCode)
val statusMap = completedForClient.associate { it.questionnaireId to it.isDone }
val answers = MyApp.database.answerDao().getAnswersForClient(client.clientCode)
val answerMap = answers.associate { it.questionId to it.answerValue }
orderedIds.forEach { id ->
val raw = when {
id == "client_code" -> client.clientCode
id in questionnaireIdSet -> if (statusMap[id] == true) "Done" else "Not Done"
else -> answerMap[id]?.takeIf { it.isNotBlank() } ?: "None"
}
val out = localizeForExportEn(id, raw)
row.createCell(c++).setCellValue(out)
}
}
val bytes = java.io.ByteArrayOutputStream().use { bos ->
wb.write(bos); bos.toByteArray()
}
wb.close()
return saveToDownloads(
filename = "ClientHeaders.xlsx",
mimeType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
bytes = bytes
)
}
/** Speichert Bytes nach "Downloads". */
private fun saveToDownloads(filename: String, mimeType: String, bytes: ByteArray): Uri? {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val resolver = context.contentResolver
val values = ContentValues().apply {
put(MediaStore.MediaColumns.DISPLAY_NAME, filename)
put(MediaStore.MediaColumns.MIME_TYPE, mimeType)
put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS)
}
val collection = MediaStore.Downloads.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
val uri = resolver.insert(collection, values)
if (uri != null) {
resolver.openOutputStream(uri)?.use { it.write(bytes) } ?: return null
uri
} else null
} else {
val downloadsDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
if (!downloadsDir.exists()) downloadsDir.mkdirs()
val outFile = java.io.File(downloadsDir, filename)
outFile.writeBytes(bytes)
MediaScannerConnection.scanFile(
context,
arrayOf(outFile.absolutePath),
arrayOf(mimeType),
null
)
Uri.fromFile(outFile)
}
}
// ---------- Export-spezifische Lokalisierung (EN) ----------
private suspend fun englishQuestionForId(id: String, questionnaireIdSet: Set<String>): String {
if (id == "client_code") return "Client code"
if (id in questionnaireIdSet && !id.contains('-')) return "Questionnaire status"
localizeEnglishNoBrackets(id)?.let { lm ->
if (!looksLikeId(lm, id)) return lm
}
val fieldPart = id.substringAfterLast('-', id)
localizeEnglishNoBrackets(fieldPart)?.let { lm ->
if (!looksLikeId(lm, fieldPart)) return lm
}
val pretty = humanizeIdToEnglish(fieldPart)
if (pretty.isNotBlank()) return pretty
return "Question"
}
private fun looksLikeId(text: String, originalId: String): Boolean {
val normText = text.lowercase().replace(Regex("[^a-z0-9]+"), "_").trim('_')
val normId = originalId.lowercase().replace(Regex("[^a-z0-9]+"), "_").trim('_')
return normText == normId
}
private fun humanizeIdToEnglish(source: String): String {
val s = source.replace(Regex("^questionnaire_\\d+_"), "").replace('_', ' ').trim()
if (s.isBlank()) return s
return s.split(Regex("\\s+")).joinToString(" ") { it.lowercase().replaceFirstChar { c -> c.titlecase() } }
}
private fun localizeEnglishNoBrackets(key: String): String? {
val t = try { LanguageManager.getText("ENGLISH", key) } catch (_: Exception) { null }
val m = Regex("^\\[(.*)]$").matchEntire(t?.trim() ?: "")
val stripped = m?.groupValues?.get(1) ?: t
if (stripped == null || stripped.isBlank() || stripped.equals(key, ignoreCase = true)) return null
return stripped
}
/** Englisch für Export; belässt Done/Not Done/None. */
private fun localizeForExportEn(id: String, raw: String): String {
if (id == "client_code") return raw
if (raw == "Done" || raw == "Not Done" || raw == "None") return raw
val norm = raw.lowercase().replace(Regex("[^a-z0-9]+"), "_").trim('_')
val candidates = buildList {
add(raw)
if (norm.isNotBlank() && norm != raw) add(norm)
if (norm.isNotBlank()) { add("${id}_$norm"); add("${id}-$norm") }
}
for (key in candidates) localizeEnglishNoBrackets(key)?.let { return it }
return raw
}
}

View File

@ -210,6 +210,7 @@ class HandlerOpeningScreen(private val activity: MainActivity) {
}
private fun updateButtonTexts() {
// --- dynamische Fragebogen-Buttons wie gehabt ---
questionnaireFiles.forEach { (button, fileName) ->
val entry = questionnaireEntries.firstOrNull { it.file == fileName }
val key = fileName.substringAfter("questionnaire_").substringAfter("_").removeSuffix(".json")
@ -220,23 +221,32 @@ class HandlerOpeningScreen(private val activity: MainActivity) {
if (entry?.showPoints == true && pointsAvailable != null) {
buttonText += " (${points} P)"
}
button.text = buttonText
if (entry?.showPoints == true && pointsAvailable != null) {
when {
points in 0..12 -> button.setBackgroundColor(Color.parseColor("#4CAF50"))
points in 0..12 -> button.setBackgroundColor(Color.parseColor("#4CAF50"))
points in 13..36 -> button.setBackgroundColor(Color.parseColor("#FFEB3B"))
points in 37..100 -> button.setBackgroundColor(Color.parseColor("#F44336"))
else -> button.setBackgroundColor(Color.parseColor("#E0E0E0"))
points in 37..100-> button.setBackgroundColor(Color.parseColor("#F44336"))
else -> button.setBackgroundColor(Color.parseColor("#E0E0E0"))
}
} else {
button.setBackgroundColor(Color.parseColor("#E0E0E0"))
}
}
buttonLoad.text = LanguageManager.getText(languageID, "load")
databaseButton.text = "Datenbank" // fixierter Text gewünscht
// --- HIER: alle Hauptbuttons nach Sprache neu setzen ---
buttonLoad.text = LanguageManager.getText(languageID, "load")
saveButton.text = LanguageManager.getText(languageID, "save")
editButton.text = LanguageManager.getText(languageID, "edit")
uploadButton.text = LanguageManager.getText(languageID, "upload")
downloadButton.text= LanguageManager.getText(languageID, "download")
databaseButton.text= LanguageManager.getText(languageID, "database")
// optional: Beispieltext/Hints auch aktualisieren
val hintTag = editText.tag as? String ?: ""
editText.hint = LanguageManager.getText(languageID, hintTag)
textView.text = LanguageManager.getText(languageID, "example_text")
}
private fun setButtonsEnabled(enabledButtons: List<Button>) {

View File

@ -0,0 +1,75 @@
package com.dano.test1
import android.content.Context
import android.util.Log
import android.widget.Toast
import org.apache.poi.xssf.usermodel.XSSFWorkbook
import org.json.JSONArray
import java.nio.charset.Charset
class HeaderOrderRepository(private val context: Context) {
private val tag = "HeaderOrderRepository"
private var orderedIdsCache: List<String>? = null
fun loadOrderedIds(): List<String> {
orderedIdsCache?.let { return it }
val fromXlsx = runCatching { loadOrderedIdsFromExcel("header_order.xlsx") }
.onFailure { e -> Log.w(tag, "header_order.xlsx konnte nicht gelesen werden: ${e.message}") }
.getOrElse { emptyList() }
if (fromXlsx.isNotEmpty()) {
orderedIdsCache = fromXlsx
return fromXlsx
}
return try {
val stream = context.assets.open("header_order.json")
val json = stream.readBytes().toString(Charset.forName("UTF-8"))
val arr = JSONArray(json)
val list = MutableList(arr.length()) { i -> arr.getString(i) }
orderedIdsCache = list
list
} catch (e: Exception) {
Log.e(tag, "Weder header_order.xlsx noch header_order.json verfügbar/gültig: ${e.message}")
Toast.makeText(context, "Keine Header-Vorlage gefunden", Toast.LENGTH_LONG).show()
emptyList()
}
}
private fun loadOrderedIdsFromExcel(assetFileName: String): List<String> {
context.assets.open(assetFileName).use { input ->
XSSFWorkbook(input).use { wb ->
val sheet = wb.getSheetAt(0) ?: return emptyList()
val row = sheet.getRow(0) ?: return emptyList()
val first = row.firstCellNum.toInt()
val last = row.lastCellNum.toInt() // exklusiv
val out = mutableListOf<String>()
for (i in first until last) {
val cell = row.getCell(i) ?: continue
val value = when (cell.cellType) {
org.apache.poi.ss.usermodel.CellType.STRING -> cell.stringCellValue
org.apache.poi.ss.usermodel.CellType.NUMERIC ->
if (org.apache.poi.ss.usermodel.DateUtil.isCellDateFormatted(cell))
cell.dateCellValue.time.toString()
else {
val n = cell.numericCellValue
if (n % 1.0 == 0.0) n.toLong().toString() else n.toString()
}
org.apache.poi.ss.usermodel.CellType.BOOLEAN -> cell.booleanCellValue.toString()
org.apache.poi.ss.usermodel.CellType.FORMULA -> cell.richStringCellValue.string
else -> ""
}.trim()
if (value.isEmpty()) continue
if (i == first && value == "#") continue // „#“ in Spalte 0 ignorieren
out.add(value)
}
return out
}
}
}
}

View File

@ -328,7 +328,33 @@ object LanguageManager {
"no_profile" to "Dieser Klient ist noch nicht Teil der Datenbank",
"questionnaires_finished" to "Für diesen Klienten wurden alle Fragebögen ausgefüllt!",
"edit" to "Bearbeiten",
"save" to "Speichern"
"save" to "Speichern",
"upload" to "Hochladen",
"download" to "Herunterladen",
"database" to "Datenbank",
"clients" to "Clients",
"client" to "Client",
"client_code_label" to "Client-Code",
"questionnaires" to "Fragebögen",
"questionnaire" to "Fragebogen",
"questionnaire_id" to "Fragebogen-ID",
"status" to "Status",
"header_label" to "Header",
"id" to "ID",
"value" to "Wert",
"no_clients" to "Keine Clients vorhanden.",
"no_questionnaires" to "Keine Fragebögen vorhanden.",
"no_questions" to "Keine Fragen vorhanden.",
"question" to "Frage",
"answer" to "Antwort",
"download_header" to "Download Header",
"back" to "Zurück",
"export_success_downloads_headers" to "Export erfolgreich: Downloads/ClientHeaders.xlsx",
"export_failed" to "Export fehlgeschlagen.",
"error_generic" to "Fehler",
"done" to "abgeschlossen",
"not_done" to "nicht abgeschlossen",
"none" to "keine Angabe",
),
@ -625,7 +651,33 @@ object LanguageManager {
"no_profile" to "This client is not yet part of the database",
"questionnaires_finished" to "All questionnaires have been completed for this client!",
"edit" to "Bearbeiten",
"save" to "Speichern"
"save" to "Speichern",
"upload" to "Upload",
"download" to "Download",
"database" to "Database",
"clients" to "Clients",
"client" to "Client",
"client_code_label" to "Client Code",
"questionnaires" to "Questionnaires",
"questionnaire" to "Questionnaire",
"questionnaire_id" to "Questionnaire ID",
"status" to "Status",
"header_label" to "Header",
"id" to "ID",
"value" to "Value",
"no_clients" to "No clients available.",
"no_questionnaires" to "No questionnaires available.",
"no_questions" to "No questions available.",
"question" to "Question",
"answer" to "Answer",
"download_header" to "Download header",
"back" to "Back",
"export_success_downloads_headers" to "Export successful: Downloads/ClientHeaders.xlsx",
"export_failed" to "Export failed.",
"error_generic" to "Error",
"done" to "Done",
"not_done" to "Not Done",
"none" to "None",
),
"FRENCH" to mapOf(
@ -925,7 +977,33 @@ object LanguageManager {
"no_profile" to "Ce client ne fait pas encore partie de la base de données",
"questionnaires_finished" to "Tous les questionnaires ont été complétés pour ce client !",
"edit" to "Modifier",
"save" to "Sauvegarder"
"save" to "Sauvegarder",
"upload" to "Téléverser",
"download" to "Télécharger",
"database" to "Base de données",
"clients" to "Clients",
"client" to "Client",
"client_code_label" to "Code client",
"questionnaires" to "Questionnaires",
"questionnaire" to "Questionnaire",
"questionnaire_id" to "ID du questionnaire",
"status" to "Statut",
"header_label" to "En-tête",
"id" to "ID",
"value" to "Valeur",
"no_clients" to "Aucun client disponible.",
"no_questionnaires" to "Aucun questionnaire disponible.",
"no_questions" to "Aucune question disponible.",
"question" to "Question",
"answer" to "Réponse",
"download_header" to "Télécharger len-tête",
"back" to "Retour",
"export_success_downloads_headers" to "Export réussi : Downloads/ClientHeaders.xlsx",
"export_failed" to "Échec de lexportation.",
"error_generic" to "Erreur",
"done" to "terminé",
"not_done" to "non terminé",
"none" to "aucune réponse",
),
"RUSSIAN" to mapOf(
@ -1221,7 +1299,33 @@ object LanguageManager {
"no_profile" to "Этот клиент еще не внесен в базу данных",
"questionnaires_finished" to "Для этого клиента все анкеты заполнены!",
"edit" to "Редактировать",
"save" to "Сохранять"
"save" to "Сохранять",
"upload" to "Загрузить",
"download" to "Скачать",
"database" to "База данных",
"clients" to "Клиенты",
"client" to "Клиент",
"client_code_label" to "Код клиента",
"questionnaires" to "Анкеты",
"questionnaire" to "Анкета",
"questionnaire_id" to "ID анкеты",
"status" to "Статус",
"header_label" to "Заголовки",
"id" to "ID",
"value" to "Значение",
"no_clients" to "Клиентов нет.",
"no_questionnaires" to "Нет анкет.",
"no_questions" to "Нет вопросов.",
"question" to "Вопрос",
"answer" to "Ответ",
"download_header" to "Скачать заголовки",
"back" to "Назад",
"export_success_downloads_headers" to "Экспорт выполнен: Downloads/ClientHeaders.xlsx",
"export_failed" to "Экспорт не выполнен.",
"error_generic" to "Ошибка",
"done" to "выполнено",
"not_done" to "не выполнено",
"none" to "нет ответа",
),
"UKRAINIAN" to mapOf(
@ -1521,7 +1625,33 @@ object LanguageManager {
"no_profile" to "Цей клієнт ще не внесений до бази даних",
"questionnaires_finished" to "Для цього клієнта всі опитувальники заповнені!",
"edit" to "Редагувати",
"save" to "Зберегти"
"save" to "Зберегти",
"upload" to "Вивантажити",
"download" to "Завантажити",
"database" to "База даних",
"clients" to "Клієнти",
"client" to "Клієнт",
"client_code_label" to "Код клієнта",
"questionnaires" to "Анкети",
"questionnaire" to "Анкета",
"questionnaire_id" to "ID анкети",
"status" to "Статус",
"header_label" to "Заголовки",
"id" to "ID",
"value" to "Значення",
"no_clients" to "Немає клієнтів.",
"no_questionnaires" to "Немає анкет.",
"no_questions" to "Немає запитань.",
"question" to "Питання",
"answer" to "Відповідь",
"download_header" to "Завантажити заголовки",
"back" to "Назад",
"export_success_downloads_headers" to "Експорт успішний: Downloads/ClientHeaders.xlsx",
"export_failed" to "Помилка експорту.",
"error_generic" to "Помилка",
"done" to "завершено",
"not_done" to "не завершено",
"none" to "немає відповіді",
),
"TURKISH" to mapOf(
@ -1821,7 +1951,33 @@ object LanguageManager {
"no_profile" to "Bu danışan henüz veritabanında kayıtlı değil",
"questionnaires_finished" to "Bu danışan için tüm anketler doldurulmuştur!",
"edit" to "Düzenlemek",
"save" to "Kaydetmek"
"save" to "Kaydetmek",
"upload" to "Yükle",
"download" to "İndir",
"database" to "Veritabanı",
"clients" to "Müşteriler",
"client" to "Müşteri",
"client_code_label" to "Müşteri Kodu",
"questionnaires" to "Anketler",
"questionnaire" to "Anket",
"questionnaire_id" to "Anket Kimliği",
"status" to "Durum",
"header_label" to "Başlıklar",
"id" to "ID",
"value" to "Değer",
"no_clients" to "Müşteri yok.",
"no_questionnaires" to "Anket yok.",
"no_questions" to "Soru yok.",
"question" to "Soru",
"answer" to "Cevap",
"download_header" to "Başlıkları indir",
"back" to "Geri",
"export_success_downloads_headers" to "Dışa aktarma başarılı: Downloads/ClientHeaders.xlsx",
"export_failed" to "Dışa aktarma başarısız.",
"error_generic" to "Hata",
"done" to "tamamlandı",
"not_done" to "tamamlanmadı",
"none" to "yanıt yok",
),
"POLISH" to mapOf(
@ -2121,7 +2277,33 @@ object LanguageManager {
"no_profile" to "Ten klient nie jest jeszcze częścią bazy danych",
"questionnaires_finished" to "Dla tego klienta wypełniono wszystkie kwestionariusze!",
"edit" to "Redagować",
"save" to "Ratować"
"save" to "Ratować",
"upload" to "Prześlij",
"download" to "Pobierz",
"database" to "Baza danych",
"clients" to "Klienci",
"client" to "Klient",
"client_code_label" to "Kod klienta",
"questionnaires" to "Kwestionariusze",
"questionnaire" to "Kwestionariusz",
"questionnaire_id" to "ID kwestionariusza",
"status" to "Status",
"header_label" to "Nagłówek",
"id" to "ID",
"value" to "Wartość",
"no_clients" to "Brak klientów.",
"no_questionnaires" to "Brak kwestionariuszy.",
"no_questions" to "Brak pytań.",
"question" to "Pytanie",
"answer" to "Odpowiedź",
"download_header" to "Pobierz nagłówki",
"back" to "Wstecz",
"export_success_downloads_headers" to "Eksport zakończony: Downloads/ClientHeaders.xlsx",
"export_failed" to "Eksport nieudany.",
"error_generic" to "Błąd",
"done" to "zakończono",
"not_done" to "nie zakończono",
"none" to "brak odpowiedzi",
),
"ARABIC" to mapOf(
@ -2421,7 +2603,33 @@ object LanguageManager {
"no_profile" to "هذا العميل غير موجود في قاعدة البيانات بعد",
"questionnaires_finished" to "تم الانتهاء من جميع الاستبيانات لهذا العميل!",
"edit" to "يحرر",
"save" to "يحفظ"
"save" to "يحفظ",
"upload" to "رفع",
"download" to "تنزيل",
"database" to "قاعدة البيانات",
"clients" to "العملاء",
"client" to "العميل",
"client_code_label" to "رمز العميل",
"questionnaires" to "الاستبيانات",
"questionnaire" to "استبيان",
"questionnaire_id" to "معرف الاستبيان",
"status" to "الحالة",
"header_label" to "رؤوس",
"id" to "المعرّف",
"value" to "القيمة",
"no_clients" to "لا يوجد عملاء.",
"no_questionnaires" to "لا توجد استبيانات.",
"no_questions" to "لا توجد أسئلة.",
"question" to "سؤال",
"answer" to "إجابة",
"download_header" to "تنزيل الرؤوس",
"back" to "رجوع",
"export_success_downloads_headers" to "تم التصدير بنجاح: Downloads/ClientHeaders.xlsx",
"export_failed" to "فشل التصدير.",
"error_generic" to "خطأ",
"done" to "مكتمل",
"not_done" to "غير مكتمل",
"none" to "لا توجد إجابة",
),
"ROMANIAN" to mapOf(
@ -2721,7 +2929,33 @@ object LanguageManager {
"no_profile" to "Acest client nu face încă parte din baza de date",
"questionnaires_finished" to "Toate chestionarele pentru acest client au fost completate!",
"edit" to "Editizel",
"save" to "Xastral"
"save" to "Xastral",
"upload" to "Încarcă",
"download" to "Descarcă",
"database" to "Bază de date",
"clients" to "Clienți",
"client" to "Client",
"client_code_label" to "Cod client",
"questionnaires" to "Chestionare",
"questionnaire" to "Chestionar",
"questionnaire_id" to "ID chestionar",
"status" to "Stare",
"header_label" to "Antet",
"id" to "ID",
"value" to "Valoare",
"no_clients" to "Nu există clienți.",
"no_questionnaires" to "Nu există chestionare.",
"no_questions" to "Nu există întrebări.",
"question" to "Întrebare",
"answer" to "Răspuns",
"download_header" to "Descarcă antetele",
"back" to "Înapoi",
"export_success_downloads_headers" to "Export reușit: Downloads/ClientHeaders.xlsx",
"export_failed" to "Export nereușit.",
"error_generic" to "Eroare",
"done" to "finalizat",
"not_done" to "nefinalizat",
"none" to "niciun răspuns",
),
"SPANISH" to mapOf(
@ -3021,7 +3255,33 @@ object LanguageManager {
"no_profile" to "Este cliente aún no forma parte de la base de datos",
"questionnaires_finished" to "Se han completado todos los cuestionarios para este cliente.",
"edit" to "Editar",
"save" to "Ahorrar"
"save" to "Ahorrar",
"upload" to "Subir",
"download" to "Descargar",
"database" to "Base de datos",
"clients" to "Clientes",
"client" to "Cliente",
"client_code_label" to "Código de cliente",
"questionnaires" to "Cuestionarios",
"questionnaire" to "Cuestionario",
"questionnaire_id" to "ID del cuestionario",
"status" to "Estado",
"header_label" to "Encabezados",
"id" to "ID",
"value" to "Valor",
"no_clients" to "No hay clientes.",
"no_questionnaires" to "No hay cuestionarios.",
"no_questions" to "No hay preguntas.",
"question" to "Pregunta",
"answer" to "Respuesta",
"download_header" to "Descargar encabezados",
"back" to "Atrás",
"export_success_downloads_headers" to "Exportación correcta: Downloads/ClientHeaders.xlsx",
"export_failed" to "Fallo en la exportación.",
"error_generic" to "Error",
"done" to "completado",
"not_done" to "no completado",
"none" to "sin respuesta",
)
)
}