i18n: complete app-wide localization (10 languages) + audit tooling
Android UI Tests / ui-tests (push) Has been cancelled
Android UI Tests / ui-tests (push) Has been cancelled
Localize all user-facing strings across iOS (SwiftUI), shared Kotlin, and Android Compose into en/es/fr/de/pt/it/ja/ko/nl/zh: - iOS String Catalogs: main + widget Localizable.xcstrings, InfoPlist.xcstrings (permissions), plural variations, ~200 new keys translated - Shared Kotlin ClientStrings table + Android composeResources/values-* (884 keys ×10), routed Api/ViewModel/util error & UI strings through localization - Backend-localized lookups/suggestions consumed via display names - Widget extension catalog; theme names, home-profile fallbacks, validation, network errors, accessibility labels all localized Add re-runnable verification gates: - scripts/i18n_audit.py — enumerate every literal, partition to GAP=0 - scripts/i18n_coverage.py — all 10 locales translated, format-specifier parity Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -248,6 +248,18 @@ object DataManager : IDataManager {
|
||||
private val _contractorSpecialtiesMap = MutableStateFlow<Map<Int, ContractorSpecialty>>(emptyMap())
|
||||
val contractorSpecialtiesMap: StateFlow<Map<Int, ContractorSpecialty>> = _contractorSpecialtiesMap.asStateFlow()
|
||||
|
||||
// Home-profile field options (heating_type -> [options], etc.), served
|
||||
// localized by the backend. Replaces the previously hardcoded client lists.
|
||||
private val _homeProfileOptions = MutableStateFlow<Map<String, List<HomeProfileOption>>>(emptyMap())
|
||||
val homeProfileOptions: StateFlow<Map<String, List<HomeProfileOption>>> = _homeProfileOptions.asStateFlow()
|
||||
|
||||
// Document type / category options ({value, display_name}), served localized.
|
||||
private val _documentTypes = MutableStateFlow<List<HomeProfileOption>>(emptyList())
|
||||
val documentTypes: StateFlow<List<HomeProfileOption>> = _documentTypes.asStateFlow()
|
||||
|
||||
private val _documentCategories = MutableStateFlow<List<HomeProfileOption>>(emptyList())
|
||||
val documentCategories: StateFlow<List<HomeProfileOption>> = _documentCategories.asStateFlow()
|
||||
|
||||
// ==================== STATE METADATA ====================
|
||||
|
||||
private val _isInitialized = MutableStateFlow(false)
|
||||
@@ -824,6 +836,15 @@ object DataManager : IDataManager {
|
||||
setTaskCategories(seededData.taskCategories)
|
||||
setContractorSpecialties(seededData.contractorSpecialties)
|
||||
setTaskTemplatesGrouped(seededData.taskTemplates)
|
||||
if (seededData.homeProfileOptions.isNotEmpty()) {
|
||||
_homeProfileOptions.value = seededData.homeProfileOptions
|
||||
}
|
||||
if (seededData.documentTypes.isNotEmpty()) {
|
||||
_documentTypes.value = seededData.documentTypes
|
||||
}
|
||||
if (seededData.documentCategories.isNotEmpty()) {
|
||||
_documentCategories.value = seededData.documentCategories
|
||||
}
|
||||
setSeededDataETag(etag)
|
||||
_lookupsInitialized.value = true
|
||||
// Persist lookups to disk for faster startup
|
||||
@@ -890,6 +911,9 @@ object DataManager : IDataManager {
|
||||
_taskCategoriesMap.value = emptyMap()
|
||||
_contractorSpecialties.value = emptyList()
|
||||
_contractorSpecialtiesMap.value = emptyMap()
|
||||
_homeProfileOptions.value = emptyMap()
|
||||
_documentTypes.value = emptyList()
|
||||
_documentCategories.value = emptyList()
|
||||
_taskTemplates.value = emptyList()
|
||||
_taskTemplatesGrouped.value = null
|
||||
_lookupsInitialized.value = false
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
package com.tt.honeyDue.i18n
|
||||
|
||||
import com.tt.honeyDue.network.getDeviceLanguage
|
||||
|
||||
/**
|
||||
* Lightweight, dependency-free client-side localization for the SHARED Kotlin
|
||||
* layer (errors, date words, fallback labels) that the iOS String Catalog and
|
||||
* Android resources can't reach because they originate in commonMain.
|
||||
*
|
||||
* Real API error messages are localized by the backend (Accept-Language); this
|
||||
* covers client-generated / offline / fallback copy only.
|
||||
*
|
||||
* Resolution: the device language (2-letter) from [getDeviceLanguage], falling
|
||||
* back to English. Use [t] for plain lookups and [t] with args for `{0}`-style
|
||||
* placeholder substitution.
|
||||
*/
|
||||
object ClientStrings {
|
||||
private val supported = setOf("en", "es", "fr", "de", "pt", "it", "ja", "ko", "nl", "zh")
|
||||
|
||||
private fun lang(): String {
|
||||
val l = getDeviceLanguage().lowercase().take(2)
|
||||
return if (l in supported) l else "en"
|
||||
}
|
||||
|
||||
/** Localized string for [key]; falls back to English, then to the key itself. */
|
||||
fun t(key: String): String {
|
||||
val byLang = strings[key] ?: return key
|
||||
return byLang[lang()] ?: byLang["en"] ?: key
|
||||
}
|
||||
|
||||
/** Localized string with `{0}`, `{1}`, … placeholder substitution. */
|
||||
fun t(key: String, vararg args: Any?): String {
|
||||
var s = t(key)
|
||||
args.forEachIndexed { i, a -> s = s.replace("{$i}", a.toString()) }
|
||||
return s
|
||||
}
|
||||
|
||||
// key -> (lang -> value). English is authoritative; other languages are
|
||||
// populated by the translation pipeline. GENERATED-BLOCK:strings
|
||||
private val strings: Map<String, Map<String, String>> = STRINGS
|
||||
}
|
||||
@@ -0,0 +1,232 @@
|
||||
package com.tt.honeyDue.i18n
|
||||
|
||||
// GENERATED string table for [ClientStrings]. English authoritative; other
|
||||
// languages machine-translated (pending native QA). Keep keys stable.
|
||||
internal val STRINGS: Map<String, Map<String, String>> = mapOf(
|
||||
"date.today" to mapOf("en" to "Today", "es" to "Hoy", "fr" to "Aujourd'hui", "de" to "Heute", "pt" to "Hoje", "it" to "Oggi", "ja" to "今日", "ko" to "오늘", "nl" to "Vandaag", "zh" to "今天"),
|
||||
"date.tomorrow" to mapOf("en" to "Tomorrow", "es" to "Mañana", "fr" to "Demain", "de" to "Morgen", "pt" to "Amanhã", "it" to "Domani", "ja" to "明日", "ko" to "내일", "nl" to "Morgen", "zh" to "明天"),
|
||||
"date.yesterday" to mapOf("en" to "Yesterday", "es" to "Ayer", "fr" to "Hier", "de" to "Gestern", "pt" to "Ontem", "it" to "Ieri", "ja" to "昨日", "ko" to "어제", "nl" to "Gisteren", "zh" to "昨天"),
|
||||
"date.in_days" to mapOf("en" to "in {0} days", "es" to "en {0} días", "fr" to "dans {0} jours", "de" to "in {0} Tagen", "pt" to "em {0} dias", "it" to "tra {0} giorni", "ja" to "{0}日後", "ko" to "{0}일 후", "nl" to "over {0} dagen", "zh" to "{0}天后"),
|
||||
"date.days_ago" to mapOf("en" to "{0} days ago", "es" to "hace {0} días", "fr" to "il y a {0} jours", "de" to "vor {0} Tagen", "pt" to "há {0} dias", "it" to "{0} giorni fa", "ja" to "{0}日前", "ko" to "{0}일 전", "nl" to "{0} dagen geleden", "zh" to "{0}天前"),
|
||||
"date.at" to mapOf("en" to "at", "es" to "a las", "fr" to "à", "de" to "um", "pt" to "às", "it" to "alle", "ja" to "", "ko" to "", "nl" to "om", "zh" to ""),
|
||||
"time.am" to mapOf("en" to "AM", "es" to "AM", "fr" to "AM", "de" to "AM", "pt" to "AM", "it" to "AM", "ja" to "午前", "ko" to "오전", "nl" to "AM", "zh" to "上午"),
|
||||
"time.pm" to mapOf("en" to "PM", "es" to "PM", "fr" to "PM", "de" to "PM", "pt" to "PM", "it" to "PM", "ja" to "午後", "ko" to "오후", "nl" to "PM", "zh" to "下午"),
|
||||
"month.1" to mapOf("en" to "Jan", "es" to "ene", "fr" to "janv.", "de" to "Jan", "pt" to "jan", "it" to "gen", "ja" to "1月", "ko" to "1월", "nl" to "jan", "zh" to "1月"),
|
||||
"month.2" to mapOf("en" to "Feb", "es" to "feb", "fr" to "févr.", "de" to "Feb", "pt" to "fev", "it" to "feb", "ja" to "2月", "ko" to "2월", "nl" to "feb", "zh" to "2月"),
|
||||
"month.3" to mapOf("en" to "Mar", "es" to "mar", "fr" to "mars", "de" to "Mär", "pt" to "mar", "it" to "mar", "ja" to "3月", "ko" to "3월", "nl" to "mrt", "zh" to "3月"),
|
||||
"month.4" to mapOf("en" to "Apr", "es" to "abr", "fr" to "avr.", "de" to "Apr", "pt" to "abr", "it" to "apr", "ja" to "4月", "ko" to "4월", "nl" to "apr", "zh" to "4月"),
|
||||
"month.5" to mapOf("en" to "May", "es" to "may", "fr" to "mai", "de" to "Mai", "pt" to "mai", "it" to "mag", "ja" to "5月", "ko" to "5월", "nl" to "mei", "zh" to "5月"),
|
||||
"month.6" to mapOf("en" to "Jun", "es" to "jun", "fr" to "juin", "de" to "Jun", "pt" to "jun", "it" to "giu", "ja" to "6月", "ko" to "6월", "nl" to "jun", "zh" to "6月"),
|
||||
"month.7" to mapOf("en" to "Jul", "es" to "jul", "fr" to "juil.", "de" to "Jul", "pt" to "jul", "it" to "lug", "ja" to "7月", "ko" to "7월", "nl" to "jul", "zh" to "7月"),
|
||||
"month.8" to mapOf("en" to "Aug", "es" to "ago", "fr" to "août", "de" to "Aug", "pt" to "ago", "it" to "ago", "ja" to "8月", "ko" to "8월", "nl" to "aug", "zh" to "8月"),
|
||||
"month.9" to mapOf("en" to "Sep", "es" to "sep", "fr" to "sept.", "de" to "Sep", "pt" to "set", "it" to "set", "ja" to "9月", "ko" to "9월", "nl" to "sep", "zh" to "9月"),
|
||||
"month.10" to mapOf("en" to "Oct", "es" to "oct", "fr" to "oct.", "de" to "Okt", "pt" to "out", "it" to "ott", "ja" to "10月", "ko" to "10월", "nl" to "okt", "zh" to "10月"),
|
||||
"month.11" to mapOf("en" to "Nov", "es" to "nov", "fr" to "nov.", "de" to "Nov", "pt" to "nov", "it" to "nov", "ja" to "11月", "ko" to "11월", "nl" to "nov", "zh" to "11月"),
|
||||
"month.12" to mapOf("en" to "Dec", "es" to "dic", "fr" to "déc.", "de" to "Dez", "pt" to "dez", "it" to "dic", "ja" to "12月", "ko" to "12월", "nl" to "dec", "zh" to "12月"),
|
||||
"err.unknown" to mapOf("en" to "Unknown error", "es" to "Error desconocido", "fr" to "Erreur inconnue", "de" to "Unbekannter Fehler", "pt" to "Erro desconhecido", "it" to "Errore sconosciuto", "ja" to "不明なエラー", "ko" to "알 수 없는 오류", "nl" to "Onbekende fout", "zh" to "未知错误"),
|
||||
"err.unknown_occurred" to mapOf("en" to "Unknown error occurred", "es" to "Ocurrió un error desconocido", "fr" to "Une erreur inconnue est survenue", "de" to "Ein unbekannter Fehler ist aufgetreten", "pt" to "Ocorreu um erro desconhecido", "it" to "Si è verificato un errore sconosciuto", "ja" to "不明なエラーが発生しました", "ko" to "알 수 없는 오류가 발생했습니다", "nl" to "Er is een onbekende fout opgetreden", "zh" to "发生未知错误"),
|
||||
"err.not_authenticated" to mapOf("en" to "Not authenticated", "es" to "No autenticado", "fr" to "Non authentifié", "de" to "Nicht angemeldet", "pt" to "Não autenticado", "it" to "Non autenticato", "ja" to "認証されていません", "ko" to "인증되지 않았습니다", "nl" to "Niet ingelogd", "zh" to "未通过身份验证"),
|
||||
"err.generic" to mapOf("en" to "Something went wrong. Please try again.", "es" to "Algo salió mal. Inténtalo de nuevo.", "fr" to "Une erreur s'est produite. Veuillez réessayer.", "de" to "Etwas ist schiefgelaufen. Bitte versuche es erneut.", "pt" to "Algo deu errado. Tente novamente.", "it" to "Qualcosa è andato storto. Riprova.", "ja" to "問題が発生しました。もう一度お試しください。", "ko" to "문제가 발생했습니다. 다시 시도해 주세요.", "nl" to "Er is iets misgegaan. Probeer het opnieuw.", "zh" to "出错了,请重试。"),
|
||||
"err.generic_retry" to mapOf("en" to "An error occurred. Please try again.", "es" to "Ocurrió un error. Inténtalo de nuevo.", "fr" to "Une erreur est survenue. Veuillez réessayer.", "de" to "Ein Fehler ist aufgetreten. Bitte versuche es erneut.", "pt" to "Ocorreu um erro. Tente novamente.", "it" to "Si è verificato un errore. Riprova.", "ja" to "エラーが発生しました。もう一度お試しください。", "ko" to "오류가 발생했습니다. 다시 시도해 주세요.", "nl" to "Er is een fout opgetreden. Probeer het opnieuw.", "zh" to "发生错误,请重试。"),
|
||||
"err.request_failed" to mapOf("en" to "Request failed. Please check your input and try again.", "es" to "La solicitud falló. Revisa los datos e inténtalo de nuevo.", "fr" to "Échec de la requête. Vérifiez votre saisie et réessayez.", "de" to "Anfrage fehlgeschlagen. Bitte überprüfe deine Eingabe und versuche es erneut.", "pt" to "Falha na solicitação. Verifique os dados e tente novamente.", "it" to "Richiesta non riuscita. Controlla i dati inseriti e riprova.", "ja" to "リクエストに失敗しました。入力内容を確認してもう一度お試しください。", "ko" to "요청에 실패했습니다. 입력 내용을 확인하고 다시 시도해 주세요.", "nl" to "Verzoek mislukt. Controleer je invoer en probeer het opnieuw.", "zh" to "请求失败,请检查输入后重试。"),
|
||||
"err.net.no_connection" to mapOf("en" to "Unable to connect to the server. Please check your internet connection.", "es" to "No se pudo conectar con el servidor. Revisa tu conexión a internet.", "fr" to "Impossible de se connecter au serveur. Vérifiez votre connexion Internet.", "de" to "Verbindung zum Server nicht möglich. Bitte überprüfe deine Internetverbindung.", "pt" to "Não foi possível conectar ao servidor. Verifique sua conexão com a internet.", "it" to "Impossibile connettersi al server. Controlla la connessione a Internet.", "ja" to "サーバーに接続できません。インターネット接続を確認してください。", "ko" to "서버에 연결할 수 없습니다. 인터넷 연결을 확인해 주세요.", "nl" to "Kan geen verbinding maken met de server. Controleer je internetverbinding.", "zh" to "无法连接到服务器,请检查网络连接。"),
|
||||
"err.net.timeout" to mapOf("en" to "Request timed out. Please try again.", "es" to "Se agotó el tiempo de espera. Inténtalo de nuevo.", "fr" to "Délai d'attente dépassé. Veuillez réessayer.", "de" to "Zeitüberschreitung der Anfrage. Bitte versuche es erneut.", "pt" to "A solicitação expirou. Tente novamente.", "it" to "Richiesta scaduta. Riprova.", "ja" to "リクエストがタイムアウトしました。もう一度お試しください。", "ko" to "요청 시간이 초과되었습니다. 다시 시도해 주세요.", "nl" to "Verzoek verlopen. Probeer het opnieuw.", "zh" to "请求超时,请重试。"),
|
||||
"err.net.no_internet" to mapOf("en" to "No internet connection. Please check your network settings.", "es" to "Sin conexión a internet. Revisa la configuración de tu red.", "fr" to "Pas de connexion Internet. Vérifiez vos paramètres réseau.", "de" to "Keine Internetverbindung. Bitte überprüfe deine Netzwerkeinstellungen.", "pt" to "Sem conexão com a internet. Verifique as configurações de rede.", "it" to "Nessuna connessione a Internet. Controlla le impostazioni di rete.", "ja" to "インターネットに接続されていません。ネットワーク設定を確認してください。", "ko" to "인터넷 연결이 없습니다. 네트워크 설정을 확인해 주세요.", "nl" to "Geen internetverbinding. Controleer je netwerkinstellingen.", "zh" to "无网络连接,请检查网络设置。"),
|
||||
"err.net.server_down" to mapOf("en" to "Unable to connect to the server. The server may be down.", "es" to "No se pudo conectar con el servidor. Es posible que esté caído.", "fr" to "Impossible de se connecter au serveur. Le serveur est peut-être indisponible.", "de" to "Verbindung zum Server nicht möglich. Der Server ist möglicherweise nicht erreichbar.", "pt" to "Não foi possível conectar ao servidor. O servidor pode estar fora do ar.", "it" to "Impossibile connettersi al server. Il server potrebbe non essere disponibile.", "ja" to "サーバーに接続できません。サーバーがダウンしている可能性があります。", "ko" to "서버에 연결할 수 없습니다. 서버가 다운되었을 수 있습니다.", "nl" to "Kan geen verbinding maken met de server. De server is mogelijk offline.", "zh" to "无法连接到服务器,服务器可能已停止运行。"),
|
||||
"err.net.interrupted" to mapOf("en" to "Connection was interrupted. Please try again.", "es" to "Se interrumpió la conexión. Inténtalo de nuevo.", "fr" to "La connexion a été interrompue. Veuillez réessayer.", "de" to "Die Verbindung wurde unterbrochen. Bitte versuche es erneut.", "pt" to "A conexão foi interrompida. Tente novamente.", "it" to "La connessione è stata interrotta. Riprova.", "ja" to "接続が中断されました。もう一度お試しください。", "ko" to "연결이 중단되었습니다. 다시 시도해 주세요.", "nl" to "Verbinding werd onderbroken. Probeer het opnieuw.", "zh" to "连接已中断,请重试。"),
|
||||
"err.net.ssl" to mapOf("en" to "Secure connection failed. Please try again.", "es" to "Falló la conexión segura. Inténtalo de nuevo.", "fr" to "Échec de la connexion sécurisée. Veuillez réessayer.", "de" to "Sichere Verbindung fehlgeschlagen. Bitte versuche es erneut.", "pt" to "Falha na conexão segura. Tente novamente.", "it" to "Connessione sicura non riuscita. Riprova.", "ja" to "セキュア接続に失敗しました。もう一度お試しください。", "ko" to "보안 연결에 실패했습니다. 다시 시도해 주세요.", "nl" to "Beveiligde verbinding mislukt. Probeer het opnieuw.", "zh" to "安全连接失败,请重试。"),
|
||||
"err.net.generic" to mapOf("en" to "Connection error. Please try again.", "es" to "Error de conexión. Inténtalo de nuevo.", "fr" to "Erreur de connexion. Veuillez réessayer.", "de" to "Verbindungsfehler. Bitte versuche es erneut.", "pt" to "Erro de conexão. Tente novamente.", "it" to "Errore di connessione. Riprova.", "ja" to "接続エラーです。もう一度お試しください。", "ko" to "연결 오류입니다. 다시 시도해 주세요.", "nl" to "Verbindingsfout. Probeer het opnieuw.", "zh" to "连接出错,请重试。"),
|
||||
"err.too_many_requests" to mapOf("en" to "Too many requests. Please try again later.", "es" to "Demasiadas solicitudes. Inténtalo más tarde.", "fr" to "Trop de requêtes. Réessayez plus tard.", "de" to "Zu viele Anfragen. Bitte versuche es später erneut.", "pt" to "Muitas solicitações. Tente novamente mais tarde.", "it" to "Troppe richieste. Riprova più tardi.", "ja" to "リクエストが多すぎます。しばらくしてからもう一度お試しください。", "ko" to "요청이 너무 많습니다. 잠시 후 다시 시도해 주세요.", "nl" to "Te veel verzoeken. Probeer het later opnieuw.", "zh" to "请求过于频繁,请稍后重试。"),
|
||||
"err.too_many_requests_retry" to mapOf("en" to "Too many requests. Please try again in {0} seconds.", "es" to "Demasiadas solicitudes. Inténtalo de nuevo en {0} segundos.", "fr" to "Trop de requêtes. Réessayez dans {0} secondes.", "de" to "Zu viele Anfragen. Bitte versuche es in {0} Sekunden erneut.", "pt" to "Muitas solicitações. Tente novamente em {0} segundos.", "it" to "Troppe richieste. Riprova tra {0} secondi.", "ja" to "リクエストが多すぎます。{0}秒後にもう一度お試しください。", "ko" to "요청이 너무 많습니다. {0}초 후 다시 시도해 주세요.", "nl" to "Te veel verzoeken. Probeer het over {0} seconden opnieuw.", "zh" to "请求过于频繁,请在{0}秒后重试。"),
|
||||
"err.details" to mapOf("en" to "Details:", "es" to "Detalles:", "fr" to "Détails :", "de" to "Details:", "pt" to "Detalhes:", "it" to "Dettagli:", "ja" to "詳細:", "ko" to "세부 정보:", "nl" to "Details:", "zh" to "详情:"),
|
||||
"err.with_status" to mapOf("en" to "An error occurred ({0})", "es" to "Ocurrió un error ({0})", "fr" to "Une erreur s'est produite ({0})", "de" to "Ein Fehler ist aufgetreten ({0})", "pt" to "Ocorreu um erro ({0})", "it" to "Si è verificato un errore ({0})", "ja" to "エラーが発生しました({0})", "ko" to "오류가 발생했습니다 ({0})", "nl" to "Er is een fout opgetreden ({0})", "zh" to "发生错误({0})"),
|
||||
"upload.too_large" to mapOf("en" to "That photo is too large after resizing.", "es" to "Esa foto es demasiado grande después de redimensionarla.", "fr" to "Cette photo est trop volumineuse après redimensionnement.", "de" to "Dieses Foto ist auch nach dem Verkleinern zu groß.", "pt" to "Essa foto está muito grande após o redimensionamento.", "it" to "La foto è troppo grande dopo il ridimensionamento.", "ja" to "リサイズ後もこの写真は大きすぎます。", "ko" to "크기 조정 후에도 사진이 너무 큽니다.", "nl" to "Die foto is te groot na het verkleinen.", "zh" to "该照片压缩后仍过大。"),
|
||||
"upload.unsupported" to mapOf("en" to "That image format isn't supported.", "es" to "Ese formato de imagen no es compatible.", "fr" to "Ce format d'image n'est pas pris en charge.", "de" to "Dieses Bildformat wird nicht unterstützt.", "pt" to "Esse formato de imagem não é compatível.", "it" to "Questo formato immagine non è supportato.", "ja" to "この画像形式はサポートされていません。", "ko" to "지원되지 않는 이미지 형식입니다.", "nl" to "Dat afbeeldingsformaat wordt niet ondersteund.", "zh" to "不支持该图片格式。"),
|
||||
"upload.too_many" to mapOf("en" to "Too many uploads in flight; try again shortly.", "es" to "Demasiadas subidas en curso; inténtalo de nuevo en un momento.", "fr" to "Trop d'envois en cours ; réessayez sous peu.", "de" to "Zu viele Uploads gleichzeitig. Bitte versuche es gleich erneut.", "pt" to "Muitos envios em andamento; tente novamente em instantes.", "it" to "Troppi caricamenti in corso; riprova tra poco.", "ja" to "アップロード中のファイルが多すぎます。しばらくしてからお試しください。", "ko" to "업로드가 너무 많습니다. 잠시 후 다시 시도해 주세요.", "nl" to "Te veel uploads tegelijk; probeer het zo opnieuw.", "zh" to "上传任务过多,请稍后再试。"),
|
||||
"upload.start_failed" to mapOf("en" to "Couldn't start upload.", "es" to "No se pudo iniciar la subida.", "fr" to "Impossible de démarrer l'envoi.", "de" to "Upload konnte nicht gestartet werden.", "pt" to "Não foi possível iniciar o envio.", "it" to "Impossibile avviare il caricamento.", "ja" to "アップロードを開始できませんでした。", "ko" to "업로드를 시작할 수 없습니다.", "nl" to "Kon upload niet starten.", "zh" to "无法开始上传。"),
|
||||
"upload.storage_failed" to mapOf("en" to "Upload to storage failed.", "es" to "Falló la subida al almacenamiento.", "fr" to "Échec de l'envoi vers le stockage.", "de" to "Upload zum Speicher fehlgeschlagen.", "pt" to "Falha no envio para o armazenamento.", "it" to "Caricamento nello spazio di archiviazione non riuscito.", "ja" to "ストレージへのアップロードに失敗しました。", "ko" to "저장소 업로드에 실패했습니다.", "nl" to "Uploaden naar opslag mislukt.", "zh" to "上传至存储失败。"),
|
||||
"upload.net_presign" to mapOf("en" to "Network error during presign.", "es" to "Error de red durante la autorización.", "fr" to "Erreur réseau lors de la pré-signature.", "de" to "Netzwerkfehler bei der Vorbereitung.", "pt" to "Erro de rede durante a pré-assinatura.", "it" to "Errore di rete durante la pre-autorizzazione.", "ja" to "署名取得中にネットワークエラーが発生しました。", "ko" to "사전 서명 중 네트워크 오류가 발생했습니다.", "nl" to "Netwerkfout tijdens presign.", "zh" to "预签名时网络出错。"),
|
||||
"upload.net_upload" to mapOf("en" to "Network error during upload.", "es" to "Error de red durante la subida.", "fr" to "Erreur réseau lors de l'envoi.", "de" to "Netzwerkfehler beim Upload.", "pt" to "Erro de rede durante o envio.", "it" to "Errore di rete durante il caricamento.", "ja" to "アップロード中にネットワークエラーが発生しました。", "ko" to "업로드 중 네트워크 오류가 발생했습니다.", "nl" to "Netwerkfout tijdens uploaden.", "zh" to "上传时网络出错。"),
|
||||
"upload.failed" to mapOf("en" to "Upload failed.", "es" to "La subida falló.", "fr" to "Échec de l'envoi.", "de" to "Upload fehlgeschlagen.", "pt" to "Falha no envio.", "it" to "Caricamento non riuscito.", "ja" to "アップロードに失敗しました。", "ko" to "업로드에 실패했습니다.", "nl" to "Uploaden mislukt.", "zh" to "上传失败。"),
|
||||
"task.frequency.one_time" to mapOf("en" to "One time", "es" to "Una vez", "fr" to "Une fois", "de" to "Einmalig", "pt" to "Uma vez", "it" to "Una volta", "ja" to "1回のみ", "ko" to "한 번", "nl" to "Eenmalig", "zh" to "一次性"),
|
||||
"task.category.uncategorized" to mapOf("en" to "Uncategorized", "es" to "Sin categoría", "fr" to "Non classé", "de" to "Ohne Kategorie", "pt" to "Sem categoria", "it" to "Senza categoria", "ja" to "未分類", "ko" to "분류 없음", "nl" to "Niet ingedeeld", "zh" to "未分类"),
|
||||
"err.api.failed_fetch_completions" to mapOf("en" to "Failed to fetch completions", "es" to "No se pudieron obtener las finalizaciones", "fr" to "Échec de la récupération des achèvements", "de" to "Erledigungen konnten nicht abgerufen werden", "pt" to "Falha ao buscar as conclusões", "it" to "Impossibile recuperare i completamenti", "ja" to "完了履歴の取得に失敗しました", "ko" to "완료 내역을 가져오지 못했습니다", "nl" to "Voltooiingen ophalen mislukt", "zh" to "获取完成记录失败"),
|
||||
"err.api.failed_fetch_completion" to mapOf("en" to "Failed to fetch completion", "es" to "No se pudo obtener la finalización", "fr" to "Échec de la récupération de l'achèvement", "de" to "Erledigung konnte nicht abgerufen werden", "pt" to "Falha ao buscar a conclusão", "it" to "Impossibile recuperare il completamento", "ja" to "完了履歴の取得に失敗しました", "ko" to "완료 내역을 가져오지 못했습니다", "nl" to "Voltooiing ophalen mislukt", "zh" to "获取完成记录失败"),
|
||||
"err.api.failed_create_completion" to mapOf("en" to "Failed to create completion", "es" to "No se pudo crear la finalización", "fr" to "Échec de la création de l'achèvement", "de" to "Erledigung konnte nicht erstellt werden", "pt" to "Falha ao criar a conclusão", "it" to "Impossibile creare il completamento", "ja" to "完了の作成に失敗しました", "ko" to "완료 내역을 생성하지 못했습니다", "nl" to "Voltooiing aanmaken mislukt", "zh" to "创建完成记录失败"),
|
||||
"err.api.failed_update_completion" to mapOf("en" to "Failed to update completion", "es" to "No se pudo actualizar la finalización", "fr" to "Échec de la mise à jour de l'achèvement", "de" to "Erledigung konnte nicht aktualisiert werden", "pt" to "Falha ao atualizar a conclusão", "it" to "Impossibile aggiornare il completamento", "ja" to "完了の更新に失敗しました", "ko" to "완료 내역을 업데이트하지 못했습니다", "nl" to "Voltooiing bijwerken mislukt", "zh" to "更新完成记录失败"),
|
||||
"err.api.failed_delete_completion" to mapOf("en" to "Failed to delete completion", "es" to "No se pudo eliminar la finalización", "fr" to "Échec de la suppression de l'achèvement", "de" to "Erledigung konnte nicht gelöscht werden", "pt" to "Falha ao excluir a conclusão", "it" to "Impossibile eliminare il completamento", "ja" to "完了の削除に失敗しました", "ko" to "완료 내역을 삭제하지 못했습니다", "nl" to "Voltooiing verwijderen mislukt", "zh" to "删除完成记录失败"),
|
||||
"err.api.failed_fetch_residence_types" to mapOf("en" to "Failed to fetch residence types", "es" to "No se pudieron obtener los tipos de residencia", "fr" to "Échec de la récupération des types de résidence", "de" to "Objekttypen konnten nicht abgerufen werden", "pt" to "Falha ao buscar os tipos de imóvel", "it" to "Impossibile recuperare i tipi di residenza", "ja" to "住居タイプの取得に失敗しました", "ko" to "주거 유형을 가져오지 못했습니다", "nl" to "Woningtypen ophalen mislukt", "zh" to "获取住宅类型失败"),
|
||||
"err.api.failed_fetch_task_frequencies" to mapOf("en" to "Failed to fetch task frequencies", "es" to "No se pudieron obtener las frecuencias de tareas", "fr" to "Échec de la récupération des fréquences de t—ches", "de" to "Aufgabenhäufigkeiten konnten nicht abgerufen werden", "pt" to "Falha ao buscar as frequências de tarefa", "it" to "Impossibile recuperare le frequenze delle attività", "ja" to "タスク頻度の取得に失敗しました", "ko" to "작업 주기를 가져오지 못했습니다", "nl" to "Taakfrequenties ophalen mislukt", "zh" to "获取任务频率失败"),
|
||||
"err.api.failed_fetch_task_priorities" to mapOf("en" to "Failed to fetch task priorities", "es" to "No se pudieron obtener las prioridades de tareas", "fr" to "Échec de la récupération des priorités de t—ches", "de" to "Aufgabenprioritäten konnten nicht abgerufen werden", "pt" to "Falha ao buscar as prioridades de tarefa", "it" to "Impossibile recuperare le priorità delle attività", "ja" to "タスク優先度の取得に失敗しました", "ko" to "작업 우선순위를 가져오지 못했습니다", "nl" to "Taakprioriteiten ophalen mislukt", "zh" to "获取任务优先级失败"),
|
||||
"err.api.failed_fetch_task_categories" to mapOf("en" to "Failed to fetch task categories", "es" to "No se pudieron obtener las categorías de tareas", "fr" to "Échec de la récupération des catégories de t—ches", "de" to "Aufgabenkategorien konnten nicht abgerufen werden", "pt" to "Falha ao buscar as categorias de tarefa", "it" to "Impossibile recuperare le categorie delle attività", "ja" to "タスクカテゴリの取得に失敗しました", "ko" to "작업 카테고리를 가져오지 못했습니다", "nl" to "Taakcategorieën ophalen mislukt", "zh" to "获取任务分类失败"),
|
||||
"err.api.failed_fetch_contractor_specialties" to mapOf("en" to "Failed to fetch contractor specialties", "es" to "No se pudieron obtener las especialidades de contratistas", "fr" to "Échec de la récupération des spécialités des prestataires", "de" to "Handwerker-Fachgebiete konnten nicht abgerufen werden", "pt" to "Falha ao buscar as especialidades dos prestadores", "it" to "Impossibile recuperare le specializzazioni dei tecnici", "ja" to "業者の専門分野の取得に失敗しました", "ko" to "업체 전문 분야를 가져오지 못했습니다", "nl" to "Specialismen van vakmensen ophalen mislukt", "zh" to "获取承包商专长失败"),
|
||||
"err.api.failed_fetch_static_data" to mapOf("en" to "Failed to fetch static data", "es" to "No se pudieron obtener los datos estáticos", "fr" to "Échec de la récupération des données statiques", "de" to "Statische Daten konnten nicht abgerufen werden", "pt" to "Falha ao buscar os dados estáticos", "it" to "Impossibile recuperare i dati statici", "ja" to "静的データの取得に失敗しました", "ko" to "정적 데이터를 가져오지 못했습니다", "nl" to "Statische gegevens ophalen mislukt", "zh" to "获取静态数据失败"),
|
||||
"err.api.failed_fetch_seeded_data" to mapOf("en" to "Failed to fetch seeded data", "es" to "No se pudieron obtener los datos iniciales", "fr" to "Échec de la récupération des données initiales", "de" to "Vorinitialisierte Daten konnten nicht abgerufen werden", "pt" to "Falha ao buscar os dados iniciais", "it" to "Impossibile recuperare i dati iniziali", "ja" to "初期データの取得に失敗しました", "ko" to "초기 데이터를 가져오지 못했습니다", "nl" to "Vooraf ingevulde gegevens ophalen mislukt", "zh" to "获取初始数据失败"),
|
||||
"err.api.failed_fetch_task_templates" to mapOf("en" to "Failed to fetch task templates", "es" to "No se pudieron obtener las plantillas de tareas", "fr" to "Échec de la récupération des modèles de t—ches", "de" to "Aufgabenvorlagen konnten nicht abgerufen werden", "pt" to "Falha ao buscar os modelos de tarefa", "it" to "Impossibile recuperare i modelli di attività", "ja" to "タスクテンプレートの取得に失敗しました", "ko" to "작업 템플릿을 가져오지 못했습니다", "nl" to "Taaksjablonen ophalen mislukt", "zh" to "获取任务模板失败"),
|
||||
"err.api.failed_fetch_grouped_task_templates" to mapOf("en" to "Failed to fetch grouped task templates", "es" to "No se pudieron obtener las plantillas de tareas agrupadas", "fr" to "Échec de la récupération des modèles de t—ches groupés", "de" to "Gruppierte Aufgabenvorlagen konnten nicht abgerufen werden", "pt" to "Falha ao buscar os modelos de tarefa agrupados", "it" to "Impossibile recuperare i modelli di attività raggruppati", "ja" to "グループ化されたタスクテンプレートの取得に失敗しました", "ko" to "그룹화된 작업 템플릿을 가져오지 못했습니다", "nl" to "Gegroepeerde taaksjablonen ophalen mislukt", "zh" to "获取分组任务模板失败"),
|
||||
"err.api.failed_search_task_templates" to mapOf("en" to "Failed to search task templates", "es" to "No se pudieron buscar las plantillas de tareas", "fr" to "Échec de la recherche des modèles de t—ches", "de" to "Aufgabenvorlagen konnten nicht durchsucht werden", "pt" to "Falha ao pesquisar os modelos de tarefa", "it" to "Impossibile cercare i modelli di attività", "ja" to "タスクテンプレートの検索に失敗しました", "ko" to "작업 템플릿을 검색하지 못했습니다", "nl" to "Zoeken naar taaksjablonen mislukt", "zh" to "搜索任务模板失败"),
|
||||
"err.api.failed_fetch_templates_by_category" to mapOf("en" to "Failed to fetch templates by category", "es" to "No se pudieron obtener las plantillas por categoría", "fr" to "Échec de la récupération des modèles par catégorie", "de" to "Vorlagen nach Kategorie konnten nicht abgerufen werden", "pt" to "Falha ao buscar os modelos por categoria", "it" to "Impossibile recuperare i modelli per categoria", "ja" to "カテゴリ別テンプレートの取得に失敗しました", "ko" to "카테고리별 템플릿을 가져오지 못했습니다", "nl" to "Sjablonen per categorie ophalen mislukt", "zh" to "按分类获取模板失败"),
|
||||
"err.api.failed_fetch_task_suggestions" to mapOf("en" to "Failed to fetch task suggestions", "es" to "No se pudieron obtener las sugerencias de tareas", "fr" to "Échec de la récupération des suggestions de t—ches", "de" to "Aufgabenvorschläge konnten nicht abgerufen werden", "pt" to "Falha ao buscar as sugestões de tarefa", "it" to "Impossibile recuperare i suggerimenti di attività", "ja" to "タスクの提案の取得に失敗しました", "ko" to "작업 추천을 가져오지 못했습니다", "nl" to "Taaksuggesties ophalen mislukt", "zh" to "获取任务建议失败"),
|
||||
"err.api.template_not_found" to mapOf("en" to "Template not found", "es" to "Plantilla no encontrada", "fr" to "Modèle introuvable", "de" to "Vorlage nicht gefunden", "pt" to "Modelo não encontrado", "it" to "Modello non trovato", "ja" to "テンプレートが見つかりません", "ko" to "템플릿을 찾을 수 없습니다", "nl" to "Sjabloon niet gevonden", "zh" to "未找到模板"),
|
||||
"err.api.failed_fetch_residences" to mapOf("en" to "Failed to fetch residences", "es" to "No se pudieron obtener las residencias", "fr" to "Échec de la récupération des résidences", "de" to "Objekte konnten nicht abgerufen werden", "pt" to "Falha ao buscar os imóveis", "it" to "Impossibile recuperare le residenze", "ja" to "住居の取得に失敗しました", "ko" to "주거지를 가져오지 못했습니다", "nl" to "Woningen ophalen mislukt", "zh" to "获取住宅失败"),
|
||||
"err.api.failed_fetch_residence" to mapOf("en" to "Failed to fetch residence", "es" to "No se pudo obtener la residencia", "fr" to "Échec de la récupération de la résidence", "de" to "Objekt konnte nicht abgerufen werden", "pt" to "Falha ao buscar o imóvel", "it" to "Impossibile recuperare la residenza", "ja" to "住居の取得に失敗しました", "ko" to "주거지를 가져오지 못했습니다", "nl" to "Woning ophalen mislukt", "zh" to "获取住宅失败"),
|
||||
"err.api.failed_create_residence" to mapOf("en" to "Failed to create residence", "es" to "No se pudo crear la residencia", "fr" to "Échec de la création de la résidence", "de" to "Objekt konnte nicht erstellt werden", "pt" to "Falha ao criar o imóvel", "it" to "Impossibile creare la residenza", "ja" to "住居の作成に失敗しました", "ko" to "주거지를 생성하지 못했습니다", "nl" to "Woning aanmaken mislukt", "zh" to "创建住宅失败"),
|
||||
"err.api.failed_update_residence" to mapOf("en" to "Failed to update residence", "es" to "No se pudo actualizar la residencia", "fr" to "Échec de la mise à jour de la résidence", "de" to "Objekt konnte nicht aktualisiert werden", "pt" to "Falha ao atualizar o imóvel", "it" to "Impossibile aggiornare la residenza", "ja" to "住居の更新に失敗しました", "ko" to "주거지를 업데이트하지 못했습니다", "nl" to "Woning bijwerken mislukt", "zh" to "更新住宅失败"),
|
||||
"err.api.failed_delete_residence" to mapOf("en" to "Failed to delete residence", "es" to "No se pudo eliminar la residencia", "fr" to "Échec de la suppression de la résidence", "de" to "Objekt konnte nicht gelöscht werden", "pt" to "Falha ao excluir o imóvel", "it" to "Impossibile eliminare la residenza", "ja" to "住居の削除に失敗しました", "ko" to "주거지를 삭제하지 못했습니다", "nl" to "Woning verwijderen mislukt", "zh" to "删除住宅失败"),
|
||||
"err.api.failed_fetch_summary" to mapOf("en" to "Failed to fetch summary", "es" to "No se pudo obtener el resumen", "fr" to "Échec de la récupération du résumé", "de" to "Übersicht konnte nicht abgerufen werden", "pt" to "Falha ao buscar o resumo", "it" to "Impossibile recuperare il riepilogo", "ja" to "サマリーの取得に失敗しました", "ko" to "요약을 가져오지 못했습니다", "nl" to "Overzicht ophalen mislukt", "zh" to "获取摘要失败"),
|
||||
"err.api.failed_fetch_my_residences" to mapOf("en" to "Failed to fetch my residences", "es" to "No se pudieron obtener mis residencias", "fr" to "Échec de la récupération de mes résidences", "de" to "Meine Objekte konnten nicht abgerufen werden", "pt" to "Falha ao buscar os meus imóveis", "it" to "Impossibile recuperare le mie residenze", "ja" to "自分の住居の取得に失敗しました", "ko" to "내 주거지를 가져오지 못했습니다", "nl" to "Mijn woningen ophalen mislukt", "zh" to "获取我的住宅失败"),
|
||||
"err.api.failed_fetch_subscription_status" to mapOf("en" to "Failed to fetch subscription status", "es" to "No se pudo obtener el estado de la suscripción", "fr" to "Échec de la récupération du statut de l'abonnement", "de" to "Abo-Status konnte nicht abgerufen werden", "pt" to "Falha ao buscar o status da assinatura", "it" to "Impossibile recuperare lo stato dell'abbonamento", "ja" to "サブスクリプション状況の取得に失敗しました", "ko" to "구독 상태를 가져오지 못했습니다", "nl" to "Abonnementsstatus ophalen mislukt", "zh" to "获取订阅状态失败"),
|
||||
"err.api.failed_fetch_upgrade_triggers" to mapOf("en" to "Failed to fetch upgrade triggers", "es" to "No se pudieron obtener los activadores de mejora", "fr" to "Échec de la récupération des déclencheurs de mise à niveau", "de" to "Upgrade-Auslöser konnten nicht abgerufen werden", "pt" to "Falha ao buscar os gatilhos de upgrade", "it" to "Impossibile recuperare i trigger di upgrade", "ja" to "アップグレードトリガーの取得に失敗しました", "ko" to "업그레이드 조건을 가져오지 못했습니다", "nl" to "Upgrade-triggers ophalen mislukt", "zh" to "获取升级触发条件失败"),
|
||||
"err.api.failed_fetch_feature_benefits" to mapOf("en" to "Failed to fetch feature benefits", "es" to "No se pudieron obtener los beneficios de las funciones", "fr" to "Échec de la récupération des avantages des fonctionnalités", "de" to "Funktionsvorteile konnten nicht abgerufen werden", "pt" to "Falha ao buscar os benefícios dos recursos", "it" to "Impossibile recuperare i vantaggi delle funzioni", "ja" to "機能の特典の取得に失敗しました", "ko" to "기능 혜택을 가져오지 못했습니다", "nl" to "Functievoordelen ophalen mislukt", "zh" to "获取功能权益失败"),
|
||||
"err.api.failed_fetch_promotions" to mapOf("en" to "Failed to fetch promotions", "es" to "No se pudieron obtener las promociones", "fr" to "Échec de la récupération des promotions", "de" to "Aktionen konnten nicht abgerufen werden", "pt" to "Falha ao buscar as promoções", "it" to "Impossibile recuperare le promozioni", "ja" to "プロモーションの取得に失敗しました", "ko" to "프로모션을 가져오지 못했습니다", "nl" to "Promoties ophalen mislukt", "zh" to "获取促销活动失败"),
|
||||
"err.api.failed_verify_ios_receipt" to mapOf("en" to "Failed to verify iOS receipt", "es" to "No se pudo verificar el recibo de iOS", "fr" to "Échec de la vérification du reçu iOS", "de" to "iOS-Beleg konnte nicht überprüft werden", "pt" to "Falha ao verificar o recibo do iOS", "it" to "Impossibile verificare la ricevuta iOS", "ja" to "iOSレシートの確認に失敗しました", "ko" to "iOS 영수증을 확인하지 못했습니다", "nl" to "iOS-bon verifiëren mislukt", "zh" to "验证 iOS 收据失败"),
|
||||
"err.api.failed_verify_android_purchase" to mapOf("en" to "Failed to verify Android purchase", "es" to "No se pudo verificar la compra de Android", "fr" to "Échec de la vérification de l'achat Android", "de" to "Android-Kauf konnte nicht überprüft werden", "pt" to "Falha ao verificar a compra do Android", "it" to "Impossibile verificare l'acquisto Android", "ja" to "Androidの購入の確認に失敗しました", "ko" to "Android 구매를 확인하지 못했습니다", "nl" to "Android-aankoop verifiëren mislukt", "zh" to "验证 Android 购买失败"),
|
||||
"err.api.failed_restore_subscription" to mapOf("en" to "Failed to restore subscription", "es" to "No se pudo restaurar la suscripción", "fr" to "Échec de la restauration de l'abonnement", "de" to "Abo konnte nicht wiederhergestellt werden", "pt" to "Falha ao restaurar a assinatura", "it" to "Impossibile ripristinare l'abbonamento", "ja" to "サブスクリプションの復元に失敗しました", "ko" to "구독을 복원하지 못했습니다", "nl" to "Abonnement herstellen mislukt", "zh" to "恢复订阅失败"),
|
||||
"err.api.failed_fetch_contractors" to mapOf("en" to "Failed to fetch contractors", "es" to "No se pudieron obtener los contratistas", "fr" to "Échec de la récupération des prestataires", "de" to "Handwerker konnten nicht abgerufen werden", "pt" to "Falha ao buscar os prestadores", "it" to "Impossibile recuperare i tecnici", "ja" to "業者の取得に失敗しました", "ko" to "업체를 가져오지 못했습니다", "nl" to "Vakmensen ophalen mislukt", "zh" to "获取承包商失败"),
|
||||
"err.api.failed_fetch_contractor" to mapOf("en" to "Failed to fetch contractor", "es" to "No se pudo obtener el contratista", "fr" to "Échec de la récupération du prestataire", "de" to "Handwerker konnte nicht abgerufen werden", "pt" to "Falha ao buscar o prestador", "it" to "Impossibile recuperare il tecnico", "ja" to "業者の取得に失敗しました", "ko" to "업체를 가져오지 못했습니다", "nl" to "Vakman ophalen mislukt", "zh" to "获取承包商失败"),
|
||||
"err.api.failed_create_contractor" to mapOf("en" to "Failed to create contractor", "es" to "No se pudo crear el contratista", "fr" to "Échec de la création du prestataire", "de" to "Handwerker konnte nicht erstellt werden", "pt" to "Falha ao criar o prestador", "it" to "Impossibile creare il tecnico", "ja" to "業者の作成に失敗しました", "ko" to "업체를 생성하지 못했습니다", "nl" to "Vakman aanmaken mislukt", "zh" to "创建承包商失败"),
|
||||
"err.api.failed_update_contractor" to mapOf("en" to "Failed to update contractor", "es" to "No se pudo actualizar el contratista", "fr" to "Échec de la mise à jour du prestataire", "de" to "Handwerker konnte nicht aktualisiert werden", "pt" to "Falha ao atualizar o prestador", "it" to "Impossibile aggiornare il tecnico", "ja" to "業者の更新に失敗しました", "ko" to "업체를 업데이트하지 못했습니다", "nl" to "Vakman bijwerken mislukt", "zh" to "更新承包商失败"),
|
||||
"err.api.failed_delete_contractor" to mapOf("en" to "Failed to delete contractor", "es" to "No se pudo eliminar el contratista", "fr" to "Échec de la suppression du prestataire", "de" to "Handwerker konnte nicht gelöscht werden", "pt" to "Falha ao excluir o prestador", "it" to "Impossibile eliminare il tecnico", "ja" to "業者の削除に失敗しました", "ko" to "업체를 삭제하지 못했습니다", "nl" to "Vakman verwijderen mislukt", "zh" to "删除承包商失败"),
|
||||
"err.api.failed_toggle_favorite" to mapOf("en" to "Failed to toggle favorite", "es" to "No se pudo cambiar el favorito", "fr" to "Échec de la modification du favori", "de" to "Favorit konnte nicht umgeschaltet werden", "pt" to "Falha ao alternar favorito", "it" to "Impossibile aggiornare i preferiti", "ja" to "お気に入りの切り替えに失敗しました", "ko" to "즐겨찾기를 변경하지 못했습니다", "nl" to "Favoriet wijzigen mislukt", "zh" to "切换收藏失败"),
|
||||
"err.api.failed_fetch_contractor_tasks" to mapOf("en" to "Failed to fetch contractor tasks", "es" to "No se pudieron obtener las tareas del contratista", "fr" to "Échec de la récupération des t—ches du prestataire", "de" to "Handwerker-Aufgaben konnten nicht abgerufen werden", "pt" to "Falha ao buscar as tarefas do prestador", "it" to "Impossibile recuperare le attività del tecnico", "ja" to "業者のタスクの取得に失敗しました", "ko" to "업체 작업을 가져오지 못했습니다", "nl" to "Taken van vakman ophalen mislukt", "zh" to "获取承包商任务失败"),
|
||||
"err.api.failed_fetch_contractors_for_residence" to mapOf("en" to "Failed to fetch contractors for residence", "es" to "No se pudieron obtener los contratistas de la residencia", "fr" to "Échec de la récupération des prestataires pour la résidence", "de" to "Handwerker für das Objekt konnten nicht abgerufen werden", "pt" to "Falha ao buscar os prestadores do imóvel", "it" to "Impossibile recuperare i tecnici per la residenza", "ja" to "住居の業者の取得に失敗しました", "ko" to "주거지의 업체를 가져오지 못했습니다", "nl" to "Vakmensen voor woning ophalen mislukt", "zh" to "获取该住宅的承包商失败"),
|
||||
"err.api.failed_fetch_documents" to mapOf("en" to "Failed to fetch documents", "es" to "No se pudieron obtener los documentos", "fr" to "Échec de la récupération des documents", "de" to "Dokumente konnten nicht abgerufen werden", "pt" to "Falha ao buscar os documentos", "it" to "Impossibile recuperare i documenti", "ja" to "ドキュメントの取得に失敗しました", "ko" to "문서를 가져오지 못했습니다", "nl" to "Documenten ophalen mislukt", "zh" to "获取文档失败"),
|
||||
"err.api.failed_fetch_document" to mapOf("en" to "Failed to fetch document", "es" to "No se pudo obtener el documento", "fr" to "Échec de la récupération du document", "de" to "Dokument konnte nicht abgerufen werden", "pt" to "Falha ao buscar o documento", "it" to "Impossibile recuperare il documento", "ja" to "ドキュメントの取得に失敗しました", "ko" to "문서를 가져오지 못했습니다", "nl" to "Document ophalen mislukt", "zh" to "获取文档失败"),
|
||||
"err.api.failed_create_document" to mapOf("en" to "Failed to create document", "es" to "No se pudo crear el documento", "fr" to "Échec de la création du document", "de" to "Dokument konnte nicht erstellt werden", "pt" to "Falha ao criar o documento", "it" to "Impossibile creare il documento", "ja" to "ドキュメントの作成に失敗しました", "ko" to "문서를 생성하지 못했습니다", "nl" to "Document aanmaken mislukt", "zh" to "创建文档失败"),
|
||||
"err.api.failed_update_document" to mapOf("en" to "Failed to update document", "es" to "No se pudo actualizar el documento", "fr" to "Échec de la mise à jour du document", "de" to "Dokument konnte nicht aktualisiert werden", "pt" to "Falha ao atualizar o documento", "it" to "Impossibile aggiornare il documento", "ja" to "ドキュメントの更新に失敗しました", "ko" to "문서를 업데이트하지 못했습니다", "nl" to "Document bijwerken mislukt", "zh" to "更新文档失败"),
|
||||
"err.api.failed_delete_document" to mapOf("en" to "Failed to delete document", "es" to "No se pudo eliminar el documento", "fr" to "Échec de la suppression du document", "de" to "Dokument konnte nicht gelöscht werden", "pt" to "Falha ao excluir o documento", "it" to "Impossibile eliminare il documento", "ja" to "ドキュメントの削除に失敗しました", "ko" to "문서를 삭제하지 못했습니다", "nl" to "Document verwijderen mislukt", "zh" to "删除文档失败"),
|
||||
"err.api.failed_download_document" to mapOf("en" to "Failed to download document", "es" to "No se pudo descargar el documento", "fr" to "Échec du téléchargement du document", "de" to "Dokument konnte nicht heruntergeladen werden", "pt" to "Falha ao baixar o documento", "it" to "Impossibile scaricare il documento", "ja" to "ドキュメントのダウンロードに失敗しました", "ko" to "문서를 다운로드하지 못했습니다", "nl" to "Document downloaden mislukt", "zh" to "下载文档失败"),
|
||||
"err.api.failed_activate_document" to mapOf("en" to "Failed to activate document", "es" to "No se pudo activar el documento", "fr" to "Échec de l'activation du document", "de" to "Dokument konnte nicht aktiviert werden", "pt" to "Falha ao ativar o documento", "it" to "Impossibile attivare il documento", "ja" to "ドキュメントの有効化に失敗しました", "ko" to "문서를 활성화하지 못했습니다", "nl" to "Document activeren mislukt", "zh" to "激活文档失败"),
|
||||
"err.api.failed_deactivate_document" to mapOf("en" to "Failed to deactivate document", "es" to "No se pudo desactivar el documento", "fr" to "Échec de la désactivation du document", "de" to "Dokument konnte nicht deaktiviert werden", "pt" to "Falha ao desativar o documento", "it" to "Impossibile disattivare il documento", "ja" to "ドキュメントの無効化に失敗しました", "ko" to "문서를 비활성화하지 못했습니다", "nl" to "Document deactiveren mislukt", "zh" to "停用文档失败"),
|
||||
"err.api.failed_upload_document_image" to mapOf("en" to "Failed to upload document image", "es" to "No se pudo subir la imagen del documento", "fr" to "Échec de l'envoi de l'image du document", "de" to "Dokumentbild konnte nicht hochgeladen werden", "pt" to "Falha ao enviar a imagem do documento", "it" to "Impossibile caricare l'immagine del documento", "ja" to "ドキュメント画像のアップロードに失敗しました", "ko" to "문서 이미지를 업로드하지 못했습니다", "nl" to "Documentafbeelding uploaden mislukt", "zh" to "上传文档图片失败"),
|
||||
"err.api.failed_delete_document_image" to mapOf("en" to "Failed to delete document image", "es" to "No se pudo eliminar la imagen del documento", "fr" to "Échec de la suppression de l'image du document", "de" to "Dokumentbild konnte nicht gelöscht werden", "pt" to "Falha ao excluir a imagem do documento", "it" to "Impossibile eliminare l'immagine del documento", "ja" to "ドキュメント画像の削除に失敗しました", "ko" to "문서 이미지를 삭제하지 못했습니다", "nl" to "Documentafbeelding verwijderen mislukt", "zh" to "删除文档图片失败"),
|
||||
"err.api.device_registration_failed" to mapOf("en" to "Device registration failed", "es" to "Falló el registro del dispositivo", "fr" to "Échec de l'enregistrement de l'appareil", "de" to "Geräteregistrierung fehlgeschlagen", "pt" to "Falha no registro do dispositivo", "it" to "Registrazione del dispositivo non riuscita", "ja" to "デバイスの登録に失敗しました", "ko" to "기기 등록에 실패했습니다", "nl" to "Apparaatregistratie mislukt", "zh" to "设备注册失败"),
|
||||
"err.api.device_unregistration_failed" to mapOf("en" to "Device unregistration failed", "es" to "Falló la cancelación del registro del dispositivo", "fr" to "Échec de la désinscription de l'appareil", "de" to "Geräteabmeldung fehlgeschlagen", "pt" to "Falha ao cancelar o registro do dispositivo", "it" to "Annullamento registrazione del dispositivo non riuscito", "ja" to "デバイスの登録解除に失敗しました", "ko" to "기기 등록 해제에 실패했습니다", "nl" to "Apparaatregistratie ongedaan maken mislukt", "zh" to "设备注销失败"),
|
||||
"err.api.failed_get_preferences" to mapOf("en" to "Failed to get preferences", "es" to "No se pudieron obtener las preferencias", "fr" to "Échec de la récupération des préférences", "de" to "Einstellungen konnten nicht abgerufen werden", "pt" to "Falha ao obter as preferências", "it" to "Impossibile recuperare le preferenze", "ja" to "設定の取得に失敗しました", "ko" to "환경설정을 가져오지 못했습니다", "nl" to "Voorkeuren ophalen mislukt", "zh" to "获取偏好设置失败"),
|
||||
"err.api.failed_update_preferences" to mapOf("en" to "Failed to update preferences", "es" to "No se pudieron actualizar las preferencias", "fr" to "Échec de la mise à jour des préférences", "de" to "Einstellungen konnten nicht aktualisiert werden", "pt" to "Falha ao atualizar as preferências", "it" to "Impossibile aggiornare le preferenze", "ja" to "設定の更新に失敗しました", "ko" to "환경설정을 업데이트하지 못했습니다", "nl" to "Voorkeuren bijwerken mislukt", "zh" to "更新偏好设置失败"),
|
||||
"err.api.failed_get_notification_history" to mapOf("en" to "Failed to get notification history", "es" to "No se pudo obtener el historial de notificaciones", "fr" to "Échec de la récupération de l'historique des notifications", "de" to "Benachrichtigungsverlauf konnte nicht abgerufen werden", "pt" to "Falha ao obter o histórico de notificações", "it" to "Impossibile recuperare la cronologia delle notifiche", "ja" to "通知履歴の取得に失敗しました", "ko" to "알림 기록을 가져오지 못했습니다", "nl" to "Meldingsgeschiedenis ophalen mislukt", "zh" to "获取通知历史失败"),
|
||||
"err.api.failed_mark_notification_read" to mapOf("en" to "Failed to mark notification as read", "es" to "No se pudo marcar la notificación como leída", "fr" to "Échec du marquage de la notification comme lue", "de" to "Benachrichtigung konnte nicht als gelesen markiert werden", "pt" to "Falha ao marcar a notificação como lida", "it" to "Impossibile contrassegnare la notifica come letta", "ja" to "通知を既読にできませんでした", "ko" to "알림을 읽음으로 표시하지 못했습니다", "nl" to "Melding als gelezen markeren mislukt", "zh" to "标记通知为已读失败"),
|
||||
"err.api.failed_mark_all_notifications_read" to mapOf("en" to "Failed to mark all notifications as read", "es" to "No se pudieron marcar todas las notificaciones como leídas", "fr" to "Échec du marquage de toutes les notifications comme lues", "de" to "Benachrichtigungen konnten nicht alle als gelesen markiert werden", "pt" to "Falha ao marcar todas as notificações como lidas", "it" to "Impossibile contrassegnare tutte le notifiche come lette", "ja" to "すべての通知を既読にできませんでした", "ko" to "모든 알림을 읽음으로 표시하지 못했습니다", "nl" to "Alle meldingen als gelezen markeren mislukt", "zh" to "标记所有通知为已读失败"),
|
||||
"err.api.failed_get_unread_count" to mapOf("en" to "Failed to get unread count", "es" to "No se pudo obtener el número de no leídas", "fr" to "Échec de la récupération du nombre de non lus", "de" to "Anzahl ungelesener Nachrichten konnte nicht abgerufen werden", "pt" to "Falha ao obter a contagem de não lidas", "it" to "Impossibile recuperare il numero di non letti", "ja" to "未読件数の取得に失敗しました", "ko" to "읽지 않은 개수를 가져오지 못했습니다", "nl" to "Aantal ongelezen ophalen mislukt", "zh" to "获取未读数量失败"),
|
||||
"err.api.task_not_found" to mapOf("en" to "Task not found", "es" to "Tarea no encontrada", "fr" to "T—che introuvable", "de" to "Aufgabe nicht gefunden", "pt" to "Tarefa não encontrada", "it" to "Attività non trovata", "ja" to "タスクが見つかりません", "ko" to "작업을 찾을 수 없습니다", "nl" to "Taak niet gevonden", "zh" to "未找到任务"),
|
||||
"err.api.access_denied" to mapOf("en" to "Access denied", "es" to "Acceso denegado", "fr" to "Accès refusé", "de" to "Zugriff verweigert", "pt" to "Acesso negado", "it" to "Accesso negato", "ja" to "アクセスが拒否されました", "ko" to "접근이 거부되었습니다", "nl" to "Toegang geweigerd", "zh" to "访问被拒绝"),
|
||||
"err.api.task_action_failed" to mapOf("en" to "Task {0} failed: {1}", "es" to "La tarea {0} falló: {1}", "fr" to "Échec de la t—che {0} : {1}", "de" to "Aufgabe {0} fehlgeschlagen: {1}", "pt" to "Tarefa {0} falhou: {1}", "it" to "Attività {0} non riuscita: {1}", "ja" to "タスク{0}が失敗しました:{1}", "ko" to "작업 {0} 실패: {1}", "nl" to "Taak {0} mislukt: {1}", "zh" to "任务 {0} 失败:{1}"),
|
||||
"err.api.init_lookups_failed" to mapOf("en" to "Failed to initialize lookups: {0}", "es" to "No se pudieron inicializar las consultas: {0}", "fr" to "Échec de l'initialisation des références : {0}", "de" to "Nachschlagewerte konnten nicht initialisiert werden: {0}", "pt" to "Falha ao inicializar as listas: {0}", "it" to "Impossibile inizializzare le ricerche: {0}", "ja" to "ルックアップの初期化に失敗しました:{0}", "ko" to "조회 항목 초기화 실패: {0}", "nl" to "Opzoekgegevens initialiseren mislukt: {0}", "zh" to "初始化查找数据失败:{0}"),
|
||||
"err.api.load_lookups_failed" to mapOf("en" to "Failed to load lookups: {0}", "es" to "No se pudieron cargar las consultas: {0}", "fr" to "Échec du chargement des références : {0}", "de" to "Nachschlagewerte konnten nicht geladen werden: {0}", "pt" to "Falha ao carregar as listas: {0}", "it" to "Impossibile caricare le ricerche: {0}", "ja" to "ルックアップの読み込みに失敗しました:{0}", "ko" to "조회 항목 로드 실패: {0}", "nl" to "Opzoekgegevens laden mislukt: {0}", "zh" to "加载查找数据失败:{0}"),
|
||||
"err.api.unknown_loading_lookups" to mapOf("en" to "Unknown error loading lookups", "es" to "Error desconocido al cargar las consultas", "fr" to "Erreur inconnue lors du chargement des références", "de" to "Unbekannter Fehler beim Laden der Nachschlagewerte", "pt" to "Erro desconhecido ao carregar as listas", "it" to "Errore sconosciuto durante il caricamento delle ricerche", "ja" to "ルックアップの読み込み中に不明なエラーが発生しました", "ko" to "조회 항목 로드 중 알 수 없는 오류", "nl" to "Onbekende fout bij laden van opzoekgegevens", "zh" to "加载查找数据时发生未知错误"),
|
||||
"err.api.tasks_unavailable" to mapOf("en" to "Tasks unavailable", "es" to "Tareas no disponibles", "fr" to "T—ches indisponibles", "de" to "Aufgaben nicht verfügbar", "pt" to "Tarefas indisponíveis", "it" to "Attività non disponibili", "ja" to "タスクを利用できません", "ko" to "작업을 사용할 수 없습니다", "nl" to "Taken niet beschikbaar", "zh" to "任务不可用"),
|
||||
"err.api.failed_load_task_templates" to mapOf("en" to "Failed to load task templates", "es" to "No se pudieron cargar las plantillas de tareas", "fr" to "Échec du chargement des modèles de t—ches", "de" to "Aufgabenvorlagen konnten nicht geladen werden", "pt" to "Falha ao carregar os modelos de tarefa", "it" to "Impossibile caricare i modelli di attività", "ja" to "タスクテンプレートの読み込みに失敗しました", "ko" to "작업 템플릿을 불러오지 못했습니다", "nl" to "Taaksjablonen laden mislukt", "zh" to "加载任务模板失败"),
|
||||
"err.api.task_template_not_found" to mapOf("en" to "Task template not found", "es" to "Plantilla de tarea no encontrada", "fr" to "Modèle de t—che introuvable", "de" to "Aufgabenvorlage nicht gefunden", "pt" to "Modelo de tarefa não encontrado", "it" to "Modello di attività non trovato", "ja" to "タスクテンプレートが見つかりません", "ko" to "작업 템플릿을 찾을 수 없습니다", "nl" to "Taaksjabloon niet gevonden", "zh" to "未找到任务模板"),
|
||||
"err.api.no_token" to mapOf("en" to "No token", "es" to "Sin token", "fr" to "Aucun jeton", "de" to "Kein Token", "pt" to "Sem token", "it" to "Nessun token", "ja" to "トークンがありません", "ko" to "토큰 없음", "nl" to "Geen token", "zh" to "无令牌"),
|
||||
"err.api.unexpected_state" to mapOf("en" to "Unexpected state", "es" to "Estado inesperado", "fr" to "État inattendu", "de" to "Unerwarteter Zustand", "pt" to "Estado inesperado", "it" to "Stato imprevisto", "ja" to "予期しない状態です", "ko" to "예기치 않은 상태", "nl" to "Onverwachte status", "zh" to "意外状态"),
|
||||
"err.auth.could_not_start_flow" to mapOf("en" to "Could not start {0} (Kratos {1})", "es" to "No se pudo iniciar {0} (Kratos {1})", "fr" to "Impossible de démarrer {0} (Kratos {1})", "de" to "{0} konnte nicht gestartet werden (Kratos {1})", "pt" to "Não foi possível iniciar {0} (Kratos {1})", "it" to "Impossibile avviare {0} (Kratos {1})", "ja" to "{0}を開始できませんでした(Kratos {1})", "ko" to "{0}을(를) 시작할 수 없습니다 (Kratos {1})", "nl" to "Kan {0} niet starten (Kratos {1})", "zh" to "无法启动 {0}(Kratos {1})"),
|
||||
"err.auth.could_not_reach_server" to mapOf("en" to "Could not reach the authentication server", "es" to "No se pudo conectar con el servidor de autenticación", "fr" to "Impossible de joindre le serveur d'authentification", "de" to "Der Authentifizierungsserver ist nicht erreichbar", "pt" to "Não foi possível conectar ao servidor de autenticação", "it" to "Impossibile raggiungere il server di autenticazione", "ja" to "認証サーバーに接続できませんでした", "ko" to "인증 서버에 연결할 수 없습니다", "nl" to "Kan de authenticatieserver niet bereiken", "zh" to "无法连接到身份验证服务器"),
|
||||
"err.auth.request_failed" to mapOf("en" to "Authentication request failed", "es" to "Falló la solicitud de autenticación", "fr" to "Échec de la demande d'authentification", "de" to "Authentifizierungsanfrage fehlgeschlagen", "pt" to "Falha na solicitação de autenticação", "it" to "Richiesta di autenticazione non riuscita", "ja" to "認証リクエストに失敗しました", "ko" to "인증 요청에 실패했습니다", "nl" to "Authenticatieverzoek mislukt", "zh" to "身份验证请求失败"),
|
||||
"err.auth.authentication_failed_status" to mapOf("en" to "Authentication failed ({0})", "es" to "Falló la autenticación ({0})", "fr" to "Échec de l'authentification ({0})", "de" to "Authentifizierung fehlgeschlagen ({0})", "pt" to "Falha na autenticação ({0})", "it" to "Autenticazione non riuscita ({0})", "ja" to "認証に失敗しました({0})", "ko" to "인증 실패 ({0})", "nl" to "Authenticatie mislukt ({0})", "zh" to "身份验证失败({0})"),
|
||||
"err.auth.could_not_start_login" to mapOf("en" to "Could not start login", "es" to "No se pudo iniciar sesión", "fr" to "Impossible de démarrer la connexion", "de" to "Anmeldung konnte nicht gestartet werden", "pt" to "Não foi possível iniciar o login", "it" to "Impossibile avviare l'accesso", "ja" to "ログインを開始できませんでした", "ko" to "로그인을 시작할 수 없습니다", "nl" to "Kan inloggen niet starten", "zh" to "无法开始登录"),
|
||||
"err.auth.login_failed" to mapOf("en" to "Login failed", "es" to "Falló el inicio de sesión", "fr" to "Échec de la connexion", "de" to "Anmeldung fehlgeschlagen", "pt" to "Falha no login", "it" to "Accesso non riuscito", "ja" to "ログインに失敗しました", "ko" to "로그인에 실패했습니다", "nl" to "Inloggen mislukt", "zh" to "登录失败"),
|
||||
"err.auth.registration_failed" to mapOf("en" to "Registration failed", "es" to "Falló el registro", "fr" to "Échec de l'inscription", "de" to "Registrierung fehlgeschlagen", "pt" to "Falha no cadastro", "it" to "Registrazione non riuscita", "ja" to "登録に失敗しました", "ko" to "회원가입에 실패했습니다", "nl" to "Registratie mislukt", "zh" to "注册失败"),
|
||||
"err.auth.could_not_create_account" to mapOf("en" to "Could not create account", "es" to "No se pudo crear la cuenta", "fr" to "Impossible de créer le compte", "de" to "Konto konnte nicht erstellt werden", "pt" to "Não foi possível criar a conta", "it" to "Impossibile creare l'account", "ja" to "アカウントを作成できませんでした", "ko" to "계정을 생성할 수 없습니다", "nl" to "Kan account niet aanmaken", "zh" to "无法创建账户"),
|
||||
"err.auth.could_not_start_signin" to mapOf("en" to "Could not start sign-in", "es" to "No se pudo iniciar la sesión", "fr" to "Impossible de démarrer la connexion", "de" to "Anmeldung konnte nicht gestartet werden", "pt" to "Não foi possível iniciar o login", "it" to "Impossibile avviare l'accesso", "ja" to "サインインを開始できませんでした", "ko" to "로그인을 시작할 수 없습니다", "nl" to "Kan aanmelden niet starten", "zh" to "无法开始登录"),
|
||||
"err.auth.could_not_start_signup" to mapOf("en" to "Could not start sign-up", "es" to "No se pudo iniciar el registro", "fr" to "Impossible de démarrer l'inscription", "de" to "Registrierung konnte nicht gestartet werden", "pt" to "Não foi possível iniciar o cadastro", "it" to "Impossibile avviare la registrazione", "ja" to "サインアップを開始できませんでした", "ko" to "회원가입을 시작할 수 없습니다", "nl" to "Kan registreren niet starten", "zh" to "无法开始注册"),
|
||||
"err.auth.signin_no_session" to mapOf("en" to "Sign-in did not return a session", "es" to "El inicio de sesión no devolvió una sesión", "fr" to "La connexion n'a pas renvoyé de session", "de" to "Bei der Anmeldung wurde keine Sitzung zurückgegeben", "pt" to "O login não retornou uma sessão", "it" to "L'accesso non ha restituito una sessione", "ja" to "サインインでセッションが返されませんでした", "ko" to "로그인 시 세션이 반환되지 않았습니다", "nl" to "Aanmelden heeft geen sessie opgeleverd", "zh" to "登录未返回会话"),
|
||||
"err.auth.signin_failed" to mapOf("en" to "Sign-in failed", "es" to "Falló el inicio de sesión", "fr" to "Échec de la connexion", "de" to "Anmeldung fehlgeschlagen", "pt" to "Falha no login", "it" to "Accesso non riuscito", "ja" to "サインインに失敗しました", "ko" to "로그인에 실패했습니다", "nl" to "Aanmelden mislukt", "zh" to "登录失败"),
|
||||
"err.auth.apple_signin_failed" to mapOf("en" to "Apple Sign In failed", "es" to "Falló Iniciar sesión con Apple", "fr" to "Échec de la connexion avec Apple", "de" to "Apple-Anmeldung fehlgeschlagen", "pt" to "Falha no Login com a Apple", "it" to "Accedi con Apple non riuscito", "ja" to "Appleでのサインインに失敗しました", "ko" to "Apple 로그인에 실패했습니다", "nl" to "Inloggen met Apple mislukt", "zh" to "Apple 登录失败"),
|
||||
"err.auth.google_signin_failed" to mapOf("en" to "Google Sign In failed", "es" to "Falló Iniciar sesión con Google", "fr" to "Échec de la connexion avec Google", "de" to "Google-Anmeldung fehlgeschlagen", "pt" to "Falha no Login com o Google", "it" to "Accedi con Google non riuscito", "ja" to "Googleでのサインインに失敗しました", "ko" to "Google 로그인에 실패했습니다", "nl" to "Inloggen met Google mislukt", "zh" to "Google 登录失败"),
|
||||
"err.auth.could_not_start_recovery" to mapOf("en" to "Could not start password recovery", "es" to "No se pudo iniciar la recuperación de contraseña", "fr" to "Impossible de démarrer la récupération du mot de passe", "de" to "Passwort-Wiederherstellung konnte nicht gestartet werden", "pt" to "Não foi possível iniciar a recuperação de senha", "it" to "Impossibile avviare il recupero della password", "ja" to "パスワードの復旧を開始できませんでした", "ko" to "비밀번호 복구를 시작할 수 없습니다", "nl" to "Kan wachtwoordherstel niet starten", "zh" to "无法开始密码找回"),
|
||||
"err.auth.could_not_send_recovery_code" to mapOf("en" to "Could not send recovery code", "es" to "No se pudo enviar el código de recuperación", "fr" to "Impossible d'envoyer le code de récupération", "de" to "Wiederherstellungscode konnte nicht gesendet werden", "pt" to "Não foi possível enviar o código de recuperação", "it" to "Impossibile inviare il codice di recupero", "ja" to "復旧コードを送信できませんでした", "ko" to "복구 코드를 전송할 수 없습니다", "nl" to "Kan herstelcode niet verzenden", "zh" to "无法发送找回验证码"),
|
||||
"err.auth.recovery_session_expired" to mapOf("en" to "Your recovery session expired. Request a new code.", "es" to "Tu sesión de recuperación expiró. Solicita un nuevo código.", "fr" to "Votre session de récupération a expiré. Demandez un nouveau code.", "de" to "Deine Wiederherstellungssitzung ist abgelaufen. Fordere einen neuen Code an.", "pt" to "Sua sessão de recuperação expirou. Solicite um novo código.", "it" to "La tua sessione di recupero è scaduta. Richiedi un nuovo codice.", "ja" to "復旧セッションの有効期限が切れました。新しいコードをリクエストしてください。", "ko" to "복구 세션이 만료되었습니다. 새 코드를 요청하세요.", "nl" to "Je herstelsessie is verlopen. Vraag een nieuwe code aan.", "zh" to "找回会话已过期,请重新获取验证码。"),
|
||||
"err.auth.invalid_code" to mapOf("en" to "Invalid or expired code", "es" to "Código no válido o expirado", "fr" to "Code non valide ou expiré", "de" to "Ungültiger oder abgelaufener Code", "pt" to "Código inválido ou expirado", "it" to "Codice non valido o scaduto", "ja" to "コードが無効か期限切れです", "ko" to "잘못되었거나 만료된 코드입니다", "nl" to "Ongeldige of verlopen code", "zh" to "验证码无效或已过期"),
|
||||
"err.auth.invalid_code_resend" to mapOf("en" to "Invalid or expired code. Tap resend for a new one.", "es" to "Código no válido o expirado. Toca reenviar para obtener uno nuevo.", "fr" to "Code non valide ou expiré. Touchez Renvoyer pour en obtenir un nouveau.", "de" to "Ungültiger oder abgelaufener Code. Tippe auf Erneut senden für einen neuen.", "pt" to "Código inválido ou expirado. Toque em reenviar para receber um novo.", "it" to "Codice non valido o scaduto. Tocca Invia di nuovo per ottenerne uno nuovo.", "ja" to "コードが無効か期限切れです。再送信をタップして新しいコードを取得してください。", "ko" to "잘못되었거나 만료된 코드입니다. 재전송을 눌러 새 코드를 받으세요.", "nl" to "Ongeldige of verlopen code. Tik op opnieuw verzenden voor een nieuwe.", "zh" to "验证码无效或已过期。点击重新发送以获取新验证码。"),
|
||||
"err.auth.reset_session_expired" to mapOf("en" to "This password reset session has expired. Request a new code.", "es" to "Esta sesión de restablecimiento de contraseña expiró. Solicita un nuevo código.", "fr" to "Cette session de réinitialisation du mot de passe a expiré. Demandez un nouveau code.", "de" to "Diese Passwort-Zurücksetzungssitzung ist abgelaufen. Fordere einen neuen Code an.", "pt" to "Esta sessão de redefinição de senha expirou. Solicite um novo código.", "it" to "Questa sessione di reimpostazione password è scaduta. Richiedi un nuovo codice.", "ja" to "このパスワードリセットセッションは期限切れです。新しいコードをリクエストしてください。", "ko" to "비밀번호 재설정 세션이 만료되었습니다. 새 코드를 요청하세요.", "nl" to "Deze sessie voor wachtwoordherstel is verlopen. Vraag een nieuwe code aan.", "zh" to "此密码重置会话已过期,请重新获取验证码。"),
|
||||
"err.auth.could_not_reset_password" to mapOf("en" to "Could not reset password", "es" to "No se pudo restablecer la contraseña", "fr" to "Impossible de réinitialiser le mot de passe", "de" to "Passwort konnte nicht zurückgesetzt werden", "pt" to "Não foi possível redefinir a senha", "it" to "Impossibile reimpostare la password", "ja" to "パスワードをリセットできませんでした", "ko" to "비밀번호를 재설정할 수 없습니다", "nl" to "Kan wachtwoord niet opnieuw instellen", "zh" to "无法重置密码"),
|
||||
"err.auth.could_not_start_verification" to mapOf("en" to "Could not start verification", "es" to "No se pudo iniciar la verificación", "fr" to "Impossible de démarrer la vérification", "de" to "Verifizierung konnte nicht gestartet werden", "pt" to "Não foi possível iniciar a verificação", "it" to "Impossibile avviare la verifica", "ja" to "認証を開始できませんでした", "ko" to "인증을 시작할 수 없습니다", "nl" to "Kan verificatie niet starten", "zh" to "无法开始验证"),
|
||||
"err.auth.could_not_send_verification_code" to mapOf("en" to "Could not send verification code", "es" to "No se pudo enviar el código de verificación", "fr" to "Impossible d'envoyer le code de vérification", "de" to "Verifizierungscode konnte nicht gesendet werden", "pt" to "Não foi possível enviar o código de verificação", "it" to "Impossibile inviare il codice di verifica", "ja" to "認証コードを送信できませんでした", "ko" to "인증 코드를 전송할 수 없습니다", "nl" to "Kan verificatiecode niet verzenden", "zh" to "无法发送验证码"),
|
||||
"err.auth.request_verification_first" to mapOf("en" to "Please request a verification code first.", "es" to "Solicita primero un código de verificación.", "fr" to "Veuillez d'abord demander un code de vérification.", "de" to "Bitte fordere zuerst einen Verifizierungscode an.", "pt" to "Solicite primeiro um código de verificação.", "it" to "Richiedi prima un codice di verifica.", "ja" to "先に認証コードをリクエストしてください。", "ko" to "먼저 인증 코드를 요청하세요.", "nl" to "Vraag eerst een verificatiecode aan.", "zh" to "请先获取验证码。"),
|
||||
"err.auth.verification_failed" to mapOf("en" to "Verification failed", "es" to "Falló la verificación", "fr" to "Échec de la vérification", "de" to "Verifizierung fehlgeschlagen", "pt" to "Falha na verificação", "it" to "Verifica non riuscita", "ja" to "認証に失敗しました", "ko" to "인증에 실패했습니다", "nl" to "Verificatie mislukt", "zh" to "验证失败"),
|
||||
"err.auth.failed_get_user" to mapOf("en" to "Failed to get user", "es" to "No se pudo obtener el usuario", "fr" to "Échec de la récupération de l'utilisateur", "de" to "Benutzer konnte nicht abgerufen werden", "pt" to "Falha ao obter o usuário", "it" to "Impossibile recuperare l'utente", "ja" to "ユーザー情報の取得に失敗しました", "ko" to "사용자 정보를 가져오지 못했습니다", "nl" to "Gebruiker ophalen mislukt", "zh" to "获取用户失败"),
|
||||
"err.auth.session_expired" to mapOf("en" to "Session expired — please sign in again", "es" to "La sesión expiró — inicia sesión de nuevo", "fr" to "Session expirée — veuillez vous reconnecter", "de" to "Sitzung abgelaufen — bitte melde dich erneut an", "pt" to "Sessão expirada — faça login novamente", "it" to "Sessione scaduta — accedi di nuovo", "ja" to "セッションの有効期限が切れました。もう一度サインインしてください", "ko" to "세션이 만료되었습니다 — 다시 로그인하세요", "nl" to "Sessie verlopen — meld je opnieuw aan", "zh" to "会话已过期 — 请重新登录"),
|
||||
"err.auth.could_not_validate_session" to mapOf("en" to "Could not validate session", "es" to "No se pudo validar la sesión", "fr" to "Impossible de valider la session", "de" to "Sitzung konnte nicht validiert werden", "pt" to "Não foi possível validar a sessão", "it" to "Impossibile convalidare la sessione", "ja" to "セッションを検証できませんでした", "ko" to "세션을 확인할 수 없습니다", "nl" to "Kan sessie niet valideren", "zh" to "无法验证会话"),
|
||||
"err.auth.could_not_load_profile" to mapOf("en" to "Could not load profile after sign-in", "es" to "No se pudo cargar el perfil después de iniciar sesión", "fr" to "Impossible de charger le profil après la connexion", "de" to "Profil konnte nach der Anmeldung nicht geladen werden", "pt" to "Não foi possível carregar o perfil após o login", "it" to "Impossibile caricare il profilo dopo l'accesso", "ja" to "サインイン後にプロフィールを読み込めませんでした", "ko" to "로그인 후 프로필을 불러올 수 없습니다", "nl" to "Kan profiel niet laden na aanmelden", "zh" to "登录后无法加载个人资料"),
|
||||
"err.vm.failed_create_residence" to mapOf("en" to "Failed to create residence", "es" to "No se pudo crear la residencia", "fr" to "Échec de la création de la résidence", "de" to "Objekt konnte nicht erstellt werden", "pt" to "Falha ao criar o imóvel", "it" to "Impossibile creare la residenza", "ja" to "住居の作成に失敗しました", "ko" to "주거지를 생성하지 못했습니다", "nl" to "Woning aanmaken mislukt", "zh" to "创建住宅失败"),
|
||||
"err.vm.failed_join_residence" to mapOf("en" to "Failed to join residence", "es" to "No se pudo unir a la residencia", "fr" to "Échec pour rejoindre la résidence", "de" to "Objekt konnte nicht beigetreten werden", "pt" to "Falha ao entrar no imóvel", "it" to "Impossibile unirsi alla residenza", "ja" to "住居への参加に失敗しました", "ko" to "주거지에 참여하지 못했습니다", "nl" to "Deelnemen aan woning mislukt", "zh" to "加入住宅失败"),
|
||||
"err.vm.upload_unexpected_state" to mapOf("en" to "Upload failed in unexpected state", "es" to "La subida falló en un estado inesperado", "fr" to "Échec de l'envoi dans un état inattendu", "de" to "Upload in unerwartetem Zustand fehlgeschlagen", "pt" to "Falha no envio em estado inesperado", "it" to "Caricamento non riuscito in uno stato imprevisto", "ja" to "予期しない状態でアップロードに失敗しました", "ko" to "예기치 않은 상태로 업로드에 실패했습니다", "nl" to "Upload mislukt in onverwachte status", "zh" to "上传因意外状态失败"),
|
||||
"err.vm.document_updated_image_upload_failed" to mapOf("en" to "Document updated but failed to upload image: {0}", "es" to "Se actualizó el documento pero no se pudo subir la imagen: {0}", "fr" to "Document mis à jour, mais échec de l'envoi de l'image : {0}", "de" to "Dokument aktualisiert, aber Bild-Upload fehlgeschlagen: {0}", "pt" to "Documento atualizado, mas falha ao enviar a imagem: {0}", "it" to "Documento aggiornato ma caricamento dell'immagine non riuscito: {0}", "ja" to "ドキュメントは更新されましたが、画像のアップロードに失敗しました:{0}", "ko" to "문서는 업데이트되었지만 이미지 업로드에 실패했습니다: {0}", "nl" to "Document bijgewerkt, maar afbeelding uploaden mislukt: {0}", "zh" to "文档已更新,但图片上传失败:{0}"),
|
||||
"err.vm.invalid_reset_token" to mapOf("en" to "Invalid reset token. Please start over.", "es" to "Token de restablecimiento no válido. Vuelve a empezar.", "fr" to "Jeton de réinitialisation non valide. Veuillez recommencer.", "de" to "Ungültiges Zurücksetzungs-Token. Bitte beginne von vorn.", "pt" to "Token de redefinição inválido. Comece novamente.", "it" to "Token di reimpostazione non valido. Ricomincia da capo.", "ja" to "リセットトークンが無効です。最初からやり直してください。", "ko" to "잘못된 재설정 토큰입니다. 처음부터 다시 시작하세요.", "nl" to "Ongeldige reset-token. Begin opnieuw.", "zh" to "重置令牌无效,请重新开始。"),
|
||||
"doc.type.warranty" to mapOf("en" to "Warranty", "es" to "Garantía", "fr" to "Garantie", "de" to "Garantie", "pt" to "Garantia", "it" to "Garanzia", "ja" to "保証書", "ko" to "보증서", "nl" to "Garantie", "zh" to "保修单"),
|
||||
"doc.type.manual" to mapOf("en" to "User Manual", "es" to "Manual de usuario", "fr" to "Manuel d'utilisation", "de" to "Bedienungsanleitung", "pt" to "Manual do usuário", "it" to "Manuale utente", "ja" to "取扱説明書", "ko" to "사용 설명서", "nl" to "Handleiding", "zh" to "用户手册"),
|
||||
"doc.type.receipt" to mapOf("en" to "Receipt/Invoice", "es" to "Recibo/Factura", "fr" to "Reçu/Facture", "de" to "Beleg/Rechnung", "pt" to "Recibo/Nota fiscal", "it" to "Ricevuta/Fattura", "ja" to "領収書/請求書", "ko" to "영수증/청구서", "nl" to "Bon/factuur", "zh" to "收据/发票"),
|
||||
"doc.type.inspection" to mapOf("en" to "Inspection Report", "es" to "Informe de inspección", "fr" to "Rapport d'inspection", "de" to "Prüfbericht", "pt" to "Laudo de vistoria", "it" to "Rapporto di ispezione", "ja" to "点検報告書", "ko" to "점검 보고서", "nl" to "Inspectierapport", "zh" to "检查报告"),
|
||||
"doc.type.permit" to mapOf("en" to "Permit", "es" to "Permiso", "fr" to "Permis", "de" to "Genehmigung", "pt" to "Alvará", "it" to "Permesso", "ja" to "許可証", "ko" to "허가증", "nl" to "Vergunning", "zh" to "许可证"),
|
||||
"doc.type.deed" to mapOf("en" to "Deed/Title", "es" to "Escritura/Título", "fr" to "Acte/Titre", "de" to "Urkunde/Eigentumstitel", "pt" to "Escritura/Título", "it" to "Atto/Titolo", "ja" to "権利証/登記", "ko" to "등기/권리증", "nl" to "Akte/eigendomsbewijs", "zh" to "房契/产权证"),
|
||||
"doc.type.insurance" to mapOf("en" to "Insurance", "es" to "Seguro", "fr" to "Assurance", "de" to "Versicherung", "pt" to "Seguro", "it" to "Assicurazione", "ja" to "保険", "ko" to "보험", "nl" to "Verzekering", "zh" to "保险"),
|
||||
"doc.type.contract" to mapOf("en" to "Contract", "es" to "Contrato", "fr" to "Contrat", "de" to "Vertrag", "pt" to "Contrato", "it" to "Contratto", "ja" to "契約書", "ko" to "계약서", "nl" to "Contract", "zh" to "合同"),
|
||||
"doc.type.photo" to mapOf("en" to "Photo", "es" to "Foto", "fr" to "Photo", "de" to "Foto", "pt" to "Foto", "it" to "Foto", "ja" to "写真", "ko" to "사진", "nl" to "Foto", "zh" to "照片"),
|
||||
"doc.type.other" to mapOf("en" to "Other", "es" to "Otro", "fr" to "Autre", "de" to "Sonstiges", "pt" to "Outro", "it" to "Altro", "ja" to "その他", "ko" to "기타", "nl" to "Overig", "zh" to "其他"),
|
||||
"doc.category.appliance" to mapOf("en" to "Appliance", "es" to "Electrodoméstico", "fr" to "Appareil électroménager", "de" to "Gerät", "pt" to "Eletrodoméstico", "it" to "Elettrodomestico", "ja" to "家電", "ko" to "가전제품", "nl" to "Apparaat", "zh" to "家电"),
|
||||
"doc.category.hvac" to mapOf("en" to "HVAC", "es" to "Climatización", "fr" to "CVC", "de" to "Heizung/Klima", "pt" to "Climatização", "it" to "HVAC", "ja" to "空調", "ko" to "냉난방", "nl" to "HVAC", "zh" to "暖通空调"),
|
||||
"doc.category.plumbing" to mapOf("en" to "Plumbing", "es" to "Fontanería", "fr" to "Plomberie", "de" to "Sanitär", "pt" to "Hidráulica", "it" to "Idraulica", "ja" to "配管", "ko" to "배관", "nl" to "Loodgieterswerk", "zh" to "管道"),
|
||||
"doc.category.electrical" to mapOf("en" to "Electrical", "es" to "Electricidad", "fr" to "Électricité", "de" to "Elektrik", "pt" to "Elétrica", "it" to "Impianto elettrico", "ja" to "電気", "ko" to "전기", "nl" to "Elektra", "zh" to "电气"),
|
||||
"doc.category.roofing" to mapOf("en" to "Roofing", "es" to "Techado", "fr" to "Toiture", "de" to "Dach", "pt" to "Telhado", "it" to "Copertura", "ja" to "屋根", "ko" to "지붕", "nl" to "Dakbedekking", "zh" to "屋顶"),
|
||||
"doc.category.structural" to mapOf("en" to "Structural", "es" to "Estructural", "fr" to "Structure", "de" to "Bausubstanz", "pt" to "Estrutural", "it" to "Struttura", "ja" to "構造", "ko" to "구조", "nl" to "Constructie", "zh" to "结构"),
|
||||
"doc.category.landscaping" to mapOf("en" to "Landscaping", "es" to "Jardinería", "fr" to "Aménagement paysager", "de" to "Garten", "pt" to "Paisagismo", "it" to "Giardinaggio", "ja" to "造園", "ko" to "조경", "nl" to "Tuinaanleg", "zh" to "园艺绿化"),
|
||||
"doc.category.general" to mapOf("en" to "General", "es" to "General", "fr" to "Général", "de" to "Allgemein", "pt" to "Geral", "it" to "Generale", "ja" to "一般", "ko" to "일반", "nl" to "Algemeen", "zh" to "通用"),
|
||||
"doc.category.other" to mapOf("en" to "Other", "es" to "Otro", "fr" to "Autre", "de" to "Sonstiges", "pt" to "Outro", "it" to "Altro", "ja" to "その他", "ko" to "기타", "nl" to "Overig", "zh" to "其他"),
|
||||
"validation.title_required" to mapOf("en" to "Title is required", "es" to "El título es obligatorio", "fr" to "Le titre est obligatoire", "de" to "Titel ist erforderlich", "pt" to "O título é obrigatório", "it" to "Il titolo è obbligatorio", "ja" to "タイトルは必須です", "ko" to "제목은 필수입니다", "nl" to "Titel is verplicht", "zh" to "标题为必填项"),
|
||||
"validation.priority_required" to mapOf("en" to "Please select a priority", "es" to "Selecciona una prioridad", "fr" to "Veuillez sélectionner une priorité", "de" to "Bitte wähle eine Priorität aus", "pt" to "Selecione uma prioridade", "it" to "Seleziona una priorità", "ja" to "優先度を選択してください", "ko" to "우선순위를 선택하세요", "nl" to "Selecteer een prioriteit", "zh" to "请选择优先级"),
|
||||
"validation.category_required" to mapOf("en" to "Please select a category", "es" to "Selecciona una categoría", "fr" to "Veuillez sélectionner une catégorie", "de" to "Bitte wähle eine Kategorie aus", "pt" to "Selecione uma categoria", "it" to "Seleziona una categoria", "ja" to "カテゴリを選択してください", "ko" to "카테고리를 선택하세요", "nl" to "Selecteer een categorie", "zh" to "请选择类别"),
|
||||
"validation.frequency_required" to mapOf("en" to "Please select a frequency", "es" to "Selecciona una frecuencia", "fr" to "Veuillez sélectionner une fréquence", "de" to "Bitte wähle eine Häufigkeit aus", "pt" to "Selecione uma frequência", "it" to "Seleziona una frequenza", "ja" to "頻度を選択してください", "ko" to "빈도를 선택하세요", "nl" to "Selecteer een frequentie", "zh" to "请选择频率"),
|
||||
"validation.est_cost_invalid" to mapOf("en" to "Estimated cost must be a valid number", "es" to "El costo estimado debe ser un número válido", "fr" to "Le coût estimé doit être un nombre valide", "de" to "Die geschätzten Kosten müssen eine gültige Zahl sein", "pt" to "O custo estimado deve ser um número válido", "it" to "Il costo stimato deve essere un numero valido", "ja" to "見積もり費用は有効な数値である必要があります", "ko" to "예상 비용은 유효한 숫자여야 합니다", "nl" to "Geschatte kosten moeten een geldig getal zijn", "zh" to "预估费用必须是有效的数字"),
|
||||
"validation.property_required" to mapOf("en" to "Property is required", "es" to "La propiedad es obligatoria", "fr" to "La propriété est obligatoire", "de" to "Immobilie ist erforderlich", "pt" to "A propriedade é obrigatória", "it" to "La proprietà è obbligatoria", "ja" to "物件は必須です", "ko" to "건물은 필수입니다", "nl" to "Woning is verplicht", "zh" to "房产为必填项"),
|
||||
"validation.name_required" to mapOf("en" to "Name is required", "es" to "El nombre es obligatorio", "fr" to "Le nom est obligatoire", "de" to "Name ist erforderlich", "pt" to "O nome é obrigatório", "it" to "Il nome è obbligatorio", "ja" to "名前は必須です", "ko" to "이름은 필수입니다", "nl" to "Naam is verplicht", "zh" to "名称为必填项"),
|
||||
"validation.name_too_long" to mapOf("en" to "Name must be {0} characters or fewer", "es" to "El nombre debe tener {0} caracteres o menos", "fr" to "Le nom doit comporter {0} caractères ou moins", "de" to "Der Name darf höchstens {0} Zeichen lang sein", "pt" to "O nome deve ter {0} caracteres ou menos", "it" to "Il nome deve contenere {0} caratteri o meno", "ja" to "名前は{0}文字以内で入力してください", "ko" to "이름은 {0}자 이하여야 합니다", "nl" to "De naam mag maximaal {0} tekens bevatten", "zh" to "名称不得超过{0}个字符"),
|
||||
"validation.bedrooms_invalid" to mapOf("en" to "Bedrooms must be a non-negative whole number", "es" to "Los dormitorios deben ser un número entero no negativo", "fr" to "Le nombre de chambres doit être un entier non négatif", "de" to "Schlafzimmer müssen eine nicht negative ganze Zahl sein", "pt" to "Os quartos devem ser um número inteiro não negativo", "it" to "Le camere da letto devono essere un numero intero non negativo", "ja" to "寝室数は0以上の整数である必要があります", "ko" to "침실 수는 0 이상의 정수여야 합니다", "nl" to "Slaapkamers moeten een niet-negatief geheel getal zijn", "zh" to "卧室数必须是非负整数"),
|
||||
"validation.bathrooms_invalid" to mapOf("en" to "Bathrooms must be a non-negative number", "es" to "Los baños deben ser un número no negativo", "fr" to "Le nombre de salles de bain doit être un nombre non négatif", "de" to "Badezimmer müssen eine nicht negative Zahl sein", "pt" to "Os banheiros devem ser um número não negativo", "it" to "I bagni devono essere un numero non negativo", "ja" to "浴室数は0以上の数値である必要があります", "ko" to "욕실 수는 0 이상의 숫자여야 합니다", "nl" to "Badkamers moeten een niet-negatief getal zijn", "zh" to "浴室数必须是非负数"),
|
||||
"validation.sqft_invalid" to mapOf("en" to "Square footage must be a positive whole number", "es" to "Los pies cuadrados deben ser un número entero positivo", "fr" to "La surface doit être un entier positif", "de" to "Die Quadratmeterzahl muss eine positive ganze Zahl sein", "pt" to "A metragem quadrada deve ser um número inteiro positivo", "it" to "La metratura deve essere un numero intero positivo", "ja" to "面積は正の整数である必要があります", "ko" to "면적은 양의 정수여야 합니다", "nl" to "De oppervlakte moet een positief geheel getal zijn", "zh" to "面积必须是正整数"),
|
||||
"validation.lot_size_invalid" to mapOf("en" to "Lot size must be a positive number", "es" to "El tamaño del terreno debe ser un número positivo", "fr" to "La taille du terrain doit être un nombre positif", "de" to "Die Grundstücksgröße muss eine positive Zahl sein", "pt" to "O tamanho do lote deve ser um número positivo", "it" to "La dimensione del lotto deve essere un numero positivo", "ja" to "敷地面積は正の数値である必要があります", "ko" to "대지 면적은 양수여야 합니다", "nl" to "De perceelgrootte moet een positief getal zijn", "zh" to "占地面积必须是正数"),
|
||||
"validation.year_built_format" to mapOf("en" to "Year built must be a 4-digit year", "es" to "El año de construcción debe tener 4 dígitos", "fr" to "L'année de construction doit comporter 4 chiffres", "de" to "Das Baujahr muss eine 4-stellige Jahreszahl sein", "pt" to "O ano de construção deve ter 4 dígitos", "it" to "L'anno di costruzione deve essere di 4 cifre", "ja" to "築年は4桁の西暦で入力してください", "ko" to "건축 연도는 4자리 연도여야 합니다", "nl" to "Het bouwjaar moet een jaartal van 4 cijfers zijn", "zh" to "建造年份必须为4位数年份"),
|
||||
"validation.year_built_range" to mapOf("en" to "Year built must be between {0} and the current year", "es" to "El año de construcción debe estar entre {0} y el año actual", "fr" to "L'année de construction doit être comprise entre {0} et l'année actuelle", "de" to "Das Baujahr muss zwischen {0} und dem aktuellen Jahr liegen", "pt" to "O ano de construção deve estar entre {0} e o ano atual", "it" to "L'anno di costruzione deve essere compreso tra {0} e l'anno corrente", "ja" to "築年は{0}年から現在の年までの間で入力してください", "ko" to "건축 연도는 {0}년부터 현재 연도 사이여야 합니다", "nl" to "Het bouwjaar moet tussen {0} en het huidige jaar liggen", "zh" to "建造年份必须介于{0}和当前年份之间"),
|
||||
"validation.share_code_length" to mapOf("en" to "Share code must be 6 characters", "es" to "El código para compartir debe tener 6 caracteres", "fr" to "Le code de partage doit comporter 6 caractères", "de" to "Der Freigabecode muss 6 Zeichen lang sein", "pt" to "O código de compartilhamento deve ter 6 caracteres", "it" to "Il codice di condivisione deve avere 6 caratteri", "ja" to "共有コードは6文字である必要があります", "ko" to "공유 코드는 6자여야 합니다", "nl" to "De deelcode moet 6 tekens lang zijn", "zh" to "分享码必须为6个字符"),
|
||||
"auth.recovery_sent" to mapOf("en" to "If that email exists, a recovery code has been sent.", "es" to "Si ese correo existe, se ha enviado un código de recuperación.", "fr" to "Si cet e-mail existe, un code de récupération a été envoyé.", "de" to "Falls diese E-Mail existiert, wurde ein Wiederherstellungscode gesendet.", "pt" to "Se esse e-mail existir, um código de recuperação foi enviado.", "it" to "Se questa email esiste, è stato inviato un codice di recupero.", "ja" to "そのメールアドレスが登録されている場合、復旧コードを送信しました。", "ko" to "해당 이메일이 존재하는 경우 복구 코드가 전송되었습니다.", "nl" to "Als dat e-mailadres bestaat, is er een herstelcode verzonden.", "zh" to "如果该邮箱存在,恢复代码已发送。"),
|
||||
"auth.code_verified" to mapOf("en" to "Code verified.", "es" to "Código verificado.", "fr" to "Code vérifié.", "de" to "Code verifiziert.", "pt" to "Código verificado.", "it" to "Codice verificato.", "ja" to "コードを確認しました。", "ko" to "코드가 확인되었습니다.", "nl" to "Code geverifieerd.", "zh" to "代码已验证。"),
|
||||
"auth.password_updated" to mapOf("en" to "Password updated. You can now sign in.", "es" to "Contraseña actualizada. Ya puedes iniciar sesión.", "fr" to "Mot de passe mis à jour. Vous pouvez maintenant vous connecter.", "de" to "Passwort aktualisiert. Du kannst dich jetzt anmelden.", "pt" to "Senha atualizada. Agora você pode entrar.", "it" to "Password aggiornata. Ora puoi accedere.", "ja" to "パスワードを更新しました。サインインできます。", "ko" to "비밀번호가 업데이트되었습니다. 이제 로그인할 수 있습니다.", "nl" to "Wachtwoord bijgewerkt. Je kunt nu inloggen.", "zh" to "密码已更新。您现在可以登录。"),
|
||||
"auth.email_verified" to mapOf("en" to "Email verified.", "es" to "Correo verificado.", "fr" to "E-mail vérifié.", "de" to "E-Mail verifiziert.", "pt" to "E-mail verificado.", "it" to "Email verificata.", "ja" to "メールアドレスを確認しました。", "ko" to "이메일이 확인되었습니다.", "nl" to "E-mail geverifieerd.", "zh" to "邮箱已验证。"),
|
||||
// Home profile picker fallback option labels (apiValue stays stable; these localize display)
|
||||
"home_profile.heating.gas_furnace" to mapOf("en" to "Gas Furnace", "es" to "Caldera de gas", "fr" to "Chaudière à gaz", "de" to "Gasheizung", "pt" to "Aquecedor a gás", "it" to "Caldaia a gas", "ja" to "ガス暖房", "ko" to "가스 난방", "nl" to "Gasverwarming", "zh" to "燃气炉"),
|
||||
"home_profile.heating.electric" to mapOf("en" to "Electric", "es" to "Eléctrica", "fr" to "Électrique", "de" to "Elektrisch", "pt" to "Elétrico", "it" to "Elettrico", "ja" to "電気", "ko" to "전기", "nl" to "Elektrisch", "zh" to "电力"),
|
||||
"home_profile.heating.heat_pump" to mapOf("en" to "Heat Pump", "es" to "Bomba de calor", "fr" to "Pompe à chaleur", "de" to "Wärmepumpe", "pt" to "Bomba de calor", "it" to "Pompa di calore", "ja" to "ヒートポンプ", "ko" to "히트 펌프", "nl" to "Warmtepomp", "zh" to "热泵"),
|
||||
"home_profile.heating.boiler" to mapOf("en" to "Boiler", "es" to "Caldera", "fr" to "Chaudière", "de" to "Heizkessel", "pt" to "Caldeira", "it" to "Caldaia", "ja" to "ボイラー", "ko" to "보일러", "nl" to "Ketel", "zh" to "锅炉"),
|
||||
"home_profile.heating.radiant" to mapOf("en" to "Radiant", "es" to "Radiante", "fr" to "Rayonnant", "de" to "Strahlungsheizung", "pt" to "Radiante", "it" to "Radiante", "ja" to "輻射熱", "ko" to "복사 난방", "nl" to "Stralingsverwarming", "zh" to "辐射供暖"),
|
||||
"home_profile.heating.wood_stove" to mapOf("en" to "Wood Stove", "es" to "Estufa de leña", "fr" to "Poêle à bois", "de" to "Holzofen", "pt" to "Fogão a lenha", "it" to "Stufa a legna", "ja" to "薪ストーブ", "ko" to "장작 난로", "nl" to "Houtkachel", "zh" to "柴火炉"),
|
||||
"home_profile.heating.none" to mapOf("en" to "None", "es" to "Ninguna", "fr" to "Aucun", "de" to "Keine", "pt" to "Nenhum", "it" to "Nessuno", "ja" to "なし", "ko" to "없음", "nl" to "Geen", "zh" to "无"),
|
||||
"home_profile.cooling.central_ac" to mapOf("en" to "Central AC", "es" to "Aire central", "fr" to "Clim centrale", "de" to "Zentrale Klimaanlage", "pt" to "Ar central", "it" to "Climatizzazione centrale", "ja" to "セントラル空調", "ko" to "중앙 냉방", "nl" to "Centrale airco", "zh" to "中央空调"),
|
||||
"home_profile.cooling.window_unit" to mapOf("en" to "Window Unit", "es" to "Equipo de ventana", "fr" to "Climatiseur de fenêtre", "de" to "Fenstergerät", "pt" to "Aparelho de janela", "it" to "Condizionatore da finestra", "ja" to "窓用エアコン", "ko" to "창문형 에어컨", "nl" to "Raamunit", "zh" to "窗式空调"),
|
||||
"home_profile.cooling.mini_split" to mapOf("en" to "Mini Split", "es" to "Mini split", "fr" to "Mini-split", "de" to "Mini-Split", "pt" to "Mini split", "it" to "Mini split", "ja" to "ミニスプリット", "ko" to "미니 스플릿", "nl" to "Mini-split", "zh" to "分体式空调"),
|
||||
"home_profile.cooling.evaporative" to mapOf("en" to "Evaporative", "es" to "Evaporativo", "fr" to "Évaporatif", "de" to "Verdunstung", "pt" to "Evaporativo", "it" to "Evaporativo", "ja" to "気化式", "ko" to "증발식", "nl" to "Verdampingskoeling", "zh" to "蒸发式"),
|
||||
"home_profile.cooling.none" to mapOf("en" to "None", "es" to "Ninguno", "fr" to "Aucun", "de" to "Keine", "pt" to "Nenhum", "it" to "Nessuno", "ja" to "なし", "ko" to "없음", "nl" to "Geen", "zh" to "无"),
|
||||
"home_profile.water_heater.tank_gas" to mapOf("en" to "Tank (Gas)", "es" to "Tanque (gas)", "fr" to "Réservoir (gaz)", "de" to "Speicher (Gas)", "pt" to "Boiler (gás)", "it" to "Serbatoio (gas)", "ja" to "タンク(ガス)", "ko" to "탱크 (가스)", "nl" to "Boiler (gas)", "zh" to "储水式(燃气)"),
|
||||
"home_profile.water_heater.tank_electric" to mapOf("en" to "Tank (Electric)", "es" to "Tanque (eléctrico)", "fr" to "Réservoir (électrique)", "de" to "Speicher (elektrisch)", "pt" to "Boiler (elétrico)", "it" to "Serbatoio (elettrico)", "ja" to "タンク(電気)", "ko" to "탱크 (전기)", "nl" to "Boiler (elektrisch)", "zh" to "储水式(电)"),
|
||||
"home_profile.water_heater.tankless" to mapOf("en" to "Tankless", "es" to "Sin tanque", "fr" to "Sans réservoir", "de" to "Durchlauferhitzer", "pt" to "Sem tanque", "it" to "Istantaneo", "ja" to "タンクレス", "ko" to "순간식", "nl" to "Doorstroom", "zh" to "即热式"),
|
||||
"home_profile.water_heater.solar" to mapOf("en" to "Solar", "es" to "Solar", "fr" to "Solaire", "de" to "Solar", "pt" to "Solar", "it" to "Solare", "ja" to "ソーラー", "ko" to "태양열", "nl" to "Zonne-energie", "zh" to "太阳能"),
|
||||
"home_profile.water_heater.heat_pump" to mapOf("en" to "Heat Pump", "es" to "Bomba de calor", "fr" to "Pompe à chaleur", "de" to "Wärmepumpe", "pt" to "Bomba de calor", "it" to "Pompa di calore", "ja" to "ヒートポンプ", "ko" to "히트 펌프", "nl" to "Warmtepomp", "zh" to "热泵"),
|
||||
"home_profile.roof.asphalt_shingle" to mapOf("en" to "Asphalt Shingle", "es" to "Teja asfáltica", "fr" to "Bardeau d'asphalte", "de" to "Asphaltschindel", "pt" to "Telha asfáltica", "it" to "Tegola bituminosa", "ja" to "アスファルトシングル", "ko" to "아스팔트 슁글", "nl" to "Asfaltshingle", "zh" to "沥青瓦"),
|
||||
"home_profile.roof.metal" to mapOf("en" to "Metal", "es" to "Metal", "fr" to "Métal", "de" to "Metall", "pt" to "Metal", "it" to "Metallo", "ja" to "金属", "ko" to "금속", "nl" to "Metaal", "zh" to "金属"),
|
||||
"home_profile.roof.tile" to mapOf("en" to "Tile", "es" to "Teja", "fr" to "Tuile", "de" to "Ziegel", "pt" to "Telha", "it" to "Tegola", "ja" to "瓦", "ko" to "기와", "nl" to "Dakpan", "zh" to "瓦片"),
|
||||
"home_profile.roof.flat_tpo" to mapOf("en" to "Flat/TPO", "es" to "Plano/TPO", "fr" to "Plat/TPO", "de" to "Flach/TPO", "pt" to "Plano/TPO", "it" to "Piano/TPO", "ja" to "陸屋根/TPO", "ko" to "평지붕/TPO", "nl" to "Plat/TPO", "zh" to "平顶/TPO"),
|
||||
"home_profile.roof.slate" to mapOf("en" to "Slate", "es" to "Pizarra", "fr" to "Ardoise", "de" to "Schiefer", "pt" to "Ardósia", "it" to "Ardesia", "ja" to "スレート", "ko" to "슬레이트", "nl" to "Leisteen", "zh" to "石板"),
|
||||
"home_profile.roof.wood_shake" to mapOf("en" to "Wood Shake", "es" to "Tejamanil de madera", "fr" to "Bardeau de bois", "de" to "Holzschindel", "pt" to "Telha de madeira", "it" to "Scandola di legno", "ja" to "木製シェイク", "ko" to "목재 셰이크", "nl" to "Houten shingle", "zh" to "木瓦"),
|
||||
"home_profile.exterior.vinyl_siding" to mapOf("en" to "Vinyl Siding", "es" to "Revestimiento de vinilo", "fr" to "Revêtement en vinyle", "de" to "Vinylverkleidung", "pt" to "Revestimento de vinil", "it" to "Rivestimento in vinile", "ja" to "ビニールサイディング", "ko" to "비닐 사이딩", "nl" to "Vinyl gevelbekleding", "zh" to "乙烯基壁板"),
|
||||
"home_profile.exterior.brick" to mapOf("en" to "Brick", "es" to "Ladrillo", "fr" to "Brique", "de" to "Ziegelstein", "pt" to "Tijolo", "it" to "Mattone", "ja" to "レンガ", "ko" to "벽돌", "nl" to "Baksteen", "zh" to "砖"),
|
||||
"home_profile.exterior.stucco" to mapOf("en" to "Stucco", "es" to "Estuco", "fr" to "Stuc", "de" to "Putz", "pt" to "Estuque", "it" to "Stucco", "ja" to "スタッコ", "ko" to "스투코", "nl" to "Stucwerk", "zh" to "灰泥"),
|
||||
"home_profile.exterior.wood" to mapOf("en" to "Wood", "es" to "Madera", "fr" to "Bois", "de" to "Holz", "pt" to "Madeira", "it" to "Legno", "ja" to "木材", "ko" to "목재", "nl" to "Hout", "zh" to "木材"),
|
||||
"home_profile.exterior.stone" to mapOf("en" to "Stone", "es" to "Piedra", "fr" to "Pierre", "de" to "Stein", "pt" to "Pedra", "it" to "Pietra", "ja" to "石", "ko" to "석재", "nl" to "Steen", "zh" to "石材"),
|
||||
"home_profile.exterior.fiber_cement" to mapOf("en" to "Fiber Cement", "es" to "Fibrocemento", "fr" to "Fibrociment", "de" to "Faserzement", "pt" to "Fibrocimento", "it" to "Fibrocemento", "ja" to "繊維セメント", "ko" to "섬유 시멘트", "nl" to "Vezelcement", "zh" to "纤维水泥"),
|
||||
"home_profile.flooring.hardwood" to mapOf("en" to "Hardwood", "es" to "Madera maciza", "fr" to "Bois massif", "de" to "Hartholz", "pt" to "Madeira maciça", "it" to "Legno massello", "ja" to "無垢材", "ko" to "원목", "nl" to "Hardhout", "zh" to "实木"),
|
||||
"home_profile.flooring.carpet" to mapOf("en" to "Carpet", "es" to "Alfombra", "fr" to "Moquette", "de" to "Teppich", "pt" to "Carpete", "it" to "Moquette", "ja" to "カーペット", "ko" to "카펫", "nl" to "Tapijt", "zh" to "地毯"),
|
||||
"home_profile.flooring.tile" to mapOf("en" to "Tile", "es" to "Baldosa", "fr" to "Carrelage", "de" to "Fliesen", "pt" to "Cerâmica", "it" to "Piastrelle", "ja" to "タイル", "ko" to "타일", "nl" to "Tegel", "zh" to "瓷砖"),
|
||||
"home_profile.flooring.laminate" to mapOf("en" to "Laminate", "es" to "Laminado", "fr" to "Stratifié", "de" to "Laminat", "pt" to "Laminado", "it" to "Laminato", "ja" to "ラミネート", "ko" to "라미네이트", "nl" to "Laminaat", "zh" to "复合地板"),
|
||||
"home_profile.flooring.vinyl" to mapOf("en" to "Vinyl", "es" to "Vinilo", "fr" to "Vinyle", "de" to "Vinyl", "pt" to "Vinil", "it" to "Vinile", "ja" to "ビニール", "ko" to "비닐", "nl" to "Vinyl", "zh" to "乙烯基"),
|
||||
"home_profile.landscaping.lawn" to mapOf("en" to "Lawn", "es" to "Césped", "fr" to "Pelouse", "de" to "Rasen", "pt" to "Gramado", "it" to "Prato", "ja" to "芝生", "ko" to "잔디", "nl" to "Gazon", "zh" to "草坪"),
|
||||
"home_profile.landscaping.xeriscaping" to mapOf("en" to "Xeriscaping", "es" to "Xerojardinería", "fr" to "Xéropaysagisme", "de" to "Xeriscaping", "pt" to "Xerojardinagem", "it" to "Xeriscaping", "ja" to "ゼリスケープ", "ko" to "건조 조경", "nl" to "Xeriscaping", "zh" to "旱生园艺"),
|
||||
"home_profile.landscaping.none" to mapOf("en" to "None", "es" to "Ninguno", "fr" to "Aucun", "de" to "Keine", "pt" to "Nenhum", "it" to "Nessuno", "ja" to "なし", "ko" to "없음", "nl" to "Geen", "zh" to "无"),
|
||||
)
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.tt.honeyDue.models
|
||||
|
||||
import com.tt.honeyDue.i18n.ClientStrings
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@@ -120,17 +121,19 @@ data class DocumentUpdateRequest(
|
||||
// API now returns List<Document> directly
|
||||
|
||||
// Document type choices
|
||||
enum class DocumentType(val value: String, val displayName: String) {
|
||||
WARRANTY("warranty", "Warranty"),
|
||||
MANUAL("manual", "User Manual"),
|
||||
RECEIPT("receipt", "Receipt/Invoice"),
|
||||
INSPECTION("inspection", "Inspection Report"),
|
||||
PERMIT("permit", "Permit"),
|
||||
DEED("deed", "Deed/Title"),
|
||||
INSURANCE("insurance", "Insurance"),
|
||||
CONTRACT("contract", "Contract"),
|
||||
PHOTO("photo", "Photo"),
|
||||
OTHER("other", "Other");
|
||||
enum class DocumentType(val value: String, private val displayNameKey: String) {
|
||||
WARRANTY("warranty", "doc.type.warranty"),
|
||||
MANUAL("manual", "doc.type.manual"),
|
||||
RECEIPT("receipt", "doc.type.receipt"),
|
||||
INSPECTION("inspection", "doc.type.inspection"),
|
||||
PERMIT("permit", "doc.type.permit"),
|
||||
DEED("deed", "doc.type.deed"),
|
||||
INSURANCE("insurance", "doc.type.insurance"),
|
||||
CONTRACT("contract", "doc.type.contract"),
|
||||
PHOTO("photo", "doc.type.photo"),
|
||||
OTHER("other", "doc.type.other");
|
||||
|
||||
val displayName: String get() = ClientStrings.t(displayNameKey)
|
||||
|
||||
companion object {
|
||||
fun fromValue(value: String): DocumentType {
|
||||
@@ -140,16 +143,18 @@ enum class DocumentType(val value: String, val displayName: String) {
|
||||
}
|
||||
|
||||
// Document/Warranty category choices
|
||||
enum class DocumentCategory(val value: String, val displayName: String) {
|
||||
APPLIANCE("appliance", "Appliance"),
|
||||
HVAC("hvac", "HVAC"),
|
||||
PLUMBING("plumbing", "Plumbing"),
|
||||
ELECTRICAL("electrical", "Electrical"),
|
||||
ROOFING("roofing", "Roofing"),
|
||||
STRUCTURAL("structural", "Structural"),
|
||||
LANDSCAPING("landscaping", "Landscaping"),
|
||||
GENERAL("general", "General"),
|
||||
OTHER("other", "Other");
|
||||
enum class DocumentCategory(val value: String, private val displayNameKey: String) {
|
||||
APPLIANCE("appliance", "doc.category.appliance"),
|
||||
HVAC("hvac", "doc.category.hvac"),
|
||||
PLUMBING("plumbing", "doc.category.plumbing"),
|
||||
ELECTRICAL("electrical", "doc.category.electrical"),
|
||||
ROOFING("roofing", "doc.category.roofing"),
|
||||
STRUCTURAL("structural", "doc.category.structural"),
|
||||
LANDSCAPING("landscaping", "doc.category.landscaping"),
|
||||
GENERAL("general", "doc.category.general"),
|
||||
OTHER("other", "doc.category.other");
|
||||
|
||||
val displayName: String get() = ClientStrings.t(displayNameKey)
|
||||
|
||||
companion object {
|
||||
fun fromValue(value: String): DocumentCategory {
|
||||
|
||||
@@ -1,59 +1,62 @@
|
||||
package com.tt.honeyDue.models
|
||||
|
||||
/**
|
||||
* Static option lists for home profile pickers.
|
||||
* Each entry is a (apiValue, displayLabel) pair.
|
||||
* Static option lists for home profile pickers (live fallback shown when the
|
||||
* backend options aren't loaded). Each entry is a (apiValue, displayLabelKey)
|
||||
* pair: the FIRST element is the stable backend API value and must never
|
||||
* change; the SECOND element is a [com.tt.honeyDue.i18n.ClientStrings] key
|
||||
* resolved to a localized label at the render site.
|
||||
*/
|
||||
object HomeProfileOptions {
|
||||
val heatingTypes = listOf(
|
||||
"gas_furnace" to "Gas Furnace",
|
||||
"electric" to "Electric",
|
||||
"heat_pump" to "Heat Pump",
|
||||
"boiler" to "Boiler",
|
||||
"radiant" to "Radiant",
|
||||
"wood_stove" to "Wood Stove",
|
||||
"none" to "None"
|
||||
"gas_furnace" to "home_profile.heating.gas_furnace",
|
||||
"electric" to "home_profile.heating.electric",
|
||||
"heat_pump" to "home_profile.heating.heat_pump",
|
||||
"boiler" to "home_profile.heating.boiler",
|
||||
"radiant" to "home_profile.heating.radiant",
|
||||
"wood_stove" to "home_profile.heating.wood_stove",
|
||||
"none" to "home_profile.heating.none"
|
||||
)
|
||||
val coolingTypes = listOf(
|
||||
"central_ac" to "Central AC",
|
||||
"window_unit" to "Window Unit",
|
||||
"mini_split" to "Mini Split",
|
||||
"evaporative" to "Evaporative",
|
||||
"none" to "None"
|
||||
"central_ac" to "home_profile.cooling.central_ac",
|
||||
"window_unit" to "home_profile.cooling.window_unit",
|
||||
"mini_split" to "home_profile.cooling.mini_split",
|
||||
"evaporative" to "home_profile.cooling.evaporative",
|
||||
"none" to "home_profile.cooling.none"
|
||||
)
|
||||
val waterHeaterTypes = listOf(
|
||||
"tank_gas" to "Tank (Gas)",
|
||||
"tank_electric" to "Tank (Electric)",
|
||||
"tankless" to "Tankless",
|
||||
"solar" to "Solar",
|
||||
"heat_pump_wh" to "Heat Pump"
|
||||
"tank_gas" to "home_profile.water_heater.tank_gas",
|
||||
"tank_electric" to "home_profile.water_heater.tank_electric",
|
||||
"tankless" to "home_profile.water_heater.tankless",
|
||||
"solar" to "home_profile.water_heater.solar",
|
||||
"heat_pump_wh" to "home_profile.water_heater.heat_pump"
|
||||
)
|
||||
val roofTypes = listOf(
|
||||
"asphalt_shingle" to "Asphalt Shingle",
|
||||
"metal" to "Metal",
|
||||
"tile" to "Tile",
|
||||
"flat_tpo" to "Flat/TPO",
|
||||
"slate" to "Slate",
|
||||
"wood_shake" to "Wood Shake"
|
||||
"asphalt_shingle" to "home_profile.roof.asphalt_shingle",
|
||||
"metal" to "home_profile.roof.metal",
|
||||
"tile" to "home_profile.roof.tile",
|
||||
"flat_tpo" to "home_profile.roof.flat_tpo",
|
||||
"slate" to "home_profile.roof.slate",
|
||||
"wood_shake" to "home_profile.roof.wood_shake"
|
||||
)
|
||||
val exteriorTypes = listOf(
|
||||
"vinyl_siding" to "Vinyl Siding",
|
||||
"brick" to "Brick",
|
||||
"stucco" to "Stucco",
|
||||
"wood" to "Wood",
|
||||
"stone" to "Stone",
|
||||
"fiber_cement" to "Fiber Cement"
|
||||
"vinyl_siding" to "home_profile.exterior.vinyl_siding",
|
||||
"brick" to "home_profile.exterior.brick",
|
||||
"stucco" to "home_profile.exterior.stucco",
|
||||
"wood" to "home_profile.exterior.wood",
|
||||
"stone" to "home_profile.exterior.stone",
|
||||
"fiber_cement" to "home_profile.exterior.fiber_cement"
|
||||
)
|
||||
val flooringTypes = listOf(
|
||||
"hardwood" to "Hardwood",
|
||||
"carpet" to "Carpet",
|
||||
"tile" to "Tile",
|
||||
"laminate" to "Laminate",
|
||||
"vinyl" to "Vinyl"
|
||||
"hardwood" to "home_profile.flooring.hardwood",
|
||||
"carpet" to "home_profile.flooring.carpet",
|
||||
"tile" to "home_profile.flooring.tile",
|
||||
"laminate" to "home_profile.flooring.laminate",
|
||||
"vinyl" to "home_profile.flooring.vinyl"
|
||||
)
|
||||
val landscapingTypes = listOf(
|
||||
"lawn" to "Lawn",
|
||||
"xeriscaping" to "Xeriscaping",
|
||||
"none" to "None"
|
||||
"lawn" to "home_profile.landscaping.lawn",
|
||||
"xeriscaping" to "home_profile.landscaping.xeriscaping",
|
||||
"none" to "home_profile.landscaping.none"
|
||||
)
|
||||
}
|
||||
|
||||
@@ -10,8 +10,13 @@ import kotlinx.serialization.Serializable
|
||||
@Serializable
|
||||
data class ResidenceType(
|
||||
val id: Int,
|
||||
val name: String
|
||||
)
|
||||
val name: String,
|
||||
@SerialName("display_name") val displayNameLocalized: String = ""
|
||||
) {
|
||||
/** Localized label for the current locale; falls back to [name]. */
|
||||
val displayName: String
|
||||
get() = displayNameLocalized.ifBlank { name }
|
||||
}
|
||||
|
||||
/**
|
||||
* Task frequency lookup - matching Go API TaskFrequencyResponse
|
||||
@@ -20,12 +25,13 @@ data class ResidenceType(
|
||||
data class TaskFrequency(
|
||||
val id: Int,
|
||||
val name: String,
|
||||
@SerialName("display_name") val displayNameLocalized: String = "",
|
||||
val days: Int? = null,
|
||||
@SerialName("display_order") val displayOrder: Int = 0
|
||||
) {
|
||||
// Helper for display
|
||||
/** Localized label for the current locale; falls back to [name]. */
|
||||
val displayName: String
|
||||
get() = name
|
||||
get() = displayNameLocalized.ifBlank { name }
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -35,13 +41,14 @@ data class TaskFrequency(
|
||||
data class TaskPriority(
|
||||
val id: Int,
|
||||
val name: String,
|
||||
@SerialName("display_name") val displayNameLocalized: String = "",
|
||||
val level: Int = 0,
|
||||
val color: String = "",
|
||||
@SerialName("display_order") val displayOrder: Int = 0
|
||||
) {
|
||||
// Helper for display
|
||||
/** Localized label for the current locale; falls back to [name]. */
|
||||
val displayName: String
|
||||
get() = name
|
||||
get() = displayNameLocalized.ifBlank { name }
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -51,11 +58,16 @@ data class TaskPriority(
|
||||
data class TaskCategory(
|
||||
val id: Int,
|
||||
val name: String,
|
||||
@SerialName("display_name") val displayNameLocalized: String = "",
|
||||
val description: String = "",
|
||||
val icon: String = "",
|
||||
val color: String = "",
|
||||
@SerialName("display_order") val displayOrder: Int = 0
|
||||
)
|
||||
) {
|
||||
/** Localized label for the current locale; falls back to [name]. */
|
||||
val displayName: String
|
||||
get() = displayNameLocalized.ifBlank { name }
|
||||
}
|
||||
|
||||
/**
|
||||
* Contractor specialty lookup
|
||||
@@ -64,9 +76,25 @@ data class TaskCategory(
|
||||
data class ContractorSpecialty(
|
||||
val id: Int,
|
||||
val name: String,
|
||||
@SerialName("display_name") val displayNameLocalized: String = "",
|
||||
val description: String? = null,
|
||||
val icon: String? = null,
|
||||
@SerialName("display_order") val displayOrder: Int = 0
|
||||
) {
|
||||
/** Localized label for the current locale; falls back to [name]. */
|
||||
val displayName: String
|
||||
get() = displayNameLocalized.ifBlank { name }
|
||||
}
|
||||
|
||||
/**
|
||||
* A selectable home-profile field option (heating type, roof type, etc.),
|
||||
* served localized by the backend. [value] is the stable code stored on the
|
||||
* residence; [displayName] is the localized label.
|
||||
*/
|
||||
@Serializable
|
||||
data class HomeProfileOption(
|
||||
val value: String,
|
||||
@SerialName("display_name") val displayName: String
|
||||
)
|
||||
|
||||
/**
|
||||
@@ -103,7 +131,10 @@ data class SeededDataResponse(
|
||||
@SerialName("task_priorities") val taskPriorities: List<TaskPriority>,
|
||||
@SerialName("task_frequencies") val taskFrequencies: List<TaskFrequency>,
|
||||
@SerialName("contractor_specialties") val contractorSpecialties: List<ContractorSpecialty>,
|
||||
@SerialName("task_templates") val taskTemplates: TaskTemplatesGroupedResponse
|
||||
@SerialName("task_templates") val taskTemplates: TaskTemplatesGroupedResponse,
|
||||
@SerialName("home_profile_options") val homeProfileOptions: Map<String, List<HomeProfileOption>> = emptyMap(),
|
||||
@SerialName("document_types") val documentTypes: List<HomeProfileOption> = emptyList(),
|
||||
@SerialName("document_categories") val documentCategories: List<HomeProfileOption> = emptyList()
|
||||
)
|
||||
|
||||
// Legacy wrapper responses for backward compatibility
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.tt.honeyDue.models
|
||||
|
||||
import com.tt.honeyDue.i18n.ClientStrings
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@@ -26,13 +27,13 @@ data class TaskTemplate(
|
||||
* Human-readable frequency display
|
||||
*/
|
||||
val frequencyDisplay: String
|
||||
get() = frequency?.displayName ?: "One time"
|
||||
get() = frequency?.displayName ?: ClientStrings.t("task.frequency.one_time")
|
||||
|
||||
/**
|
||||
* Category name for display
|
||||
*/
|
||||
val categoryName: String
|
||||
get() = category?.name ?: "Uncategorized"
|
||||
get() = category?.name ?: ClientStrings.t("task.category.uncategorized")
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package com.tt.honeyDue.network
|
||||
|
||||
import com.tt.honeyDue.i18n.ClientStrings
|
||||
|
||||
import com.tt.honeyDue.data.DataManager
|
||||
import com.tt.honeyDue.models.*
|
||||
import com.tt.honeyDue.network.*
|
||||
@@ -54,7 +56,7 @@ object APILayer {
|
||||
* Call this when app comes to foreground or when limits might have changed.
|
||||
*/
|
||||
suspend fun refreshSubscriptionStatus(): ApiResult<SubscriptionStatus> {
|
||||
val token = getToken() ?: return ApiResult.Error("Not authenticated", 401)
|
||||
val token = getToken() ?: return ApiResult.Error(ClientStrings.t("err.not_authenticated"), 401)
|
||||
|
||||
println("🔄 [APILayer] Force refreshing subscription status from backend...")
|
||||
val result = subscriptionApi.getSubscriptionStatus(token)
|
||||
@@ -184,7 +186,7 @@ object APILayer {
|
||||
DataManager.markLookupsInitialized()
|
||||
return ApiResult.Success(Unit)
|
||||
} catch (e: Exception) {
|
||||
return ApiResult.Error("Failed to initialize lookups: ${e.message}")
|
||||
return ApiResult.Error(ClientStrings.t("err.api.init_lookups_failed", e.message ?: ""))
|
||||
} finally {
|
||||
lookupsInitMutex.unlock()
|
||||
}
|
||||
@@ -251,10 +253,10 @@ object APILayer {
|
||||
DataManager.markLookupsInitialized()
|
||||
return ApiResult.Success(Unit)
|
||||
} else if (staticDataResult is ApiResult.Error) {
|
||||
return ApiResult.Error("Failed to load lookups: ${staticDataResult.message}")
|
||||
return ApiResult.Error(ClientStrings.t("err.api.load_lookups_failed", staticDataResult.message))
|
||||
}
|
||||
|
||||
return ApiResult.Error("Unknown error loading lookups")
|
||||
return ApiResult.Error(ClientStrings.t("err.api.unknown_loading_lookups"))
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -268,7 +270,7 @@ object APILayer {
|
||||
}
|
||||
}
|
||||
|
||||
val token = getToken() ?: return ApiResult.Error("Not authenticated", 401)
|
||||
val token = getToken() ?: return ApiResult.Error(ClientStrings.t("err.not_authenticated"), 401)
|
||||
val result = lookupsApi.getResidenceTypes(token)
|
||||
|
||||
if (result is ApiResult.Success) {
|
||||
@@ -289,7 +291,7 @@ object APILayer {
|
||||
}
|
||||
}
|
||||
|
||||
val token = getToken() ?: return ApiResult.Error("Not authenticated", 401)
|
||||
val token = getToken() ?: return ApiResult.Error(ClientStrings.t("err.not_authenticated"), 401)
|
||||
val result = lookupsApi.getTaskFrequencies(token)
|
||||
|
||||
if (result is ApiResult.Success) {
|
||||
@@ -310,7 +312,7 @@ object APILayer {
|
||||
}
|
||||
}
|
||||
|
||||
val token = getToken() ?: return ApiResult.Error("Not authenticated", 401)
|
||||
val token = getToken() ?: return ApiResult.Error(ClientStrings.t("err.not_authenticated"), 401)
|
||||
val result = lookupsApi.getTaskPriorities(token)
|
||||
|
||||
if (result is ApiResult.Success) {
|
||||
@@ -331,7 +333,7 @@ object APILayer {
|
||||
}
|
||||
}
|
||||
|
||||
val token = getToken() ?: return ApiResult.Error("Not authenticated", 401)
|
||||
val token = getToken() ?: return ApiResult.Error(ClientStrings.t("err.not_authenticated"), 401)
|
||||
val result = lookupsApi.getTaskCategories(token)
|
||||
|
||||
if (result is ApiResult.Success) {
|
||||
@@ -352,7 +354,7 @@ object APILayer {
|
||||
}
|
||||
}
|
||||
|
||||
val token = getToken() ?: return ApiResult.Error("Not authenticated", 401)
|
||||
val token = getToken() ?: return ApiResult.Error(ClientStrings.t("err.not_authenticated"), 401)
|
||||
val result = lookupsApi.getContractorSpecialties(token)
|
||||
|
||||
if (result is ApiResult.Success) {
|
||||
@@ -374,7 +376,7 @@ object APILayer {
|
||||
println("[APILayer] CACHE MISS: residences (forceRefresh=$forceRefresh)")
|
||||
|
||||
// Fetch from API
|
||||
val token = getToken() ?: return ApiResult.Error("Not authenticated", 401)
|
||||
val token = getToken() ?: return ApiResult.Error(ClientStrings.t("err.not_authenticated"), 401)
|
||||
val result = residenceApi.getResidences(token)
|
||||
|
||||
// Update DataManager on success
|
||||
@@ -397,7 +399,7 @@ object APILayer {
|
||||
println("[APILayer] CACHE MISS: myResidences (forceRefresh=$forceRefresh)")
|
||||
|
||||
// Fetch from API
|
||||
val token = getToken() ?: return ApiResult.Error("Not authenticated", 401)
|
||||
val token = getToken() ?: return ApiResult.Error(ClientStrings.t("err.not_authenticated"), 401)
|
||||
val result = residenceApi.getMyResidences(token)
|
||||
|
||||
// Update DataManager on success
|
||||
@@ -422,7 +424,7 @@ object APILayer {
|
||||
* cache without requiring a manual pull-to-refresh.
|
||||
*/
|
||||
suspend fun acceptResidenceInvite(residenceId: Int): ApiResult<Unit> {
|
||||
val token = getToken() ?: return ApiResult.Error("Not authenticated", 401)
|
||||
val token = getToken() ?: return ApiResult.Error(ClientStrings.t("err.not_authenticated"), 401)
|
||||
val result = residenceApi.acceptResidenceInvite(token, residenceId)
|
||||
if (result is ApiResult.Success) {
|
||||
// Residence list may have changed — force a refresh so any
|
||||
@@ -438,7 +440,7 @@ object APILayer {
|
||||
* client-side; the next app-open will re-query the server anyway.
|
||||
*/
|
||||
suspend fun declineResidenceInvite(residenceId: Int): ApiResult<Unit> {
|
||||
val token = getToken() ?: return ApiResult.Error("Not authenticated", 401)
|
||||
val token = getToken() ?: return ApiResult.Error(ClientStrings.t("err.not_authenticated"), 401)
|
||||
return residenceApi.declineResidenceInvite(token, residenceId)
|
||||
}
|
||||
|
||||
@@ -462,7 +464,7 @@ object APILayer {
|
||||
}
|
||||
|
||||
// Fetch from API
|
||||
val token = getToken() ?: return ApiResult.Error("Not authenticated", 401)
|
||||
val token = getToken() ?: return ApiResult.Error(ClientStrings.t("err.not_authenticated"), 401)
|
||||
val result = residenceApi.getResidence(token, id)
|
||||
|
||||
// Update DataManager on success
|
||||
@@ -486,7 +488,7 @@ object APILayer {
|
||||
}
|
||||
}
|
||||
|
||||
val token = getToken() ?: return ApiResult.Error("Not authenticated", 401)
|
||||
val token = getToken() ?: return ApiResult.Error(ClientStrings.t("err.not_authenticated"), 401)
|
||||
val result = residenceApi.getSummary(token)
|
||||
|
||||
if (result is ApiResult.Success) {
|
||||
@@ -497,7 +499,7 @@ object APILayer {
|
||||
}
|
||||
|
||||
suspend fun createResidence(request: ResidenceCreateRequest): ApiResult<ResidenceResponse> {
|
||||
val token = getToken() ?: return ApiResult.Error("Not authenticated", 401)
|
||||
val token = getToken() ?: return ApiResult.Error(ClientStrings.t("err.not_authenticated"), 401)
|
||||
val result = residenceApi.createResidence(token, request)
|
||||
|
||||
// Extract summary and update local cache
|
||||
@@ -509,12 +511,12 @@ object APILayer {
|
||||
|
||||
return when (result) {
|
||||
is ApiResult.Error -> result
|
||||
else -> ApiResult.Error("Unknown error")
|
||||
else -> ApiResult.Error(ClientStrings.t("err.unknown"))
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun updateResidence(id: Int, request: ResidenceCreateRequest): ApiResult<ResidenceResponse> {
|
||||
val token = getToken() ?: return ApiResult.Error("Not authenticated", 401)
|
||||
val token = getToken() ?: return ApiResult.Error(ClientStrings.t("err.not_authenticated"), 401)
|
||||
val result = residenceApi.updateResidence(token, id, request)
|
||||
|
||||
// Extract summary and update local cache
|
||||
@@ -526,12 +528,12 @@ object APILayer {
|
||||
|
||||
return when (result) {
|
||||
is ApiResult.Error -> result
|
||||
else -> ApiResult.Error("Unknown error")
|
||||
else -> ApiResult.Error(ClientStrings.t("err.unknown"))
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun deleteResidence(id: Int): ApiResult<Unit> {
|
||||
val token = getToken() ?: return ApiResult.Error("Not authenticated", 401)
|
||||
val token = getToken() ?: return ApiResult.Error(ClientStrings.t("err.not_authenticated"), 401)
|
||||
val result = residenceApi.deleteResidence(token, id)
|
||||
|
||||
// Extract summary and update local cache
|
||||
@@ -543,17 +545,17 @@ object APILayer {
|
||||
|
||||
return when (result) {
|
||||
is ApiResult.Error -> result
|
||||
else -> ApiResult.Error("Unknown error")
|
||||
else -> ApiResult.Error(ClientStrings.t("err.unknown"))
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun generateTasksReport(residenceId: Int, email: String? = null): ApiResult<GenerateReportResponse> {
|
||||
val token = getToken() ?: return ApiResult.Error("Not authenticated", 401)
|
||||
val token = getToken() ?: return ApiResult.Error(ClientStrings.t("err.not_authenticated"), 401)
|
||||
return residenceApi.generateTasksReport(token, residenceId, email)
|
||||
}
|
||||
|
||||
suspend fun joinWithCode(code: String): ApiResult<JoinResidenceResponse> {
|
||||
val token = getToken() ?: return ApiResult.Error("Not authenticated", 401)
|
||||
val token = getToken() ?: return ApiResult.Error(ClientStrings.t("err.not_authenticated"), 401)
|
||||
val result = residenceApi.joinWithCode(token, code)
|
||||
|
||||
// Extract summary and update local cache
|
||||
@@ -566,27 +568,27 @@ object APILayer {
|
||||
}
|
||||
|
||||
suspend fun getResidenceUsers(residenceId: Int): ApiResult<ResidenceUsersResponse> {
|
||||
val token = getToken() ?: return ApiResult.Error("Not authenticated", 401)
|
||||
val token = getToken() ?: return ApiResult.Error(ClientStrings.t("err.not_authenticated"), 401)
|
||||
return residenceApi.getResidenceUsers(token, residenceId)
|
||||
}
|
||||
|
||||
suspend fun getShareCode(residenceId: Int): ApiResult<ShareCodeResponse> {
|
||||
val token = getToken() ?: return ApiResult.Error("Not authenticated", 401)
|
||||
val token = getToken() ?: return ApiResult.Error(ClientStrings.t("err.not_authenticated"), 401)
|
||||
return residenceApi.getShareCode(token, residenceId)
|
||||
}
|
||||
|
||||
suspend fun generateShareCode(residenceId: Int): ApiResult<GenerateShareCodeResponse> {
|
||||
val token = getToken() ?: return ApiResult.Error("Not authenticated", 401)
|
||||
val token = getToken() ?: return ApiResult.Error(ClientStrings.t("err.not_authenticated"), 401)
|
||||
return residenceApi.generateShareCode(token, residenceId)
|
||||
}
|
||||
|
||||
suspend fun generateSharePackage(residenceId: Int): ApiResult<SharedResidence> {
|
||||
val token = getToken() ?: return ApiResult.Error("Not authenticated", 401)
|
||||
val token = getToken() ?: return ApiResult.Error(ClientStrings.t("err.not_authenticated"), 401)
|
||||
return residenceApi.generateSharePackage(token, residenceId)
|
||||
}
|
||||
|
||||
suspend fun removeUser(residenceId: Int, userId: Int): ApiResult<RemoveUserResponse> {
|
||||
val token = getToken() ?: return ApiResult.Error("Not authenticated", 401)
|
||||
val token = getToken() ?: return ApiResult.Error(ClientStrings.t("err.not_authenticated"), 401)
|
||||
return residenceApi.removeUser(token, residenceId, userId)
|
||||
}
|
||||
|
||||
@@ -604,7 +606,7 @@ object APILayer {
|
||||
println("[APILayer] CACHE MISS: tasks (forceRefresh=$forceRefresh)")
|
||||
|
||||
// Fetch from API
|
||||
val token = getToken() ?: return ApiResult.Error("Not authenticated", 401)
|
||||
val token = getToken() ?: return ApiResult.Error(ClientStrings.t("err.not_authenticated"), 401)
|
||||
val result = taskApi.getTasks(token)
|
||||
|
||||
// Update DataManager on success
|
||||
@@ -629,12 +631,12 @@ object APILayer {
|
||||
if (allTasksResult is ApiResult.Error) return allTasksResult
|
||||
|
||||
val filtered = DataManager.getTasksForResidence(residenceId)
|
||||
?: return ApiResult.Error("Tasks unavailable", 0)
|
||||
?: return ApiResult.Error(ClientStrings.t("err.api.tasks_unavailable"), 0)
|
||||
return ApiResult.Success(filtered)
|
||||
}
|
||||
|
||||
suspend fun createTask(request: TaskCreateRequest): ApiResult<TaskResponse> {
|
||||
val token = getToken() ?: return ApiResult.Error("Not authenticated", 401)
|
||||
val token = getToken() ?: return ApiResult.Error(ClientStrings.t("err.not_authenticated"), 401)
|
||||
val result = taskApi.createTask(token, request)
|
||||
|
||||
// Extract summary and update local cache with new task
|
||||
@@ -647,7 +649,7 @@ object APILayer {
|
||||
|
||||
return when (result) {
|
||||
is ApiResult.Error -> result
|
||||
else -> ApiResult.Error("Unknown error")
|
||||
else -> ApiResult.Error(ClientStrings.t("err.unknown"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -664,7 +666,7 @@ object APILayer {
|
||||
* after onboarding), silently dropping the new tasks from cache.
|
||||
*/
|
||||
suspend fun bulkCreateTasks(request: BulkCreateTasksRequest): ApiResult<BulkCreateTasksResponse> {
|
||||
val token = getToken() ?: return ApiResult.Error("Not authenticated", 401)
|
||||
val token = getToken() ?: return ApiResult.Error(ClientStrings.t("err.not_authenticated"), 401)
|
||||
val result = taskApi.bulkCreateTasks(token, request)
|
||||
|
||||
if (result is ApiResult.Success) {
|
||||
@@ -677,7 +679,7 @@ object APILayer {
|
||||
}
|
||||
|
||||
suspend fun updateTask(id: Int, request: TaskCreateRequest): ApiResult<TaskResponse> {
|
||||
val token = getToken() ?: return ApiResult.Error("Not authenticated", 401)
|
||||
val token = getToken() ?: return ApiResult.Error(ClientStrings.t("err.not_authenticated"), 401)
|
||||
val result = taskApi.updateTask(token, id, request)
|
||||
|
||||
// Extract summary and update local cache with modified task
|
||||
@@ -690,12 +692,12 @@ object APILayer {
|
||||
|
||||
return when (result) {
|
||||
is ApiResult.Error -> result
|
||||
else -> ApiResult.Error("Unknown error")
|
||||
else -> ApiResult.Error(ClientStrings.t("err.unknown"))
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun cancelTask(taskId: Int): ApiResult<TaskResponse> {
|
||||
val token = getToken() ?: return ApiResult.Error("Not authenticated", 401)
|
||||
val token = getToken() ?: return ApiResult.Error(ClientStrings.t("err.not_authenticated"), 401)
|
||||
val result = taskApi.cancelTask(token, taskId)
|
||||
|
||||
if (result is ApiResult.Success) {
|
||||
@@ -706,12 +708,12 @@ object APILayer {
|
||||
|
||||
return when (result) {
|
||||
is ApiResult.Error -> result
|
||||
else -> ApiResult.Error("Unknown error")
|
||||
else -> ApiResult.Error(ClientStrings.t("err.unknown"))
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun uncancelTask(taskId: Int): ApiResult<TaskResponse> {
|
||||
val token = getToken() ?: return ApiResult.Error("Not authenticated", 401)
|
||||
val token = getToken() ?: return ApiResult.Error(ClientStrings.t("err.not_authenticated"), 401)
|
||||
val result = taskApi.uncancelTask(token, taskId)
|
||||
|
||||
if (result is ApiResult.Success) {
|
||||
@@ -722,12 +724,12 @@ object APILayer {
|
||||
|
||||
return when (result) {
|
||||
is ApiResult.Error -> result
|
||||
else -> ApiResult.Error("Unknown error")
|
||||
else -> ApiResult.Error(ClientStrings.t("err.unknown"))
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun markInProgress(taskId: Int): ApiResult<TaskResponse> {
|
||||
val token = getToken() ?: return ApiResult.Error("Not authenticated", 401)
|
||||
val token = getToken() ?: return ApiResult.Error(ClientStrings.t("err.not_authenticated"), 401)
|
||||
val result = taskApi.markInProgress(token, taskId)
|
||||
|
||||
if (result is ApiResult.Success) {
|
||||
@@ -738,12 +740,12 @@ object APILayer {
|
||||
|
||||
return when (result) {
|
||||
is ApiResult.Error -> result
|
||||
else -> ApiResult.Error("Unknown error")
|
||||
else -> ApiResult.Error(ClientStrings.t("err.unknown"))
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun clearInProgress(taskId: Int): ApiResult<TaskResponse> {
|
||||
val token = getToken() ?: return ApiResult.Error("Not authenticated", 401)
|
||||
val token = getToken() ?: return ApiResult.Error(ClientStrings.t("err.not_authenticated"), 401)
|
||||
val result = taskApi.clearInProgress(token, taskId)
|
||||
|
||||
if (result is ApiResult.Success) {
|
||||
@@ -754,12 +756,12 @@ object APILayer {
|
||||
|
||||
return when (result) {
|
||||
is ApiResult.Error -> result
|
||||
else -> ApiResult.Error("Unknown error")
|
||||
else -> ApiResult.Error(ClientStrings.t("err.unknown"))
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun archiveTask(taskId: Int): ApiResult<TaskResponse> {
|
||||
val token = getToken() ?: return ApiResult.Error("Not authenticated", 401)
|
||||
val token = getToken() ?: return ApiResult.Error(ClientStrings.t("err.not_authenticated"), 401)
|
||||
val result = taskApi.archiveTask(token, taskId)
|
||||
|
||||
if (result is ApiResult.Success) {
|
||||
@@ -770,12 +772,12 @@ object APILayer {
|
||||
|
||||
return when (result) {
|
||||
is ApiResult.Error -> result
|
||||
else -> ApiResult.Error("Unknown error")
|
||||
else -> ApiResult.Error(ClientStrings.t("err.unknown"))
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun unarchiveTask(taskId: Int): ApiResult<TaskResponse> {
|
||||
val token = getToken() ?: return ApiResult.Error("Not authenticated", 401)
|
||||
val token = getToken() ?: return ApiResult.Error(ClientStrings.t("err.not_authenticated"), 401)
|
||||
val result = taskApi.unarchiveTask(token, taskId)
|
||||
|
||||
if (result is ApiResult.Success) {
|
||||
@@ -786,12 +788,12 @@ object APILayer {
|
||||
|
||||
return when (result) {
|
||||
is ApiResult.Error -> result
|
||||
else -> ApiResult.Error("Unknown error")
|
||||
else -> ApiResult.Error(ClientStrings.t("err.unknown"))
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun createTaskCompletion(request: TaskCompletionCreateRequest): ApiResult<TaskCompletionResponse> {
|
||||
val token = getToken() ?: return ApiResult.Error("Not authenticated", 401)
|
||||
val token = getToken() ?: return ApiResult.Error(ClientStrings.t("err.not_authenticated"), 401)
|
||||
val result = taskCompletionApi.createCompletion(token, request)
|
||||
|
||||
if (result is ApiResult.Success) {
|
||||
@@ -806,7 +808,7 @@ object APILayer {
|
||||
|
||||
return when (result) {
|
||||
is ApiResult.Error -> result
|
||||
else -> ApiResult.Error("Unknown error")
|
||||
else -> ApiResult.Error(ClientStrings.t("err.unknown"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -814,7 +816,7 @@ object APILayer {
|
||||
* Get all completions for a specific task
|
||||
*/
|
||||
suspend fun getTaskCompletions(taskId: Int): ApiResult<List<TaskCompletionResponse>> {
|
||||
val token = getToken() ?: return ApiResult.Error("Not authenticated", 401)
|
||||
val token = getToken() ?: return ApiResult.Error(ClientStrings.t("err.not_authenticated"), 401)
|
||||
val result = taskApi.getTaskCompletions(token, taskId)
|
||||
if (result is ApiResult.Success) {
|
||||
DataManager.setTaskCompletions(taskId, result.data)
|
||||
@@ -848,7 +850,7 @@ object APILayer {
|
||||
println("[APILayer] CACHE MISS: documents (forceRefresh=$forceRefresh, hasFilters=$hasFilters)")
|
||||
|
||||
// Fetch from API
|
||||
val token = getToken() ?: return ApiResult.Error("Not authenticated", 401)
|
||||
val token = getToken() ?: return ApiResult.Error(ClientStrings.t("err.not_authenticated"), 401)
|
||||
val result = documentApi.getDocuments(
|
||||
token, residenceId, documentType, category, contractorId,
|
||||
isActive, expiringSoon, tags, search
|
||||
@@ -872,7 +874,7 @@ object APILayer {
|
||||
}
|
||||
|
||||
// Fetch from API
|
||||
val token = getToken() ?: return ApiResult.Error("Not authenticated", 401)
|
||||
val token = getToken() ?: return ApiResult.Error(ClientStrings.t("err.not_authenticated"), 401)
|
||||
val result = documentApi.getDocument(token, id)
|
||||
|
||||
// Update DataManager on success
|
||||
@@ -912,7 +914,7 @@ object APILayer {
|
||||
fileNamesList: List<String>? = null,
|
||||
mimeTypesList: List<String>? = null
|
||||
): ApiResult<Document> {
|
||||
val token = getToken() ?: return ApiResult.Error("Not authenticated", 401)
|
||||
val token = getToken() ?: return ApiResult.Error(ClientStrings.t("err.not_authenticated"), 401)
|
||||
val result = documentApi.createDocument(
|
||||
token, title, documentType, residenceId, description, category,
|
||||
tags, notes, contractorId, isActive, itemName, modelNumber,
|
||||
@@ -951,7 +953,7 @@ object APILayer {
|
||||
startDate: String? = null,
|
||||
endDate: String? = null
|
||||
): ApiResult<Document> {
|
||||
val token = getToken() ?: return ApiResult.Error("Not authenticated", 401)
|
||||
val token = getToken() ?: return ApiResult.Error(ClientStrings.t("err.not_authenticated"), 401)
|
||||
val result = documentApi.updateDocument(
|
||||
token, id, title, documentType, description, category, tags, notes,
|
||||
contractorId, isActive, itemName, modelNumber, serialNumber, provider,
|
||||
@@ -968,7 +970,7 @@ object APILayer {
|
||||
}
|
||||
|
||||
suspend fun deleteDocument(id: Int): ApiResult<Unit> {
|
||||
val token = getToken() ?: return ApiResult.Error("Not authenticated", 401)
|
||||
val token = getToken() ?: return ApiResult.Error(ClientStrings.t("err.not_authenticated"), 401)
|
||||
val result = documentApi.deleteDocument(token, id)
|
||||
|
||||
// Update DataManager on success
|
||||
@@ -986,7 +988,7 @@ object APILayer {
|
||||
mimeType: String,
|
||||
caption: String? = null
|
||||
): ApiResult<Document> {
|
||||
val token = getToken() ?: return ApiResult.Error("Not authenticated", 401)
|
||||
val token = getToken() ?: return ApiResult.Error(ClientStrings.t("err.not_authenticated"), 401)
|
||||
val result = documentApi.uploadDocumentImage(token, documentId, imageBytes, fileName, mimeType, caption)
|
||||
if (result is ApiResult.Success) {
|
||||
DataManager.updateDocument(result.data)
|
||||
@@ -995,7 +997,7 @@ object APILayer {
|
||||
}
|
||||
|
||||
suspend fun deleteDocumentImage(documentId: Int, imageId: Int): ApiResult<Document> {
|
||||
val token = getToken() ?: return ApiResult.Error("Not authenticated", 401)
|
||||
val token = getToken() ?: return ApiResult.Error(ClientStrings.t("err.not_authenticated"), 401)
|
||||
val result = documentApi.deleteDocumentImage(token, documentId, imageId)
|
||||
if (result is ApiResult.Success) {
|
||||
DataManager.updateDocument(result.data)
|
||||
@@ -1004,7 +1006,7 @@ object APILayer {
|
||||
}
|
||||
|
||||
suspend fun downloadDocument(url: String): ApiResult<ByteArray> {
|
||||
val token = getToken() ?: return ApiResult.Error("Not authenticated", 401)
|
||||
val token = getToken() ?: return ApiResult.Error(ClientStrings.t("err.not_authenticated"), 401)
|
||||
return documentApi.downloadDocument(token, url)
|
||||
}
|
||||
|
||||
@@ -1026,7 +1028,7 @@ object APILayer {
|
||||
}
|
||||
|
||||
// Fetch from API
|
||||
val token = getToken() ?: return ApiResult.Error("Not authenticated", 401)
|
||||
val token = getToken() ?: return ApiResult.Error(ClientStrings.t("err.not_authenticated"), 401)
|
||||
val result = contractorApi.getContractors(token, specialty, isFavorite, isActive, search)
|
||||
|
||||
// Update DataManager on success (only for unfiltered results)
|
||||
@@ -1039,7 +1041,7 @@ object APILayer {
|
||||
|
||||
suspend fun getContractor(id: Int, forceRefresh: Boolean = false): ApiResult<Contractor> {
|
||||
// Fetch from API (summaries don't have full detail, always fetch)
|
||||
val token = getToken() ?: return ApiResult.Error("Not authenticated", 401)
|
||||
val token = getToken() ?: return ApiResult.Error(ClientStrings.t("err.not_authenticated"), 401)
|
||||
val result = contractorApi.getContractor(token, id)
|
||||
|
||||
// Update the summary in DataManager on success
|
||||
@@ -1052,7 +1054,7 @@ object APILayer {
|
||||
}
|
||||
|
||||
suspend fun createContractor(request: ContractorCreateRequest): ApiResult<Contractor> {
|
||||
val token = getToken() ?: return ApiResult.Error("Not authenticated", 401)
|
||||
val token = getToken() ?: return ApiResult.Error(ClientStrings.t("err.not_authenticated"), 401)
|
||||
val result = contractorApi.createContractor(token, request)
|
||||
|
||||
// Update DataManager on success
|
||||
@@ -1064,7 +1066,7 @@ object APILayer {
|
||||
}
|
||||
|
||||
suspend fun updateContractor(id: Int, request: ContractorUpdateRequest): ApiResult<Contractor> {
|
||||
val token = getToken() ?: return ApiResult.Error("Not authenticated", 401)
|
||||
val token = getToken() ?: return ApiResult.Error(ClientStrings.t("err.not_authenticated"), 401)
|
||||
val result = contractorApi.updateContractor(token, id, request)
|
||||
|
||||
// Update DataManager on success
|
||||
@@ -1076,7 +1078,7 @@ object APILayer {
|
||||
}
|
||||
|
||||
suspend fun deleteContractor(id: Int): ApiResult<Unit> {
|
||||
val token = getToken() ?: return ApiResult.Error("Not authenticated", 401)
|
||||
val token = getToken() ?: return ApiResult.Error(ClientStrings.t("err.not_authenticated"), 401)
|
||||
val result = contractorApi.deleteContractor(token, id)
|
||||
|
||||
// Update DataManager on success
|
||||
@@ -1088,7 +1090,7 @@ object APILayer {
|
||||
}
|
||||
|
||||
suspend fun toggleFavorite(id: Int): ApiResult<Contractor> {
|
||||
val token = getToken() ?: return ApiResult.Error("Not authenticated", 401)
|
||||
val token = getToken() ?: return ApiResult.Error(ClientStrings.t("err.not_authenticated"), 401)
|
||||
val result = contractorApi.toggleFavorite(token, id)
|
||||
|
||||
// Update DataManager on success
|
||||
@@ -1110,7 +1112,7 @@ object APILayer {
|
||||
|
||||
// If cache is empty or stale, fetch all contractors first to populate cache
|
||||
if (DataManager.contractors.value.isEmpty() || !DataManager.isCacheValid(DataManager.contractorsCacheTime)) {
|
||||
val token = getToken() ?: return ApiResult.Error("Not authenticated", 401)
|
||||
val token = getToken() ?: return ApiResult.Error(ClientStrings.t("err.not_authenticated"), 401)
|
||||
val result = contractorApi.getContractors(token, null, null, null, null)
|
||||
if (result is ApiResult.Success) {
|
||||
DataManager.setContractors(result.data)
|
||||
@@ -1172,7 +1174,7 @@ object APILayer {
|
||||
initializeLookups()
|
||||
return DataManager.taskTemplatesGrouped.value?.let {
|
||||
ApiResult.Success(it)
|
||||
} ?: ApiResult.Error("Failed to load task templates")
|
||||
} ?: ApiResult.Error(ClientStrings.t("err.api.failed_load_task_templates"))
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1220,14 +1222,14 @@ object APILayer {
|
||||
val cached = DataManager.taskTemplates.value.find { it.id == id }
|
||||
return cached?.let {
|
||||
ApiResult.Success(it)
|
||||
} ?: ApiResult.Error("Task template not found")
|
||||
} ?: ApiResult.Error(ClientStrings.t("err.api.task_template_not_found"))
|
||||
}
|
||||
|
||||
/**
|
||||
* Get personalized task suggestions for a residence based on its home profile.
|
||||
*/
|
||||
suspend fun getTaskSuggestions(residenceId: Int): ApiResult<TaskSuggestionsResponse> {
|
||||
val token = getToken() ?: return ApiResult.Error("Not authenticated", 401)
|
||||
val token = getToken() ?: return ApiResult.Error(ClientStrings.t("err.not_authenticated"), 401)
|
||||
return taskTemplateApi.getTaskSuggestions(token, residenceId)
|
||||
}
|
||||
|
||||
@@ -1266,7 +1268,7 @@ object APILayer {
|
||||
}
|
||||
|
||||
suspend fun logout(): ApiResult<Unit> {
|
||||
val token = getToken() ?: return ApiResult.Error("Not authenticated", 401)
|
||||
val token = getToken() ?: return ApiResult.Error(ClientStrings.t("err.not_authenticated"), 401)
|
||||
val result = authApi.logout(token)
|
||||
|
||||
// Clear DataManager on logout (success or failure)
|
||||
@@ -1289,13 +1291,13 @@ object APILayer {
|
||||
* `ApiClient` 401 plumbing continue to compile.
|
||||
*/
|
||||
suspend fun refreshToken(): ApiResult<String> {
|
||||
val currentToken = getToken() ?: return ApiResult.Error("No token", 401)
|
||||
val currentToken = getToken() ?: return ApiResult.Error(ClientStrings.t("err.api.no_token"), 401)
|
||||
return when (val result = authApi.refreshToken(currentToken)) {
|
||||
// Kratos session tokens are never rotated — echo the same token
|
||||
// back when the session is confirmed still valid.
|
||||
is ApiResult.Success -> ApiResult.Success(currentToken)
|
||||
is ApiResult.Error -> ApiResult.Error(result.message, result.code)
|
||||
else -> ApiResult.Error("Unexpected state")
|
||||
else -> ApiResult.Error(ClientStrings.t("err.api.unexpected_state"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1309,7 +1311,7 @@ object APILayer {
|
||||
}
|
||||
|
||||
// Fetch from API
|
||||
val token = getToken() ?: return ApiResult.Error("Not authenticated", 401)
|
||||
val token = getToken() ?: return ApiResult.Error(ClientStrings.t("err.not_authenticated"), 401)
|
||||
val result = authApi.getCurrentUser(token)
|
||||
|
||||
// Update DataManager on success
|
||||
@@ -1389,7 +1391,7 @@ object APILayer {
|
||||
}
|
||||
|
||||
suspend fun deleteAccount(password: String? = null, confirmation: String? = null): ApiResult<Unit> {
|
||||
val token = getToken() ?: return ApiResult.Error("Not authenticated", 401)
|
||||
val token = getToken() ?: return ApiResult.Error(ClientStrings.t("err.not_authenticated"), 401)
|
||||
val result = authApi.deleteAccount(token, DeleteAccountRequest(password = password, confirmation = confirmation))
|
||||
|
||||
// Clear DataManager on successful deletion
|
||||
@@ -1426,7 +1428,7 @@ object APILayer {
|
||||
bytes: ByteArray,
|
||||
fileName: String,
|
||||
): ApiResult<Int> {
|
||||
val token = getToken() ?: return ApiResult.Error("Not authenticated", 401)
|
||||
val token = getToken() ?: return ApiResult.Error(ClientStrings.t("err.not_authenticated"), 401)
|
||||
return uploadApi.uploadOne(
|
||||
token = token,
|
||||
category = category,
|
||||
@@ -1439,46 +1441,46 @@ object APILayer {
|
||||
// ==================== Notification Operations ====================
|
||||
|
||||
suspend fun registerDevice(request: DeviceRegistrationRequest): ApiResult<DeviceRegistrationResponse> {
|
||||
val token = getToken() ?: return ApiResult.Error("Not authenticated", 401)
|
||||
val token = getToken() ?: return ApiResult.Error(ClientStrings.t("err.not_authenticated"), 401)
|
||||
return notificationApi.registerDevice(token, request)
|
||||
}
|
||||
|
||||
suspend fun unregisterDevice(deviceId: Int): ApiResult<Unit> {
|
||||
val token = getToken() ?: return ApiResult.Error("Not authenticated", 401)
|
||||
val token = getToken() ?: return ApiResult.Error(ClientStrings.t("err.not_authenticated"), 401)
|
||||
return notificationApi.unregisterDevice(token, deviceId)
|
||||
}
|
||||
|
||||
suspend fun getNotificationPreferences(): ApiResult<NotificationPreference> {
|
||||
val token = getToken() ?: return ApiResult.Error("Not authenticated", 401)
|
||||
val token = getToken() ?: return ApiResult.Error(ClientStrings.t("err.not_authenticated"), 401)
|
||||
val result = notificationApi.getNotificationPreferences(token)
|
||||
if (result is ApiResult.Success) DataManager.setNotificationPreferences(result.data)
|
||||
return result
|
||||
}
|
||||
|
||||
suspend fun updateNotificationPreferences(request: UpdateNotificationPreferencesRequest): ApiResult<NotificationPreference> {
|
||||
val token = getToken() ?: return ApiResult.Error("Not authenticated", 401)
|
||||
val token = getToken() ?: return ApiResult.Error(ClientStrings.t("err.not_authenticated"), 401)
|
||||
val result = notificationApi.updateNotificationPreferences(token, request)
|
||||
if (result is ApiResult.Success) DataManager.setNotificationPreferences(result.data)
|
||||
return result
|
||||
}
|
||||
|
||||
suspend fun getNotificationHistory(): ApiResult<List<Notification>> {
|
||||
val token = getToken() ?: return ApiResult.Error("Not authenticated", 401)
|
||||
val token = getToken() ?: return ApiResult.Error(ClientStrings.t("err.not_authenticated"), 401)
|
||||
return notificationApi.getNotificationHistory(token)
|
||||
}
|
||||
|
||||
suspend fun markNotificationAsRead(notificationId: Int): ApiResult<MessageResponse> {
|
||||
val token = getToken() ?: return ApiResult.Error("Not authenticated", 401)
|
||||
val token = getToken() ?: return ApiResult.Error(ClientStrings.t("err.not_authenticated"), 401)
|
||||
return notificationApi.markNotificationAsRead(token, notificationId)
|
||||
}
|
||||
|
||||
suspend fun markAllNotificationsAsRead(): ApiResult<MessageResponse> {
|
||||
val token = getToken() ?: return ApiResult.Error("Not authenticated", 401)
|
||||
val token = getToken() ?: return ApiResult.Error(ClientStrings.t("err.not_authenticated"), 401)
|
||||
return notificationApi.markAllNotificationsAsRead(token)
|
||||
}
|
||||
|
||||
suspend fun getUnreadCount(): ApiResult<UnreadCountResponse> {
|
||||
val token = getToken() ?: return ApiResult.Error("Not authenticated", 401)
|
||||
val token = getToken() ?: return ApiResult.Error(ClientStrings.t("err.not_authenticated"), 401)
|
||||
return notificationApi.getUnreadCount(token)
|
||||
}
|
||||
|
||||
@@ -1489,7 +1491,7 @@ object APILayer {
|
||||
* Returns cached data from DataManager if available and forceRefresh is false.
|
||||
*/
|
||||
suspend fun getSubscriptionStatus(forceRefresh: Boolean = false): ApiResult<SubscriptionStatus> {
|
||||
val token = getToken() ?: return ApiResult.Error("Not authenticated", 401)
|
||||
val token = getToken() ?: return ApiResult.Error(ClientStrings.t("err.not_authenticated"), 401)
|
||||
|
||||
// Return cached subscription if available and not forcing refresh
|
||||
if (!forceRefresh) {
|
||||
@@ -1513,7 +1515,7 @@ object APILayer {
|
||||
* Verify Android purchase with backend
|
||||
*/
|
||||
suspend fun verifyAndroidPurchase(purchaseToken: String, productId: String): ApiResult<VerificationResponse> {
|
||||
val token = getToken() ?: return ApiResult.Error("Not authenticated", 401)
|
||||
val token = getToken() ?: return ApiResult.Error(ClientStrings.t("err.not_authenticated"), 401)
|
||||
return subscriptionApi.verifyAndroidPurchase(token, purchaseToken, productId)
|
||||
}
|
||||
|
||||
@@ -1521,7 +1523,7 @@ object APILayer {
|
||||
* Verify iOS receipt with backend
|
||||
*/
|
||||
suspend fun verifyIOSReceipt(receiptData: String, transactionId: String): ApiResult<VerificationResponse> {
|
||||
val token = getToken() ?: return ApiResult.Error("Not authenticated", 401)
|
||||
val token = getToken() ?: return ApiResult.Error(ClientStrings.t("err.not_authenticated"), 401)
|
||||
return subscriptionApi.verifyIOSReceipt(token, receiptData, transactionId)
|
||||
}
|
||||
|
||||
@@ -1529,7 +1531,7 @@ object APILayer {
|
||||
* Fetch feature benefits from backend (requires auth).
|
||||
*/
|
||||
suspend fun getFeatureBenefits(): ApiResult<List<FeatureBenefit>> {
|
||||
val token = getToken() ?: return ApiResult.Error("Not authenticated", 401)
|
||||
val token = getToken() ?: return ApiResult.Error(ClientStrings.t("err.not_authenticated"), 401)
|
||||
val result = subscriptionApi.getFeatureBenefits(token)
|
||||
if (result is ApiResult.Success) {
|
||||
DataManager.setFeatureBenefits(result.data)
|
||||
|
||||
@@ -196,8 +196,8 @@ private suspend fun attemptTokenRefresh(currentToken: String): Boolean {
|
||||
* ([CoilAuthInterceptor] plumbing, tests) continue to compile.
|
||||
*/
|
||||
class TokenExpiredException(val refreshed: Boolean) : Exception(
|
||||
if (refreshed) "Session was briefly rejected but is still valid — retry the request"
|
||||
else "Session expired — user must re-authenticate"
|
||||
if (refreshed) "Session was briefly rejected but is still valid — retry the request" // i18n-ignore: internal log/diagnostic message, non-UI
|
||||
else "Session expired — user must re-authenticate" // i18n-ignore: internal log/diagnostic message, non-UI
|
||||
)
|
||||
|
||||
object ApiClient {
|
||||
|
||||
@@ -67,9 +67,9 @@ object ApiConfig {
|
||||
*/
|
||||
fun getEnvironmentName(): String {
|
||||
return when (CURRENT_ENV) {
|
||||
Environment.LOCAL -> "Local (${getLocalhostAddress()}:8000)"
|
||||
Environment.DEV -> "Dev Server (devapi.myhoneydue.com)"
|
||||
Environment.PROD -> "Production (api.myhoneydue.com)"
|
||||
Environment.LOCAL -> "Local (${getLocalhostAddress()}:8000)" // i18n-ignore: dev environment label, non-UI debug menu
|
||||
Environment.DEV -> "Dev Server (devapi.myhoneydue.com)" // i18n-ignore: dev environment label, non-UI debug menu
|
||||
Environment.PROD -> "Production (api.myhoneydue.com)" // i18n-ignore: dev environment label, non-UI debug menu
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package com.tt.honeyDue.network
|
||||
|
||||
import com.tt.honeyDue.i18n.ClientStrings
|
||||
|
||||
import com.tt.honeyDue.data.DataManager
|
||||
import com.tt.honeyDue.models.*
|
||||
import io.ktor.client.*
|
||||
@@ -102,12 +104,12 @@ class AuthApi(private val client: HttpClient = ApiClient.httpClient) {
|
||||
ApiResult.Success(json.decodeFromString(KratosFlow.serializer(), response.bodyAsText()))
|
||||
} else {
|
||||
ApiResult.Error(
|
||||
"Could not start $flow (Kratos ${response.status.value})",
|
||||
ClientStrings.t("err.auth.could_not_start_flow", flow, response.status.value.toString()),
|
||||
response.status.value,
|
||||
)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Could not reach the authentication server")
|
||||
ApiResult.Error(e.message ?: ClientStrings.t("err.auth.could_not_reach_server"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,7 +136,7 @@ class AuthApi(private val client: HttpClient = ApiClient.httpClient) {
|
||||
ApiResult.Error(extractKratosError(text, response.status.value), response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Authentication request failed")
|
||||
ApiResult.Error(e.message ?: ClientStrings.t("err.auth.request_failed"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -164,7 +166,7 @@ class AuthApi(private val client: HttpClient = ApiClient.httpClient) {
|
||||
val msg = env.error?.reason ?: env.error?.message
|
||||
if (!msg.isNullOrBlank()) return msg
|
||||
}
|
||||
return "Authentication failed ($statusCode)"
|
||||
return ClientStrings.t("err.auth.authentication_failed_status", statusCode.toString())
|
||||
}
|
||||
|
||||
// ==================== Login ====================
|
||||
@@ -179,7 +181,7 @@ class AuthApi(private val client: HttpClient = ApiClient.httpClient) {
|
||||
val flow = when (val f = initFlow("login")) {
|
||||
is ApiResult.Success -> f.data
|
||||
is ApiResult.Error -> return f
|
||||
else -> return ApiResult.Error("Could not start login")
|
||||
else -> return ApiResult.Error(ClientStrings.t("err.auth.could_not_start_login"))
|
||||
}
|
||||
val body = json.encodeToString(
|
||||
KratosPasswordLoginBody.serializer(),
|
||||
@@ -197,7 +199,7 @@ class AuthApi(private val client: HttpClient = ApiClient.httpClient) {
|
||||
resolveSession(success.data.sessionToken, success.data.session)
|
||||
}
|
||||
is ApiResult.Error -> success
|
||||
else -> ApiResult.Error("Login failed")
|
||||
else -> ApiResult.Error(ClientStrings.t("err.auth.login_failed"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -227,7 +229,7 @@ class AuthApi(private val client: HttpClient = ApiClient.httpClient) {
|
||||
when (val created = createAccount(request)) {
|
||||
is ApiResult.Success -> Unit
|
||||
is ApiResult.Error -> return created
|
||||
else -> return ApiResult.Error("Registration failed")
|
||||
else -> return ApiResult.Error(ClientStrings.t("err.auth.registration_failed"))
|
||||
}
|
||||
// 2. Log in immediately to get a session token. The identifier is the
|
||||
// email (the Kratos credential identifier), not the display username.
|
||||
@@ -252,7 +254,7 @@ class AuthApi(private val client: HttpClient = ApiClient.httpClient) {
|
||||
ApiResult.Error(ErrorParser.parseError(response), response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Could not create account")
|
||||
ApiResult.Error(e.message ?: ClientStrings.t("err.auth.could_not_create_account"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -279,7 +281,7 @@ class AuthApi(private val client: HttpClient = ApiClient.httpClient) {
|
||||
val loginFlow = when (val f = initFlow("login")) {
|
||||
is ApiResult.Success -> f.data
|
||||
is ApiResult.Error -> return f
|
||||
else -> return ApiResult.Error("Could not start sign-in")
|
||||
else -> return ApiResult.Error(ClientStrings.t("err.auth.could_not_start_signin"))
|
||||
}
|
||||
val loginBody = json.encodeToString(
|
||||
KratosOidcBody.serializer(),
|
||||
@@ -302,7 +304,7 @@ class AuthApi(private val client: HttpClient = ApiClient.httpClient) {
|
||||
val regFlow = when (val f = initFlow("registration")) {
|
||||
is ApiResult.Success -> f.data
|
||||
is ApiResult.Error -> return (loginResult as? ApiResult.Error) ?: f
|
||||
else -> return ApiResult.Error("Could not start sign-up")
|
||||
else -> return ApiResult.Error(ClientStrings.t("err.auth.could_not_start_signup"))
|
||||
}
|
||||
val regBody = json.encodeToString(
|
||||
KratosOidcBody.serializer(),
|
||||
@@ -320,13 +322,13 @@ class AuthApi(private val client: HttpClient = ApiClient.httpClient) {
|
||||
val token = regResult.data.sessionToken
|
||||
if (token.isNullOrBlank()) {
|
||||
(loginResult as? ApiResult.Error)
|
||||
?: ApiResult.Error("Sign-in did not return a session")
|
||||
?: ApiResult.Error(ClientStrings.t("err.auth.signin_no_session"))
|
||||
} else {
|
||||
resolveSession(token, regResult.data.session)
|
||||
}
|
||||
}
|
||||
is ApiResult.Error -> regResult
|
||||
else -> ApiResult.Error("Sign-in failed")
|
||||
else -> ApiResult.Error(ClientStrings.t("err.auth.signin_failed"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -350,7 +352,7 @@ class AuthApi(private val client: HttpClient = ApiClient.httpClient) {
|
||||
AppleSignInResponse(token = r.data.token, user = r.data.user, isNewUser = false),
|
||||
)
|
||||
is ApiResult.Error -> r
|
||||
else -> ApiResult.Error("Apple Sign In failed")
|
||||
else -> ApiResult.Error(ClientStrings.t("err.auth.apple_signin_failed"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -364,7 +366,7 @@ class AuthApi(private val client: HttpClient = ApiClient.httpClient) {
|
||||
GoogleSignInResponse(token = r.data.token, user = r.data.user, isNewUser = false),
|
||||
)
|
||||
is ApiResult.Error -> r
|
||||
else -> ApiResult.Error("Google Sign In failed")
|
||||
else -> ApiResult.Error(ClientStrings.t("err.auth.google_signin_failed"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -405,7 +407,7 @@ class AuthApi(private val client: HttpClient = ApiClient.httpClient) {
|
||||
val flow = when (val f = initFlow("recovery")) {
|
||||
is ApiResult.Success -> f.data
|
||||
is ApiResult.Error -> return f
|
||||
else -> return ApiResult.Error("Could not start password recovery")
|
||||
else -> return ApiResult.Error(ClientStrings.t("err.auth.could_not_start_recovery"))
|
||||
}
|
||||
val body = json.encodeToString(
|
||||
KratosRecoveryBody.serializer(),
|
||||
@@ -424,12 +426,12 @@ class AuthApi(private val client: HttpClient = ApiClient.httpClient) {
|
||||
val info = result.data?.ui?.messages?.firstOrNull()?.text
|
||||
ApiResult.Success(
|
||||
ForgotPasswordResponse(
|
||||
message = info ?: "If that email exists, a recovery code has been sent.",
|
||||
message = info ?: ClientStrings.t("auth.recovery_sent"),
|
||||
),
|
||||
)
|
||||
}
|
||||
is ApiResult.Error -> result
|
||||
else -> ApiResult.Error("Could not send recovery code")
|
||||
else -> ApiResult.Error(ClientStrings.t("err.auth.could_not_send_recovery_code"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -445,7 +447,7 @@ class AuthApi(private val client: HttpClient = ApiClient.httpClient) {
|
||||
*/
|
||||
suspend fun verifyResetCode(request: VerifyResetCodeRequest): ApiResult<VerifyResetCodeResponse> {
|
||||
val action = pendingRecoveryAction
|
||||
?: return ApiResult.Error("Your recovery session expired. Request a new code.")
|
||||
?: return ApiResult.Error(ClientStrings.t("err.auth.recovery_session_expired"))
|
||||
val body = json.encodeToString(
|
||||
KratosRecoveryBody.serializer(),
|
||||
KratosRecoveryBody(code = request.code.trim()),
|
||||
@@ -464,7 +466,7 @@ class AuthApi(private val client: HttpClient = ApiClient.httpClient) {
|
||||
pendingRecoveryAction = null
|
||||
ApiResult.Success(
|
||||
VerifyResetCodeResponse(
|
||||
message = "Code verified.",
|
||||
message = ClientStrings.t("auth.code_verified"),
|
||||
// Opaque to the UI: carries the settings flow id and
|
||||
// the privileged session token resetPassword needs,
|
||||
// packed as "<settingsFlowId>|<sessionToken>".
|
||||
@@ -477,11 +479,11 @@ class AuthApi(private val client: HttpClient = ApiClient.httpClient) {
|
||||
val msg = flow?.ui?.messages?.firstOrNull { it.type == "error" }?.text
|
||||
?: flow?.ui?.nodes?.flatMap { it.messages }
|
||||
?.firstOrNull { it.type == "error" }?.text
|
||||
ApiResult.Error(msg ?: "Invalid or expired code")
|
||||
ApiResult.Error(msg ?: ClientStrings.t("err.auth.invalid_code"))
|
||||
}
|
||||
}
|
||||
is ApiResult.Error -> result
|
||||
else -> ApiResult.Error("Invalid or expired code")
|
||||
else -> ApiResult.Error(ClientStrings.t("err.auth.invalid_code"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -496,7 +498,7 @@ class AuthApi(private val client: HttpClient = ApiClient.httpClient) {
|
||||
suspend fun resetPassword(request: ResetPasswordRequest): ApiResult<ResetPasswordResponse> {
|
||||
val parts = request.resetToken.split("|", limit = 2)
|
||||
if (parts.size != 2 || parts[0].isBlank() || parts[1].isBlank()) {
|
||||
return ApiResult.Error("This password reset session has expired. Request a new code.")
|
||||
return ApiResult.Error(ClientStrings.t("err.auth.reset_session_expired"))
|
||||
}
|
||||
val settingsFlowId = parts[0]
|
||||
val sessionToken = parts[1]
|
||||
@@ -517,12 +519,12 @@ class AuthApi(private val client: HttpClient = ApiClient.httpClient) {
|
||||
}
|
||||
val text = response.bodyAsText()
|
||||
if (response.status.isSuccess()) {
|
||||
ApiResult.Success(ResetPasswordResponse(message = "Password updated. You can now sign in."))
|
||||
ApiResult.Success(ResetPasswordResponse(message = ClientStrings.t("auth.password_updated")))
|
||||
} else {
|
||||
ApiResult.Error(extractKratosError(text, response.status.value), response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Could not reset password")
|
||||
ApiResult.Error(e.message ?: ClientStrings.t("err.auth.could_not_reset_password"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -554,7 +556,7 @@ class AuthApi(private val client: HttpClient = ApiClient.httpClient) {
|
||||
val flow = when (val f = initFlow("verification")) {
|
||||
is ApiResult.Success -> f.data
|
||||
is ApiResult.Error -> return f
|
||||
else -> return ApiResult.Error("Could not start verification")
|
||||
else -> return ApiResult.Error(ClientStrings.t("err.auth.could_not_start_verification"))
|
||||
}
|
||||
val body = json.encodeToString(
|
||||
KratosVerificationBody.serializer(),
|
||||
@@ -571,7 +573,7 @@ class AuthApi(private val client: HttpClient = ApiClient.httpClient) {
|
||||
ApiResult.Success(Unit)
|
||||
}
|
||||
is ApiResult.Error -> result
|
||||
else -> ApiResult.Error("Could not send verification code")
|
||||
else -> ApiResult.Error(ClientStrings.t("err.auth.could_not_send_verification_code"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -593,7 +595,7 @@ class AuthApi(private val client: HttpClient = ApiClient.httpClient) {
|
||||
suspend fun verifyEmail(token: String, request: VerifyEmailRequest): ApiResult<VerifyEmailResponse> {
|
||||
val pendingFlowId = DataManager.pendingVerificationFlowId.value
|
||||
if (pendingFlowId.isNullOrBlank()) {
|
||||
return ApiResult.Error("Please request a verification code first.")
|
||||
return ApiResult.Error(ClientStrings.t("err.auth.request_verification_first"))
|
||||
}
|
||||
val body = json.encodeToString(
|
||||
KratosVerificationBody.serializer(),
|
||||
@@ -609,18 +611,18 @@ class AuthApi(private val client: HttpClient = ApiClient.httpClient) {
|
||||
// One-shot — drop the cached flow so a future verification
|
||||
// (different account, address change, etc.) starts clean.
|
||||
DataManager.setPendingVerificationFlowId(null)
|
||||
ApiResult.Success(VerifyEmailResponse(message = "Email verified.", verified = true))
|
||||
ApiResult.Success(VerifyEmailResponse(message = ClientStrings.t("auth.email_verified"), verified = true))
|
||||
} else {
|
||||
// 200 with a re-rendered flow == wrong/expired code. Surface
|
||||
// Kratos' own message so the user knows to retry/resend.
|
||||
val msg = result.data?.ui?.messages?.firstOrNull { it.type == "error" }?.text
|
||||
?: result.data?.ui?.nodes?.flatMap { it.messages }
|
||||
?.firstOrNull { it.type == "error" }?.text
|
||||
ApiResult.Error(msg ?: "Invalid or expired code. Tap resend for a new one.")
|
||||
ApiResult.Error(msg ?: ClientStrings.t("err.auth.invalid_code_resend"))
|
||||
}
|
||||
}
|
||||
is ApiResult.Error -> result
|
||||
else -> ApiResult.Error("Verification failed")
|
||||
else -> ApiResult.Error(ClientStrings.t("err.auth.verification_failed"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -641,10 +643,10 @@ class AuthApi(private val client: HttpClient = ApiClient.httpClient) {
|
||||
if (response.status.isSuccess()) {
|
||||
ApiResult.Success(response.body())
|
||||
} else {
|
||||
ApiResult.Error("Failed to get user", response.status.value)
|
||||
ApiResult.Error(ClientStrings.t("err.auth.failed_get_user"), response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
ApiResult.Error(e.message ?: ClientStrings.t("err.unknown_occurred"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -665,7 +667,7 @@ class AuthApi(private val client: HttpClient = ApiClient.httpClient) {
|
||||
ApiResult.Error(ErrorParser.parseError(response), response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
ApiResult.Error(e.message ?: ClientStrings.t("err.unknown_occurred"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -687,7 +689,7 @@ class AuthApi(private val client: HttpClient = ApiClient.httpClient) {
|
||||
ApiResult.Error(ErrorParser.parseError(response), response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
ApiResult.Error(e.message ?: ClientStrings.t("err.unknown_occurred"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -710,10 +712,10 @@ class AuthApi(private val client: HttpClient = ApiClient.httpClient) {
|
||||
if (response.status.isSuccess()) {
|
||||
ApiResult.Success(TokenRefreshResponse(token = token))
|
||||
} else {
|
||||
ApiResult.Error("Session expired — please sign in again", response.status.value)
|
||||
ApiResult.Error(ClientStrings.t("err.auth.session_expired"), response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Could not validate session")
|
||||
ApiResult.Error(e.message ?: ClientStrings.t("err.auth.could_not_validate_session"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -744,7 +746,7 @@ class AuthApi(private val client: HttpClient = ApiClient.httpClient) {
|
||||
me
|
||||
}
|
||||
}
|
||||
else -> ApiResult.Error("Could not load profile after sign-in")
|
||||
else -> ApiResult.Error(ClientStrings.t("err.auth.could_not_load_profile"))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package com.tt.honeyDue.network
|
||||
|
||||
import com.tt.honeyDue.i18n.ClientStrings
|
||||
|
||||
import com.tt.honeyDue.models.*
|
||||
import io.ktor.client.*
|
||||
import io.ktor.client.call.*
|
||||
@@ -28,10 +30,10 @@ class ContractorApi(private val client: HttpClient = ApiClient.httpClient) {
|
||||
if (response.status.isSuccess()) {
|
||||
ApiResult.Success(response.body())
|
||||
} else {
|
||||
ApiResult.Error("Failed to fetch contractors", response.status.value)
|
||||
ApiResult.Error(ClientStrings.t("err.api.failed_fetch_contractors"), response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
ApiResult.Error(e.message ?: ClientStrings.t("err.unknown_occurred"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,10 +46,10 @@ class ContractorApi(private val client: HttpClient = ApiClient.httpClient) {
|
||||
if (response.status.isSuccess()) {
|
||||
ApiResult.Success(response.body())
|
||||
} else {
|
||||
ApiResult.Error("Failed to fetch contractor", response.status.value)
|
||||
ApiResult.Error(ClientStrings.t("err.api.failed_fetch_contractor"), response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
ApiResult.Error(e.message ?: ClientStrings.t("err.unknown_occurred"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,12 +67,12 @@ class ContractorApi(private val client: HttpClient = ApiClient.httpClient) {
|
||||
val errorBody = try {
|
||||
response.body<String>()
|
||||
} catch (e: Exception) {
|
||||
"Failed to create contractor"
|
||||
ClientStrings.t("err.api.failed_create_contractor")
|
||||
}
|
||||
ApiResult.Error(errorBody, response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
ApiResult.Error(e.message ?: ClientStrings.t("err.unknown_occurred"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,12 +90,12 @@ class ContractorApi(private val client: HttpClient = ApiClient.httpClient) {
|
||||
val errorBody = try {
|
||||
response.body<String>()
|
||||
} catch (e: Exception) {
|
||||
"Failed to update contractor"
|
||||
ClientStrings.t("err.api.failed_update_contractor")
|
||||
}
|
||||
ApiResult.Error(errorBody, response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
ApiResult.Error(e.message ?: ClientStrings.t("err.unknown_occurred"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,10 +108,10 @@ class ContractorApi(private val client: HttpClient = ApiClient.httpClient) {
|
||||
if (response.status.isSuccess()) {
|
||||
ApiResult.Success(Unit)
|
||||
} else {
|
||||
ApiResult.Error("Failed to delete contractor", response.status.value)
|
||||
ApiResult.Error(ClientStrings.t("err.api.failed_delete_contractor"), response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
ApiResult.Error(e.message ?: ClientStrings.t("err.unknown_occurred"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,10 +124,10 @@ class ContractorApi(private val client: HttpClient = ApiClient.httpClient) {
|
||||
if (response.status.isSuccess()) {
|
||||
ApiResult.Success(response.body())
|
||||
} else {
|
||||
ApiResult.Error("Failed to toggle favorite", response.status.value)
|
||||
ApiResult.Error(ClientStrings.t("err.api.failed_toggle_favorite"), response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
ApiResult.Error(e.message ?: ClientStrings.t("err.unknown_occurred"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -138,10 +140,10 @@ class ContractorApi(private val client: HttpClient = ApiClient.httpClient) {
|
||||
if (response.status.isSuccess()) {
|
||||
ApiResult.Success(response.body())
|
||||
} else {
|
||||
ApiResult.Error("Failed to fetch contractor tasks", response.status.value)
|
||||
ApiResult.Error(ClientStrings.t("err.api.failed_fetch_contractor_tasks"), response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
ApiResult.Error(e.message ?: ClientStrings.t("err.unknown_occurred"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -154,10 +156,10 @@ class ContractorApi(private val client: HttpClient = ApiClient.httpClient) {
|
||||
if (response.status.isSuccess()) {
|
||||
ApiResult.Success(response.body())
|
||||
} else {
|
||||
ApiResult.Error("Failed to fetch contractors for residence", response.status.value)
|
||||
ApiResult.Error(ClientStrings.t("err.api.failed_fetch_contractors_for_residence"), response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
ApiResult.Error(e.message ?: ClientStrings.t("err.unknown_occurred"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package com.tt.honeyDue.network
|
||||
|
||||
import com.tt.honeyDue.i18n.ClientStrings
|
||||
|
||||
import com.tt.honeyDue.models.*
|
||||
import io.ktor.client.*
|
||||
import io.ktor.client.call.*
|
||||
@@ -37,10 +39,10 @@ class DocumentApi(private val client: HttpClient = ApiClient.httpClient) {
|
||||
if (response.status.isSuccess()) {
|
||||
ApiResult.Success(response.body())
|
||||
} else {
|
||||
ApiResult.Error("Failed to fetch documents", response.status.value)
|
||||
ApiResult.Error(ClientStrings.t("err.api.failed_fetch_documents"), response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
ApiResult.Error(e.message ?: ClientStrings.t("err.unknown_occurred"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,10 +55,10 @@ class DocumentApi(private val client: HttpClient = ApiClient.httpClient) {
|
||||
if (response.status.isSuccess()) {
|
||||
ApiResult.Success(response.body())
|
||||
} else {
|
||||
ApiResult.Error("Failed to fetch document", response.status.value)
|
||||
ApiResult.Error(ClientStrings.t("err.api.failed_fetch_document"), response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
ApiResult.Error(e.message ?: ClientStrings.t("err.unknown_occurred"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,12 +119,12 @@ class DocumentApi(private val client: HttpClient = ApiClient.httpClient) {
|
||||
// Send first file as "file" (backend only accepts single file)
|
||||
append("file", fileBytesList[0], Headers.build {
|
||||
append(HttpHeaders.ContentType, mimeTypesList.getOrElse(0) { "application/octet-stream" })
|
||||
append(HttpHeaders.ContentDisposition, "filename=\"${fileNamesList.getOrElse(0) { "file_0" }}\"")
|
||||
append(HttpHeaders.ContentDisposition, "filename=\"${fileNamesList.getOrElse(0) { "file_0" }}\"") // i18n-ignore: Content-Disposition filename, non-UI
|
||||
})
|
||||
} else if (fileBytes != null && fileName != null && mimeType != null) {
|
||||
append("file", fileBytes, Headers.build {
|
||||
append(HttpHeaders.ContentType, mimeType)
|
||||
append(HttpHeaders.ContentDisposition, "filename=\"$fileName\"")
|
||||
append(HttpHeaders.ContentDisposition, "filename=\"$fileName\"") // i18n-ignore: Content-Disposition filename, non-UI
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -155,12 +157,12 @@ class DocumentApi(private val client: HttpClient = ApiClient.httpClient) {
|
||||
val errorBody = try {
|
||||
response.body<String>()
|
||||
} catch (e: Exception) {
|
||||
"Failed to create document"
|
||||
ClientStrings.t("err.api.failed_create_document")
|
||||
}
|
||||
ApiResult.Error(errorBody, response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
ApiResult.Error(e.message ?: ClientStrings.t("err.unknown_occurred"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -212,12 +214,12 @@ class DocumentApi(private val client: HttpClient = ApiClient.httpClient) {
|
||||
val errorBody = try {
|
||||
response.body<String>()
|
||||
} catch (e: Exception) {
|
||||
"Failed to update document"
|
||||
ClientStrings.t("err.api.failed_update_document")
|
||||
}
|
||||
ApiResult.Error(errorBody, response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
ApiResult.Error(e.message ?: ClientStrings.t("err.unknown_occurred"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -230,10 +232,10 @@ class DocumentApi(private val client: HttpClient = ApiClient.httpClient) {
|
||||
if (response.status.isSuccess()) {
|
||||
ApiResult.Success(Unit)
|
||||
} else {
|
||||
ApiResult.Error("Failed to delete document", response.status.value)
|
||||
ApiResult.Error(ClientStrings.t("err.api.failed_delete_document"), response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
ApiResult.Error(e.message ?: ClientStrings.t("err.unknown_occurred"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -246,10 +248,10 @@ class DocumentApi(private val client: HttpClient = ApiClient.httpClient) {
|
||||
if (response.status.isSuccess()) {
|
||||
ApiResult.Success(response.body())
|
||||
} else {
|
||||
ApiResult.Error("Failed to download document", response.status.value)
|
||||
ApiResult.Error(ClientStrings.t("err.api.failed_download_document"), response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
ApiResult.Error(e.message ?: ClientStrings.t("err.unknown_occurred"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -264,10 +266,10 @@ class DocumentApi(private val client: HttpClient = ApiClient.httpClient) {
|
||||
val document: Document = response.body()
|
||||
ApiResult.Success(document)
|
||||
} else {
|
||||
ApiResult.Error("Failed to activate document", response.status.value)
|
||||
ApiResult.Error(ClientStrings.t("err.api.failed_activate_document"), response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
ApiResult.Error(e.message ?: ClientStrings.t("err.unknown_occurred"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -282,10 +284,10 @@ class DocumentApi(private val client: HttpClient = ApiClient.httpClient) {
|
||||
val document: Document = response.body()
|
||||
ApiResult.Success(document)
|
||||
} else {
|
||||
ApiResult.Error("Failed to deactivate document", response.status.value)
|
||||
ApiResult.Error(ClientStrings.t("err.api.failed_deactivate_document"), response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
ApiResult.Error(e.message ?: ClientStrings.t("err.unknown_occurred"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -303,7 +305,7 @@ class DocumentApi(private val client: HttpClient = ApiClient.httpClient) {
|
||||
formData = formData {
|
||||
append("image", imageBytes, Headers.build {
|
||||
append(HttpHeaders.ContentType, mimeType)
|
||||
append(HttpHeaders.ContentDisposition, "filename=\"$fileName\"")
|
||||
append(HttpHeaders.ContentDisposition, "filename=\"$fileName\"") // i18n-ignore: Content-Disposition filename, non-UI
|
||||
})
|
||||
caption?.let { append("caption", it) }
|
||||
}
|
||||
@@ -317,12 +319,12 @@ class DocumentApi(private val client: HttpClient = ApiClient.httpClient) {
|
||||
val errorBody = try {
|
||||
response.body<String>()
|
||||
} catch (e: Exception) {
|
||||
"Failed to upload document image"
|
||||
ClientStrings.t("err.api.failed_upload_document_image")
|
||||
}
|
||||
ApiResult.Error(errorBody, response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
ApiResult.Error(e.message ?: ClientStrings.t("err.unknown_occurred"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -335,10 +337,10 @@ class DocumentApi(private val client: HttpClient = ApiClient.httpClient) {
|
||||
if (response.status.isSuccess()) {
|
||||
ApiResult.Success(response.body())
|
||||
} else {
|
||||
ApiResult.Error("Failed to delete document image", response.status.value)
|
||||
ApiResult.Error(ClientStrings.t("err.api.failed_delete_document_image"), response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
ApiResult.Error(e.message ?: ClientStrings.t("err.unknown_occurred"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.tt.honeyDue.network
|
||||
|
||||
import com.tt.honeyDue.i18n.ClientStrings
|
||||
import com.tt.honeyDue.models.ErrorResponse
|
||||
import io.ktor.client.call.body
|
||||
import io.ktor.client.statement.HttpResponse
|
||||
@@ -22,9 +23,9 @@ object ErrorParser {
|
||||
if (response.status.value == 429) {
|
||||
val retryAfter = response.headers["Retry-After"]?.toLongOrNull()
|
||||
return if (retryAfter != null) {
|
||||
"Too many requests. Please try again in $retryAfter seconds."
|
||||
ClientStrings.t("err.too_many_requests_retry", retryAfter)
|
||||
} else {
|
||||
"Too many requests. Please try again later."
|
||||
ClientStrings.t("err.too_many_requests")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,9 +48,9 @@ object ErrorParser {
|
||||
// Add field-specific errors if present
|
||||
errorResponse.errors?.let { fieldErrors ->
|
||||
if (fieldErrors.isNotEmpty()) {
|
||||
message.append("\n\nDetails:")
|
||||
message.append("\n\n${ClientStrings.t("err.details")}") // i18n-ignore: interpolation residue; 'Details:' localized via ClientStrings
|
||||
fieldErrors.forEach { (field, errors) ->
|
||||
message.append("\n• $field: ${errors.joinToString(", ")}")
|
||||
message.append("\n• $field: ${errors.joinToString(", ")}") // i18n-ignore: interpolation residue; backend field name, non-UI prose
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -60,13 +61,13 @@ object ErrorParser {
|
||||
try {
|
||||
val simpleError = response.body<Map<String, String>>()
|
||||
simpleError["error"] ?: simpleError["message"] ?: simpleError["detail"]
|
||||
?: "An error occurred (${response.status.value})"
|
||||
?: ClientStrings.t("err.with_status", response.status.value)
|
||||
} catch (e2: Exception) {
|
||||
// Last resort: read as plain text
|
||||
try {
|
||||
response.body<String>()
|
||||
} catch (e3: Exception) {
|
||||
"An error occurred (${response.status.value})"
|
||||
ClientStrings.t("err.with_status", response.status.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package com.tt.honeyDue.network
|
||||
|
||||
import com.tt.honeyDue.i18n.ClientStrings
|
||||
|
||||
import com.tt.honeyDue.models.*
|
||||
import io.ktor.client.*
|
||||
import io.ktor.client.call.*
|
||||
@@ -42,10 +44,10 @@ class LookupsApi(private val client: HttpClient = ApiClient.httpClient) {
|
||||
if (response.status.isSuccess()) {
|
||||
ApiResult.Success(response.body())
|
||||
} else {
|
||||
ApiResult.Error("Failed to fetch residence types", response.status.value)
|
||||
ApiResult.Error(ClientStrings.t("err.api.failed_fetch_residence_types"), response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
ApiResult.Error(e.message ?: ClientStrings.t("err.unknown_occurred"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,10 +60,10 @@ class LookupsApi(private val client: HttpClient = ApiClient.httpClient) {
|
||||
if (response.status.isSuccess()) {
|
||||
ApiResult.Success(response.body())
|
||||
} else {
|
||||
ApiResult.Error("Failed to fetch task frequencies", response.status.value)
|
||||
ApiResult.Error(ClientStrings.t("err.api.failed_fetch_task_frequencies"), response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
ApiResult.Error(e.message ?: ClientStrings.t("err.unknown_occurred"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,10 +76,10 @@ class LookupsApi(private val client: HttpClient = ApiClient.httpClient) {
|
||||
if (response.status.isSuccess()) {
|
||||
ApiResult.Success(response.body())
|
||||
} else {
|
||||
ApiResult.Error("Failed to fetch task priorities", response.status.value)
|
||||
ApiResult.Error(ClientStrings.t("err.api.failed_fetch_task_priorities"), response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
ApiResult.Error(e.message ?: ClientStrings.t("err.unknown_occurred"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,10 +92,10 @@ class LookupsApi(private val client: HttpClient = ApiClient.httpClient) {
|
||||
if (response.status.isSuccess()) {
|
||||
ApiResult.Success(response.body())
|
||||
} else {
|
||||
ApiResult.Error("Failed to fetch task categories", response.status.value)
|
||||
ApiResult.Error(ClientStrings.t("err.api.failed_fetch_task_categories"), response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
ApiResult.Error(e.message ?: ClientStrings.t("err.unknown_occurred"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,10 +108,10 @@ class LookupsApi(private val client: HttpClient = ApiClient.httpClient) {
|
||||
if (response.status.isSuccess()) {
|
||||
ApiResult.Success(response.body())
|
||||
} else {
|
||||
ApiResult.Error("Failed to fetch contractor specialties", response.status.value)
|
||||
ApiResult.Error(ClientStrings.t("err.api.failed_fetch_contractor_specialties"), response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
ApiResult.Error(e.message ?: ClientStrings.t("err.unknown_occurred"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,10 +125,10 @@ class LookupsApi(private val client: HttpClient = ApiClient.httpClient) {
|
||||
if (response.status.isSuccess()) {
|
||||
ApiResult.Success(response.body())
|
||||
} else {
|
||||
ApiResult.Error("Failed to fetch static data", response.status.value)
|
||||
ApiResult.Error(ClientStrings.t("err.api.failed_fetch_static_data"), response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
ApiResult.Error(e.message ?: ClientStrings.t("err.unknown_occurred"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -158,18 +160,18 @@ class LookupsApi(private val client: HttpClient = ApiClient.httpClient) {
|
||||
response.status.isSuccess() -> {
|
||||
// Data has changed or first request - get new data and ETag
|
||||
val data: SeededDataResponse = response.body()
|
||||
val newETag = response.headers["ETag"]
|
||||
val newETag = response.headers["ETag"] // i18n-ignore: HTTP header name, non-UI
|
||||
ConditionalResult.Success(data, newETag)
|
||||
}
|
||||
else -> {
|
||||
ConditionalResult.Error(
|
||||
"Failed to fetch seeded data",
|
||||
ClientStrings.t("err.api.failed_fetch_seeded_data"),
|
||||
response.status.value
|
||||
)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ConditionalResult.Error(e.message ?: "Unknown error occurred")
|
||||
ConditionalResult.Error(e.message ?: ClientStrings.t("err.unknown_occurred"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package com.tt.honeyDue.network
|
||||
|
||||
import com.tt.honeyDue.i18n.ClientStrings
|
||||
|
||||
import com.tt.honeyDue.models.*
|
||||
import io.ktor.client.*
|
||||
import io.ktor.client.call.*
|
||||
@@ -29,12 +31,12 @@ class NotificationApi(private val client: HttpClient = ApiClient.httpClient) {
|
||||
val errorBody = try {
|
||||
response.body<Map<String, String>>()
|
||||
} catch (e: Exception) {
|
||||
mapOf("error" to "Device registration failed")
|
||||
mapOf("error" to ClientStrings.t("err.api.device_registration_failed"))
|
||||
}
|
||||
ApiResult.Error(errorBody["error"] ?: "Device registration failed", response.status.value)
|
||||
ApiResult.Error(errorBody["error"] ?: ClientStrings.t("err.api.device_registration_failed"), response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
ApiResult.Error(e.message ?: ClientStrings.t("err.unknown_occurred"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,10 +52,10 @@ class NotificationApi(private val client: HttpClient = ApiClient.httpClient) {
|
||||
if (response.status.isSuccess()) {
|
||||
ApiResult.Success(Unit)
|
||||
} else {
|
||||
ApiResult.Error("Device unregistration failed", response.status.value)
|
||||
ApiResult.Error(ClientStrings.t("err.api.device_unregistration_failed"), response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
ApiResult.Error(e.message ?: ClientStrings.t("err.unknown_occurred"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,10 +71,10 @@ class NotificationApi(private val client: HttpClient = ApiClient.httpClient) {
|
||||
if (response.status.isSuccess()) {
|
||||
ApiResult.Success(response.body())
|
||||
} else {
|
||||
ApiResult.Error("Failed to get preferences", response.status.value)
|
||||
ApiResult.Error(ClientStrings.t("err.api.failed_get_preferences"), response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
ApiResult.Error(e.message ?: ClientStrings.t("err.unknown_occurred"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,10 +95,10 @@ class NotificationApi(private val client: HttpClient = ApiClient.httpClient) {
|
||||
if (response.status.isSuccess()) {
|
||||
ApiResult.Success(response.body())
|
||||
} else {
|
||||
ApiResult.Error("Failed to update preferences", response.status.value)
|
||||
ApiResult.Error(ClientStrings.t("err.api.failed_update_preferences"), response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
ApiResult.Error(e.message ?: ClientStrings.t("err.unknown_occurred"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,10 +112,10 @@ class NotificationApi(private val client: HttpClient = ApiClient.httpClient) {
|
||||
val listResponse: NotificationListResponse = response.body()
|
||||
ApiResult.Success(listResponse.results)
|
||||
} else {
|
||||
ApiResult.Error("Failed to get notification history", response.status.value)
|
||||
ApiResult.Error(ClientStrings.t("err.api.failed_get_notification_history"), response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
ApiResult.Error(e.message ?: ClientStrings.t("err.unknown_occurred"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -129,10 +131,10 @@ class NotificationApi(private val client: HttpClient = ApiClient.httpClient) {
|
||||
if (response.status.isSuccess()) {
|
||||
ApiResult.Success(response.body())
|
||||
} else {
|
||||
ApiResult.Error("Failed to mark notification as read", response.status.value)
|
||||
ApiResult.Error(ClientStrings.t("err.api.failed_mark_notification_read"), response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
ApiResult.Error(e.message ?: ClientStrings.t("err.unknown_occurred"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,10 +147,10 @@ class NotificationApi(private val client: HttpClient = ApiClient.httpClient) {
|
||||
if (response.status.isSuccess()) {
|
||||
ApiResult.Success(response.body())
|
||||
} else {
|
||||
ApiResult.Error("Failed to mark all notifications as read", response.status.value)
|
||||
ApiResult.Error(ClientStrings.t("err.api.failed_mark_all_notifications_read"), response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
ApiResult.Error(e.message ?: ClientStrings.t("err.unknown_occurred"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -161,10 +163,10 @@ class NotificationApi(private val client: HttpClient = ApiClient.httpClient) {
|
||||
if (response.status.isSuccess()) {
|
||||
ApiResult.Success(response.body())
|
||||
} else {
|
||||
ApiResult.Error("Failed to get unread count", response.status.value)
|
||||
ApiResult.Error(ClientStrings.t("err.api.failed_get_unread_count"), response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
ApiResult.Error(e.message ?: ClientStrings.t("err.unknown_occurred"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package com.tt.honeyDue.network
|
||||
|
||||
import com.tt.honeyDue.i18n.ClientStrings
|
||||
|
||||
import com.tt.honeyDue.models.*
|
||||
import io.ktor.client.*
|
||||
import io.ktor.client.call.*
|
||||
@@ -18,10 +20,10 @@ class ResidenceApi(private val client: HttpClient = ApiClient.httpClient) {
|
||||
if (response.status.isSuccess()) {
|
||||
ApiResult.Success(response.body())
|
||||
} else {
|
||||
ApiResult.Error("Failed to fetch residences", response.status.value)
|
||||
ApiResult.Error(ClientStrings.t("err.api.failed_fetch_residences"), response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
ApiResult.Error(e.message ?: ClientStrings.t("err.unknown_occurred"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,10 +36,10 @@ class ResidenceApi(private val client: HttpClient = ApiClient.httpClient) {
|
||||
if (response.status.isSuccess()) {
|
||||
ApiResult.Success(response.body())
|
||||
} else {
|
||||
ApiResult.Error("Failed to fetch residence", response.status.value)
|
||||
ApiResult.Error(ClientStrings.t("err.api.failed_fetch_residence"), response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
ApiResult.Error(e.message ?: ClientStrings.t("err.unknown_occurred"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,10 +54,10 @@ class ResidenceApi(private val client: HttpClient = ApiClient.httpClient) {
|
||||
if (response.status.isSuccess()) {
|
||||
ApiResult.Success(response.body())
|
||||
} else {
|
||||
ApiResult.Error("Failed to create residence", response.status.value)
|
||||
ApiResult.Error(ClientStrings.t("err.api.failed_create_residence"), response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
ApiResult.Error(e.message ?: ClientStrings.t("err.unknown_occurred"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,10 +72,10 @@ class ResidenceApi(private val client: HttpClient = ApiClient.httpClient) {
|
||||
if (response.status.isSuccess()) {
|
||||
ApiResult.Success(response.body())
|
||||
} else {
|
||||
ApiResult.Error("Failed to update residence", response.status.value)
|
||||
ApiResult.Error(ClientStrings.t("err.api.failed_update_residence"), response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
ApiResult.Error(e.message ?: ClientStrings.t("err.unknown_occurred"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,10 +88,10 @@ class ResidenceApi(private val client: HttpClient = ApiClient.httpClient) {
|
||||
if (response.status.isSuccess()) {
|
||||
ApiResult.Success(response.body())
|
||||
} else {
|
||||
ApiResult.Error("Failed to delete residence", response.status.value)
|
||||
ApiResult.Error(ClientStrings.t("err.api.failed_delete_residence"), response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
ApiResult.Error(e.message ?: ClientStrings.t("err.unknown_occurred"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,10 +104,10 @@ class ResidenceApi(private val client: HttpClient = ApiClient.httpClient) {
|
||||
if (response.status.isSuccess()) {
|
||||
ApiResult.Success(response.body())
|
||||
} else {
|
||||
ApiResult.Error("Failed to fetch summary", response.status.value)
|
||||
ApiResult.Error(ClientStrings.t("err.api.failed_fetch_summary"), response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
ApiResult.Error(e.message ?: ClientStrings.t("err.unknown_occurred"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,10 +120,10 @@ class ResidenceApi(private val client: HttpClient = ApiClient.httpClient) {
|
||||
if (response.status.isSuccess()) {
|
||||
ApiResult.Success(response.body())
|
||||
} else {
|
||||
ApiResult.Error("Failed to fetch my residences", response.status.value)
|
||||
ApiResult.Error(ClientStrings.t("err.api.failed_fetch_my_residences"), response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
ApiResult.Error(e.message ?: ClientStrings.t("err.unknown_occurred"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,7 +141,7 @@ class ResidenceApi(private val client: HttpClient = ApiClient.httpClient) {
|
||||
ApiResult.Error(errorMessage, response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
ApiResult.Error(e.message ?: ClientStrings.t("err.unknown_occurred"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -156,7 +158,7 @@ class ResidenceApi(private val client: HttpClient = ApiClient.httpClient) {
|
||||
ApiResult.Error(errorMessage, response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
ApiResult.Error(e.message ?: ClientStrings.t("err.unknown_occurred"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -173,7 +175,7 @@ class ResidenceApi(private val client: HttpClient = ApiClient.httpClient) {
|
||||
ApiResult.Error(errorMessage, response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
ApiResult.Error(e.message ?: ClientStrings.t("err.unknown_occurred"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -196,7 +198,7 @@ class ResidenceApi(private val client: HttpClient = ApiClient.httpClient) {
|
||||
ApiResult.Error(errorMessage, response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
ApiResult.Error(e.message ?: ClientStrings.t("err.unknown_occurred"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -217,7 +219,7 @@ class ResidenceApi(private val client: HttpClient = ApiClient.httpClient) {
|
||||
ApiResult.Error(errorMessage, response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
ApiResult.Error(e.message ?: ClientStrings.t("err.unknown_occurred"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -236,7 +238,7 @@ class ResidenceApi(private val client: HttpClient = ApiClient.httpClient) {
|
||||
ApiResult.Error(errorMessage, response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
ApiResult.Error(e.message ?: ClientStrings.t("err.unknown_occurred"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -254,7 +256,7 @@ class ResidenceApi(private val client: HttpClient = ApiClient.httpClient) {
|
||||
ApiResult.Error(errorMessage, response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
ApiResult.Error(e.message ?: ClientStrings.t("err.unknown_occurred"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -271,7 +273,7 @@ class ResidenceApi(private val client: HttpClient = ApiClient.httpClient) {
|
||||
ApiResult.Error(errorMessage, response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
ApiResult.Error(e.message ?: ClientStrings.t("err.unknown_occurred"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -293,7 +295,7 @@ class ResidenceApi(private val client: HttpClient = ApiClient.httpClient) {
|
||||
ApiResult.Error(errorMessage, response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
ApiResult.Error(e.message ?: ClientStrings.t("err.unknown_occurred"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package com.tt.honeyDue.network
|
||||
|
||||
import com.tt.honeyDue.i18n.ClientStrings
|
||||
|
||||
import com.tt.honeyDue.models.*
|
||||
import io.ktor.client.*
|
||||
import io.ktor.client.call.*
|
||||
@@ -18,10 +20,10 @@ class SubscriptionApi(private val client: HttpClient = ApiClient.httpClient) {
|
||||
if (response.status.isSuccess()) {
|
||||
ApiResult.Success(response.body())
|
||||
} else {
|
||||
ApiResult.Error("Failed to fetch subscription status", response.status.value)
|
||||
ApiResult.Error(ClientStrings.t("err.api.failed_fetch_subscription_status"), response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
ApiResult.Error(e.message ?: ClientStrings.t("err.unknown_occurred"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,10 +37,10 @@ class SubscriptionApi(private val client: HttpClient = ApiClient.httpClient) {
|
||||
if (response.status.isSuccess()) {
|
||||
ApiResult.Success(response.body())
|
||||
} else {
|
||||
ApiResult.Error("Failed to fetch upgrade triggers", response.status.value)
|
||||
ApiResult.Error(ClientStrings.t("err.api.failed_fetch_upgrade_triggers"), response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
ApiResult.Error(e.message ?: ClientStrings.t("err.unknown_occurred"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,10 +53,10 @@ class SubscriptionApi(private val client: HttpClient = ApiClient.httpClient) {
|
||||
if (response.status.isSuccess()) {
|
||||
ApiResult.Success(response.body())
|
||||
} else {
|
||||
ApiResult.Error("Failed to fetch feature benefits", response.status.value)
|
||||
ApiResult.Error(ClientStrings.t("err.api.failed_fetch_feature_benefits"), response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
ApiResult.Error(e.message ?: ClientStrings.t("err.unknown_occurred"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,10 +69,10 @@ class SubscriptionApi(private val client: HttpClient = ApiClient.httpClient) {
|
||||
if (response.status.isSuccess()) {
|
||||
ApiResult.Success(response.body())
|
||||
} else {
|
||||
ApiResult.Error("Failed to fetch promotions", response.status.value)
|
||||
ApiResult.Error(ClientStrings.t("err.api.failed_fetch_promotions"), response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
ApiResult.Error(e.message ?: ClientStrings.t("err.unknown_occurred"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,10 +99,10 @@ class SubscriptionApi(private val client: HttpClient = ApiClient.httpClient) {
|
||||
if (response.status.isSuccess()) {
|
||||
ApiResult.Success(response.body())
|
||||
} else {
|
||||
ApiResult.Error("Failed to verify iOS receipt", response.status.value)
|
||||
ApiResult.Error(ClientStrings.t("err.api.failed_verify_ios_receipt"), response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
ApiResult.Error(e.message ?: ClientStrings.t("err.unknown_occurred"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,10 +129,10 @@ class SubscriptionApi(private val client: HttpClient = ApiClient.httpClient) {
|
||||
if (response.status.isSuccess()) {
|
||||
ApiResult.Success(response.body())
|
||||
} else {
|
||||
ApiResult.Error("Failed to verify Android purchase", response.status.value)
|
||||
ApiResult.Error(ClientStrings.t("err.api.failed_verify_android_purchase"), response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
ApiResult.Error(e.message ?: ClientStrings.t("err.unknown_occurred"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -162,10 +164,10 @@ class SubscriptionApi(private val client: HttpClient = ApiClient.httpClient) {
|
||||
if (response.status.isSuccess()) {
|
||||
ApiResult.Success(response.body())
|
||||
} else {
|
||||
ApiResult.Error("Failed to restore subscription", response.status.value)
|
||||
ApiResult.Error(ClientStrings.t("err.api.failed_restore_subscription"), response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
ApiResult.Error(e.message ?: ClientStrings.t("err.unknown_occurred"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package com.tt.honeyDue.network
|
||||
|
||||
import com.tt.honeyDue.i18n.ClientStrings
|
||||
|
||||
import com.tt.honeyDue.models.*
|
||||
import io.ktor.client.*
|
||||
import io.ktor.client.call.*
|
||||
@@ -26,7 +28,7 @@ class TaskApi(private val client: HttpClient = ApiClient.httpClient) {
|
||||
ApiResult.Error(errorMessage, response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
ApiResult.Error(e.message ?: ClientStrings.t("err.unknown_occurred"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,7 +45,7 @@ class TaskApi(private val client: HttpClient = ApiClient.httpClient) {
|
||||
ApiResult.Error(errorMessage, response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
ApiResult.Error(e.message ?: ClientStrings.t("err.unknown_occurred"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,7 +64,7 @@ class TaskApi(private val client: HttpClient = ApiClient.httpClient) {
|
||||
ApiResult.Error(errorMessage, response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
ApiResult.Error(e.message ?: ClientStrings.t("err.unknown_occurred"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,7 +89,7 @@ class TaskApi(private val client: HttpClient = ApiClient.httpClient) {
|
||||
ApiResult.Error(errorMessage, response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
ApiResult.Error(e.message ?: ClientStrings.t("err.unknown_occurred"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,7 +108,7 @@ class TaskApi(private val client: HttpClient = ApiClient.httpClient) {
|
||||
ApiResult.Error(errorMessage, response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
ApiResult.Error(e.message ?: ClientStrings.t("err.unknown_occurred"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,7 +125,7 @@ class TaskApi(private val client: HttpClient = ApiClient.httpClient) {
|
||||
ApiResult.Error(errorMessage, response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
ApiResult.Error(e.message ?: ClientStrings.t("err.unknown_occurred"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,7 +147,7 @@ class TaskApi(private val client: HttpClient = ApiClient.httpClient) {
|
||||
ApiResult.Error(errorMessage, response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
ApiResult.Error(e.message ?: ClientStrings.t("err.unknown_occurred"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -169,7 +171,7 @@ class TaskApi(private val client: HttpClient = ApiClient.httpClient) {
|
||||
ApiResult.Error(errorMessage, response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
ApiResult.Error(e.message ?: ClientStrings.t("err.unknown_occurred"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -214,16 +216,16 @@ class TaskApi(private val client: HttpClient = ApiClient.httpClient) {
|
||||
val data = response.body<WithSummaryResponse<TaskResponse>>()
|
||||
ApiResult.Success(data)
|
||||
}
|
||||
HttpStatusCode.NotFound -> ApiResult.Error("Task not found", 404)
|
||||
HttpStatusCode.Forbidden -> ApiResult.Error("Access denied", 403)
|
||||
HttpStatusCode.NotFound -> ApiResult.Error(ClientStrings.t("err.api.task_not_found"), 404)
|
||||
HttpStatusCode.Forbidden -> ApiResult.Error(ClientStrings.t("err.api.access_denied"), 403)
|
||||
HttpStatusCode.BadRequest -> {
|
||||
val errorBody = response.body<String>()
|
||||
ApiResult.Error(errorBody, 400)
|
||||
}
|
||||
else -> ApiResult.Error("Task $action failed: ${response.status}", response.status.value)
|
||||
else -> ApiResult.Error(ClientStrings.t("err.api.task_action_failed", action, response.status.toString()), response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
ApiResult.Error(e.message ?: ClientStrings.t("err.unknown_occurred"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -243,7 +245,7 @@ class TaskApi(private val client: HttpClient = ApiClient.httpClient) {
|
||||
ApiResult.Error(errorMessage, response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
ApiResult.Error(e.message ?: ClientStrings.t("err.unknown_occurred"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package com.tt.honeyDue.network
|
||||
|
||||
import com.tt.honeyDue.i18n.ClientStrings
|
||||
|
||||
import com.tt.honeyDue.models.*
|
||||
import io.ktor.client.*
|
||||
import io.ktor.client.call.*
|
||||
@@ -19,10 +21,10 @@ class TaskCompletionApi(private val client: HttpClient = ApiClient.httpClient) {
|
||||
if (response.status.isSuccess()) {
|
||||
ApiResult.Success(response.body())
|
||||
} else {
|
||||
ApiResult.Error("Failed to fetch completions", response.status.value)
|
||||
ApiResult.Error(ClientStrings.t("err.api.failed_fetch_completions"), response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
ApiResult.Error(e.message ?: ClientStrings.t("err.unknown_occurred"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,10 +37,10 @@ class TaskCompletionApi(private val client: HttpClient = ApiClient.httpClient) {
|
||||
if (response.status.isSuccess()) {
|
||||
ApiResult.Success(response.body())
|
||||
} else {
|
||||
ApiResult.Error("Failed to fetch completion", response.status.value)
|
||||
ApiResult.Error(ClientStrings.t("err.api.failed_fetch_completion"), response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
ApiResult.Error(e.message ?: ClientStrings.t("err.unknown_occurred"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,10 +55,10 @@ class TaskCompletionApi(private val client: HttpClient = ApiClient.httpClient) {
|
||||
if (response.status.isSuccess()) {
|
||||
ApiResult.Success(response.body())
|
||||
} else {
|
||||
ApiResult.Error("Failed to create completion", response.status.value)
|
||||
ApiResult.Error(ClientStrings.t("err.api.failed_create_completion"), response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
ApiResult.Error(e.message ?: ClientStrings.t("err.unknown_occurred"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,10 +73,10 @@ class TaskCompletionApi(private val client: HttpClient = ApiClient.httpClient) {
|
||||
if (response.status.isSuccess()) {
|
||||
ApiResult.Success(response.body())
|
||||
} else {
|
||||
ApiResult.Error("Failed to update completion", response.status.value)
|
||||
ApiResult.Error(ClientStrings.t("err.api.failed_update_completion"), response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
ApiResult.Error(e.message ?: ClientStrings.t("err.unknown_occurred"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,10 +89,10 @@ class TaskCompletionApi(private val client: HttpClient = ApiClient.httpClient) {
|
||||
if (response.status.isSuccess()) {
|
||||
ApiResult.Success(response.body())
|
||||
} else {
|
||||
ApiResult.Error("Failed to delete completion", response.status.value)
|
||||
ApiResult.Error(ClientStrings.t("err.api.failed_delete_completion"), response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
ApiResult.Error(e.message ?: ClientStrings.t("err.unknown_occurred"))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package com.tt.honeyDue.network
|
||||
|
||||
import com.tt.honeyDue.i18n.ClientStrings
|
||||
|
||||
import com.tt.honeyDue.models.TaskTemplate
|
||||
import com.tt.honeyDue.models.TaskSuggestionsResponse
|
||||
import com.tt.honeyDue.models.TaskTemplatesGroupedResponse
|
||||
@@ -25,10 +27,10 @@ class TaskTemplateApi(private val client: HttpClient = ApiClient.httpClient) {
|
||||
if (response.status.isSuccess()) {
|
||||
ApiResult.Success(response.body())
|
||||
} else {
|
||||
ApiResult.Error("Failed to fetch task templates", response.status.value)
|
||||
ApiResult.Error(ClientStrings.t("err.api.failed_fetch_task_templates"), response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
ApiResult.Error(e.message ?: ClientStrings.t("err.unknown_occurred"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,10 +44,10 @@ class TaskTemplateApi(private val client: HttpClient = ApiClient.httpClient) {
|
||||
if (response.status.isSuccess()) {
|
||||
ApiResult.Success(response.body())
|
||||
} else {
|
||||
ApiResult.Error("Failed to fetch grouped task templates", response.status.value)
|
||||
ApiResult.Error(ClientStrings.t("err.api.failed_fetch_grouped_task_templates"), response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
ApiResult.Error(e.message ?: ClientStrings.t("err.unknown_occurred"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,10 +63,10 @@ class TaskTemplateApi(private val client: HttpClient = ApiClient.httpClient) {
|
||||
if (response.status.isSuccess()) {
|
||||
ApiResult.Success(response.body())
|
||||
} else {
|
||||
ApiResult.Error("Failed to search task templates", response.status.value)
|
||||
ApiResult.Error(ClientStrings.t("err.api.failed_search_task_templates"), response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
ApiResult.Error(e.message ?: ClientStrings.t("err.unknown_occurred"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,10 +80,10 @@ class TaskTemplateApi(private val client: HttpClient = ApiClient.httpClient) {
|
||||
if (response.status.isSuccess()) {
|
||||
ApiResult.Success(response.body())
|
||||
} else {
|
||||
ApiResult.Error("Failed to fetch templates by category", response.status.value)
|
||||
ApiResult.Error(ClientStrings.t("err.api.failed_fetch_templates_by_category"), response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
ApiResult.Error(e.message ?: ClientStrings.t("err.unknown_occurred"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,10 +101,10 @@ class TaskTemplateApi(private val client: HttpClient = ApiClient.httpClient) {
|
||||
if (response.status.isSuccess()) {
|
||||
ApiResult.Success(response.body())
|
||||
} else {
|
||||
ApiResult.Error("Failed to fetch task suggestions", response.status.value)
|
||||
ApiResult.Error(ClientStrings.t("err.api.failed_fetch_task_suggestions"), response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
ApiResult.Error(e.message ?: ClientStrings.t("err.unknown_occurred"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,10 +118,10 @@ class TaskTemplateApi(private val client: HttpClient = ApiClient.httpClient) {
|
||||
if (response.status.isSuccess()) {
|
||||
ApiResult.Success(response.body())
|
||||
} else {
|
||||
ApiResult.Error("Template not found", response.status.value)
|
||||
ApiResult.Error(ClientStrings.t("err.api.template_not_found"), response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
ApiResult.Error(e.message ?: ClientStrings.t("err.unknown_occurred"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.tt.honeyDue.network
|
||||
|
||||
import com.tt.honeyDue.i18n.ClientStrings
|
||||
import com.tt.honeyDue.models.PresignUploadRequest
|
||||
import com.tt.honeyDue.models.PresignUploadResponse
|
||||
import io.ktor.client.*
|
||||
@@ -45,16 +46,16 @@ class UploadApi(private val client: HttpClient = ApiClient.httpClient) {
|
||||
} else {
|
||||
ApiResult.Error(
|
||||
when (response.status.value) {
|
||||
413 -> "That photo is too large after resizing."
|
||||
422 -> "That image format isn't supported."
|
||||
429 -> "Too many uploads in flight; try again shortly."
|
||||
else -> "Couldn't start upload (HTTP ${response.status.value})."
|
||||
413 -> ClientStrings.t("upload.too_large")
|
||||
422 -> ClientStrings.t("upload.unsupported")
|
||||
429 -> ClientStrings.t("upload.too_many")
|
||||
else -> ClientStrings.t("upload.start_failed")
|
||||
},
|
||||
response.status.value,
|
||||
)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Network error during presign")
|
||||
ApiResult.Error(e.message ?: ClientStrings.t("upload.net_presign"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,12 +99,12 @@ class UploadApi(private val client: HttpClient = ApiClient.httpClient) {
|
||||
""
|
||||
}
|
||||
ApiResult.Error(
|
||||
"Upload to storage failed (HTTP ${response.status.value}): $body",
|
||||
ClientStrings.t("upload.storage_failed"),
|
||||
response.status.value,
|
||||
)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Network error during upload")
|
||||
ApiResult.Error(e.message ?: ClientStrings.t("upload.net_upload"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,7 +126,7 @@ class UploadApi(private val client: HttpClient = ApiClient.httpClient) {
|
||||
val presignResult = presign(token, category, contentType, data.size.toLong())
|
||||
val presigned = (presignResult as? ApiResult.Success)?.data
|
||||
?: return ApiResult.Error(
|
||||
(presignResult as? ApiResult.Error)?.message ?: "Presign failed",
|
||||
(presignResult as? ApiResult.Error)?.message ?: ClientStrings.t("upload.failed"),
|
||||
(presignResult as? ApiResult.Error)?.code,
|
||||
)
|
||||
|
||||
@@ -138,7 +139,7 @@ class UploadApi(private val client: HttpClient = ApiClient.httpClient) {
|
||||
return when (putResult) {
|
||||
is ApiResult.Success -> ApiResult.Success(presigned.id)
|
||||
is ApiResult.Error -> putResult
|
||||
else -> ApiResult.Error("Upload failed in unknown state")
|
||||
else -> ApiResult.Error(ClientStrings.t("upload.failed"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -136,7 +136,7 @@ object TaskAnimations {
|
||||
* iOS: `.spring(response: 0.4, dampingFraction: 0.6)`
|
||||
*/
|
||||
val completionCheckmark = AnimationSpecValues(
|
||||
name = "completionCheckmark",
|
||||
name = "completionCheckmark", // i18n-ignore: animation transition label, non-UI
|
||||
durationMillis = 400,
|
||||
easing = Easing.SPRING,
|
||||
springResponseMillis = 400,
|
||||
@@ -150,7 +150,7 @@ object TaskAnimations {
|
||||
* exit starts).
|
||||
*/
|
||||
val cardEnter = AnimationSpecValues(
|
||||
name = "cardEnter",
|
||||
name = "cardEnter", // i18n-ignore: animation transition label, non-UI
|
||||
durationMillis = 350,
|
||||
easing = Easing.SPRING,
|
||||
springResponseMillis = 350,
|
||||
@@ -162,7 +162,7 @@ object TaskAnimations {
|
||||
* iOS: `.easeIn(duration: 0.3)` on the card shrink+fade.
|
||||
*/
|
||||
val cardDismiss = AnimationSpecValues(
|
||||
name = "cardDismiss",
|
||||
name = "cardDismiss", // i18n-ignore: animation transition label, non-UI
|
||||
durationMillis = 300,
|
||||
easing = Easing.EASE_IN,
|
||||
)
|
||||
@@ -174,7 +174,7 @@ object TaskAnimations {
|
||||
* pulse so the motion reads as "breathing" at ~50bpm.
|
||||
*/
|
||||
val priorityPulse = LoopSpecValues(
|
||||
name = "priorityPulse",
|
||||
name = "priorityPulse", // i18n-ignore: animation transition label, non-UI
|
||||
periodMillis = 1200,
|
||||
easing = Easing.EASE_IN_OUT,
|
||||
reverses = true,
|
||||
@@ -186,7 +186,7 @@ object TaskAnimations {
|
||||
* cycle; we match that for the loading shimmer.
|
||||
*/
|
||||
val honeycombLoop = LoopSpecValues(
|
||||
name = "honeycombLoop",
|
||||
name = "honeycombLoop", // i18n-ignore: animation transition label, non-UI
|
||||
periodMillis = 8000,
|
||||
easing = Easing.LINEAR,
|
||||
reverses = false,
|
||||
|
||||
@@ -294,7 +294,7 @@ fun AddTaskDialog(
|
||||
frequency = freq
|
||||
showFrequencyDropdown = false
|
||||
// Clear interval days if frequency is not "Custom"
|
||||
if (!freq.name.equals("Custom", ignoreCase = true)) {
|
||||
if (!freq.name.equals("Custom", ignoreCase = true)) { // i18n-ignore: API enum value comparison, non-UI
|
||||
intervalDays = ""
|
||||
}
|
||||
}
|
||||
@@ -304,7 +304,7 @@ fun AddTaskDialog(
|
||||
}
|
||||
|
||||
// Custom Interval Days (only for "Custom" frequency)
|
||||
if (frequency.name.equals("Custom", ignoreCase = true)) {
|
||||
if (frequency.name.equals("Custom", ignoreCase = true)) { // i18n-ignore: API enum value comparison, non-UI
|
||||
OutlinedTextField(
|
||||
value = intervalDays,
|
||||
onValueChange = { intervalDays = it.filter { char -> char.isDigit() } },
|
||||
@@ -425,7 +425,7 @@ fun AddTaskDialog(
|
||||
description = description.ifBlank { null },
|
||||
categoryId = if (category.id > 0) category.id else null,
|
||||
frequencyId = if (frequency.id > 0) frequency.id else null,
|
||||
customIntervalDays = if (frequency.name.equals("Custom", ignoreCase = true) && intervalDays.isNotBlank()) {
|
||||
customIntervalDays = if (frequency.name.equals("Custom", ignoreCase = true) && intervalDays.isNotBlank()) { // i18n-ignore: API enum value comparison, non-UI
|
||||
intervalDays.toIntOrNull()
|
||||
} else null,
|
||||
priorityId = if (priority.id > 0) priority.id else null,
|
||||
|
||||
@@ -7,6 +7,9 @@ import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.tt.honeyDue.network.ApiResult
|
||||
import honeydue.composeapp.generated.resources.Res
|
||||
import honeydue.composeapp.generated.resources.error_network_title
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
|
||||
/**
|
||||
* Handles ApiResult states automatically with loading, error dialogs, and success content.
|
||||
@@ -38,9 +41,10 @@ fun <T> ApiResultHandler(
|
||||
onRetry: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
loadingContent: @Composable (() -> Unit)? = null,
|
||||
errorTitle: String = "Network Error",
|
||||
errorTitle: String? = null,
|
||||
content: @Composable (T) -> Unit
|
||||
) {
|
||||
val resolvedErrorTitle = errorTitle ?: stringResource(Res.string.error_network_title)
|
||||
var showErrorDialog by remember { mutableStateOf(false) }
|
||||
var errorMessage by remember { mutableStateOf("") }
|
||||
|
||||
@@ -84,7 +88,7 @@ fun <T> ApiResultHandler(
|
||||
// Error dialog
|
||||
if (showErrorDialog) {
|
||||
ErrorDialog(
|
||||
title = errorTitle,
|
||||
title = resolvedErrorTitle,
|
||||
message = errorMessage,
|
||||
onRetry = {
|
||||
showErrorDialog = false
|
||||
@@ -120,8 +124,9 @@ fun <T> ApiResultHandler(
|
||||
@Composable
|
||||
fun <T> ApiResult<T>.HandleErrors(
|
||||
onRetry: () -> Unit,
|
||||
errorTitle: String = "Network Error"
|
||||
errorTitle: String? = null
|
||||
) {
|
||||
val resolvedErrorTitle = errorTitle ?: stringResource(Res.string.error_network_title)
|
||||
var showErrorDialog by remember { mutableStateOf(false) }
|
||||
var errorMessage by remember { mutableStateOf("") }
|
||||
|
||||
@@ -134,7 +139,7 @@ fun <T> ApiResult<T>.HandleErrors(
|
||||
|
||||
if (showErrorDialog) {
|
||||
ErrorDialog(
|
||||
title = errorTitle,
|
||||
title = resolvedErrorTitle,
|
||||
message = errorMessage,
|
||||
onRetry = {
|
||||
showErrorDialog = false
|
||||
|
||||
+4
-2
@@ -19,6 +19,8 @@ import coil3.network.httpHeaders
|
||||
import com.tt.honeyDue.network.ApiClient
|
||||
import com.tt.honeyDue.network.SESSION_TOKEN_HEADER
|
||||
import com.tt.honeyDue.storage.TokenStorage
|
||||
import honeydue.composeapp.generated.resources.*
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
|
||||
/**
|
||||
* A Compose component that loads images from authenticated API endpoints.
|
||||
@@ -114,13 +116,13 @@ private fun DefaultErrorContent() {
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.BrokenImage,
|
||||
contentDescription = "Failed to load",
|
||||
contentDescription = stringResource(Res.string.image_failed_to_load),
|
||||
modifier = Modifier.size(40.dp),
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Text(
|
||||
"Failed to load",
|
||||
stringResource(Res.string.image_failed_to_load),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
|
||||
+6
-4
@@ -88,6 +88,8 @@ fun CompleteTaskDialog(
|
||||
val noneManualEntry = stringResource(Res.string.completions_none_manual)
|
||||
val cancelText = stringResource(Res.string.common_cancel)
|
||||
val removeImageDesc = stringResource(Res.string.completions_remove_image)
|
||||
val contractorPrefix = stringResource(Res.string.completions_contractor_prefix)
|
||||
val completedByPrefix = stringResource(Res.string.completions_completed_by_prefix)
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
@@ -239,7 +241,7 @@ fun CompleteTaskDialog(
|
||||
val starColor by animateColorAsState(
|
||||
targetValue = if (isSelected) Color(0xFFFFD700) else MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.3f),
|
||||
animationSpec = tween(durationMillis = 150),
|
||||
label = "starColor"
|
||||
label = "starColor" // i18n-ignore: animation label, non-UI
|
||||
)
|
||||
|
||||
IconButton(
|
||||
@@ -251,7 +253,7 @@ fun CompleteTaskDialog(
|
||||
) {
|
||||
Icon(
|
||||
imageVector = if (isSelected) Icons.Default.Star else Icons.Default.StarOutline,
|
||||
contentDescription = "$star stars",
|
||||
contentDescription = stringResource(Res.string.completions_star_rating_cd, star),
|
||||
tint = starColor,
|
||||
modifier = Modifier.size(32.dp)
|
||||
)
|
||||
@@ -360,10 +362,10 @@ fun CompleteTaskDialog(
|
||||
// Build notes with contractor info if selected
|
||||
val notesWithContractor = buildString {
|
||||
if (selectedContractorName != null) {
|
||||
append("Contractor: $selectedContractorName\n")
|
||||
append("$contractorPrefix $selectedContractorName\n") // i18n-ignore: localized prefix var + data concatenation, no literal prose
|
||||
}
|
||||
if (completedByName.isNotBlank()) {
|
||||
append("Completed by: $completedByName\n")
|
||||
append("$completedByPrefix $completedByName\n") // i18n-ignore: localized prefix var + data concatenation, no literal prose
|
||||
}
|
||||
if (notes.isNotBlank()) {
|
||||
append(notes)
|
||||
|
||||
+2
-2
@@ -156,7 +156,7 @@ fun ContractorImportSuccessDialog(
|
||||
icon = {
|
||||
Icon(
|
||||
imageVector = Icons.Default.CheckCircle,
|
||||
contentDescription = "Success",
|
||||
contentDescription = stringResource(Res.string.common_success),
|
||||
modifier = Modifier.size(48.dp),
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
@@ -202,7 +202,7 @@ fun ContractorImportErrorDialog(
|
||||
icon = {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Error,
|
||||
contentDescription = "Error",
|
||||
contentDescription = stringResource(Res.string.common_error),
|
||||
modifier = Modifier.size(48.dp),
|
||||
tint = MaterialTheme.colorScheme.error
|
||||
)
|
||||
|
||||
@@ -10,6 +10,11 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import com.tt.honeyDue.ui.haptics.Haptics
|
||||
import honeydue.composeapp.generated.resources.Res
|
||||
import honeydue.composeapp.generated.resources.error_network_title
|
||||
import honeydue.composeapp.generated.resources.common_try_again
|
||||
import honeydue.composeapp.generated.resources.common_cancel
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
|
||||
/**
|
||||
* Reusable error dialog component that shows network errors with retry/cancel options
|
||||
@@ -23,12 +28,12 @@ import com.tt.honeyDue.ui.haptics.Haptics
|
||||
*/
|
||||
@Composable
|
||||
fun ErrorDialog(
|
||||
title: String = "Network Error",
|
||||
title: String = stringResource(Res.string.error_network_title),
|
||||
message: String,
|
||||
onRetry: () -> Unit,
|
||||
onDismiss: () -> Unit,
|
||||
retryButtonText: String = "Try Again",
|
||||
dismissButtonText: String = "Cancel"
|
||||
retryButtonText: String = stringResource(Res.string.common_try_again),
|
||||
dismissButtonText: String = stringResource(Res.string.common_cancel)
|
||||
) {
|
||||
// P5 Stream S — error haptic when the dialog appears
|
||||
LaunchedEffect(message) { Haptics.error() }
|
||||
|
||||
+20
-18
@@ -25,6 +25,8 @@ import com.tt.honeyDue.models.ResidenceShareCode
|
||||
import com.tt.honeyDue.network.ApiResult
|
||||
import com.tt.honeyDue.network.APILayer
|
||||
import com.tt.honeyDue.ui.theme.AppSpacing
|
||||
import honeydue.composeapp.generated.resources.*
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
@@ -83,10 +85,10 @@ fun ManageUsersDialog(
|
||||
modifier = Modifier.size(28.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(AppSpacing.sm))
|
||||
Text("Invite Others")
|
||||
Text(stringResource(Res.string.manage_users_invite_title))
|
||||
}
|
||||
IconButton(onClick = onDismiss) {
|
||||
Icon(Icons.Default.Close, "Close")
|
||||
Icon(Icons.Default.Close, stringResource(Res.string.a11y_close))
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -101,7 +103,7 @@ fun ManageUsersDialog(
|
||||
}
|
||||
} else if (error != null) {
|
||||
Text(
|
||||
text = error ?: "Unknown error",
|
||||
text = error ?: stringResource(Res.string.error_generic),
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
modifier = Modifier.padding(AppSpacing.lg)
|
||||
)
|
||||
@@ -117,7 +119,7 @@ fun ManageUsersDialog(
|
||||
) {
|
||||
Column(modifier = Modifier.padding(AppSpacing.lg)) {
|
||||
Text(
|
||||
text = "Easy Share",
|
||||
text = stringResource(Res.string.manage_users_easy_share),
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
@@ -127,13 +129,13 @@ fun ManageUsersDialog(
|
||||
onClick = { onSharePackage() },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Icon(Icons.Default.Share, "Share", modifier = Modifier.size(18.dp))
|
||||
Icon(Icons.Default.Share, stringResource(Res.string.common_share), modifier = Modifier.size(18.dp))
|
||||
Spacer(modifier = Modifier.width(AppSpacing.sm))
|
||||
Text("Send Invite Link")
|
||||
Text(stringResource(Res.string.manage_users_send_invite))
|
||||
}
|
||||
|
||||
Text(
|
||||
text = "Send a .honeydue file via Messages, Email, or share. They just tap to join.",
|
||||
text = stringResource(Res.string.manage_users_easy_share_desc),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(top = AppSpacing.sm)
|
||||
@@ -148,7 +150,7 @@ fun ManageUsersDialog(
|
||||
) {
|
||||
HorizontalDivider(modifier = Modifier.weight(1f))
|
||||
Text(
|
||||
text = "or",
|
||||
text = stringResource(Res.string.manage_users_or),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(horizontal = AppSpacing.lg)
|
||||
@@ -165,7 +167,7 @@ fun ManageUsersDialog(
|
||||
) {
|
||||
Column(modifier = Modifier.padding(AppSpacing.lg)) {
|
||||
Text(
|
||||
text = "Share Code",
|
||||
text = stringResource(Res.string.manage_users_share_code),
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
@@ -192,13 +194,13 @@ fun ManageUsersDialog(
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.ContentCopy,
|
||||
contentDescription = "Copy code",
|
||||
contentDescription = stringResource(Res.string.manage_users_copy_code),
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
} else {
|
||||
Text(
|
||||
text = "No active code",
|
||||
text = stringResource(Res.string.manage_users_no_code),
|
||||
style = MaterialTheme.typography.bodyMedium.copy(
|
||||
fontStyle = FontStyle.Italic
|
||||
),
|
||||
@@ -235,15 +237,15 @@ fun ManageUsersDialog(
|
||||
strokeWidth = 2.dp
|
||||
)
|
||||
} else {
|
||||
Icon(Icons.Default.Refresh, "Generate", modifier = Modifier.size(18.dp))
|
||||
Icon(Icons.Default.Refresh, stringResource(Res.string.manage_users_generate), modifier = Modifier.size(18.dp))
|
||||
}
|
||||
Spacer(modifier = Modifier.width(AppSpacing.sm))
|
||||
Text(if (shareCode != null) "Generate New Code" else "Generate Code")
|
||||
Text(if (shareCode != null) stringResource(Res.string.manage_users_generate_new) else stringResource(Res.string.manage_users_generate))
|
||||
}
|
||||
|
||||
if (shareCode != null) {
|
||||
Text(
|
||||
text = "Share this 6-character code. They can enter it in the app to join.",
|
||||
text = stringResource(Res.string.manage_users_code_desc),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(top = AppSpacing.sm)
|
||||
@@ -255,7 +257,7 @@ fun ManageUsersDialog(
|
||||
|
||||
// Users list
|
||||
Text(
|
||||
text = "Users (${users.size})",
|
||||
text = stringResource(Res.string.manage_users_users_count, users.size),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
modifier = Modifier.padding(bottom = AppSpacing.sm)
|
||||
)
|
||||
@@ -290,7 +292,7 @@ fun ManageUsersDialog(
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(onClick = onDismiss) {
|
||||
Text("Close")
|
||||
Text(stringResource(Res.string.common_close))
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -324,7 +326,7 @@ private fun UserListItem(
|
||||
shape = MaterialTheme.shapes.small
|
||||
) {
|
||||
Text(
|
||||
text = "Owner",
|
||||
text = stringResource(Res.string.manage_users_owner_badge),
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.onPrimaryContainer,
|
||||
modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp)
|
||||
@@ -355,7 +357,7 @@ private fun UserListItem(
|
||||
IconButton(onClick = onRemove) {
|
||||
Icon(
|
||||
Icons.Default.Delete,
|
||||
contentDescription = "Remove user",
|
||||
contentDescription = stringResource(Res.string.manage_users_remove_user),
|
||||
tint = MaterialTheme.colorScheme.error
|
||||
)
|
||||
}
|
||||
|
||||
@@ -27,6 +27,8 @@ import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import coil3.compose.AsyncImage
|
||||
import com.tt.honeyDue.ui.theme.AppSpacing
|
||||
import honeydue.composeapp.generated.resources.*
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
|
||||
/**
|
||||
* Full-screen photo viewer with swipeable gallery.
|
||||
@@ -87,7 +89,7 @@ fun PhotoViewerScreen(
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Close,
|
||||
contentDescription = "Close",
|
||||
contentDescription = stringResource(Res.string.common_close),
|
||||
tint = Color.White
|
||||
)
|
||||
}
|
||||
@@ -122,11 +124,11 @@ fun PhotoViewerScreen(
|
||||
val isSelected = pagerState.currentPage == index
|
||||
val size by animateFloatAsState(
|
||||
targetValue = if (isSelected) 10f else 6f,
|
||||
label = "dotSize"
|
||||
label = "dotSize" // i18n-ignore: animation label, non-UI
|
||||
)
|
||||
val alpha by animateFloatAsState(
|
||||
targetValue = if (isSelected) 1f else 0.5f,
|
||||
label = "dotAlpha"
|
||||
label = "dotAlpha" // i18n-ignore: animation label, non-UI
|
||||
)
|
||||
|
||||
Box(
|
||||
@@ -187,7 +189,7 @@ private fun ZoomableImage(
|
||||
) {
|
||||
AsyncImage(
|
||||
model = imageUrl,
|
||||
contentDescription = "Photo",
|
||||
contentDescription = stringResource(Res.string.common_photo),
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.graphicsLayer(
|
||||
|
||||
+2
-2
@@ -145,7 +145,7 @@ fun ResidenceImportSuccessDialog(
|
||||
icon = {
|
||||
Icon(
|
||||
imageVector = Icons.Default.CheckCircle,
|
||||
contentDescription = "Success",
|
||||
contentDescription = stringResource(Res.string.common_success),
|
||||
modifier = Modifier.size(48.dp),
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
@@ -191,7 +191,7 @@ fun ResidenceImportErrorDialog(
|
||||
icon = {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Error,
|
||||
contentDescription = "Error",
|
||||
contentDescription = stringResource(Res.string.common_error),
|
||||
modifier = Modifier.size(48.dp),
|
||||
tint = MaterialTheme.colorScheme.error
|
||||
)
|
||||
|
||||
+1
-1
@@ -146,7 +146,7 @@ internal fun getCategoryColor(category: String): androidx.compose.ui.graphics.Co
|
||||
"safety", "electrical" -> MaterialTheme.colorScheme.error
|
||||
"hvac" -> MaterialTheme.colorScheme.primary
|
||||
"appliances" -> MaterialTheme.colorScheme.tertiary
|
||||
"exterior", "lawn & garden" -> androidx.compose.ui.graphics.Color(0xFF34C759)
|
||||
"exterior", "lawn & garden" -> androidx.compose.ui.graphics.Color(0xFF34C759) // i18n-ignore: category key color lookup, non-UI
|
||||
"interior" -> androidx.compose.ui.graphics.Color(0xFFAF52DE)
|
||||
"general", "seasonal" -> androidx.compose.ui.graphics.Color(0xFFFF9500)
|
||||
else -> MaterialTheme.colorScheme.primary
|
||||
|
||||
@@ -54,7 +54,7 @@ fun AuthHeaderPreview() {
|
||||
AuthHeader(
|
||||
icon = Icons.Default.Home,
|
||||
title = "honeyDue",
|
||||
subtitle = "Manage your properties with ease",
|
||||
subtitle = "Manage your properties with ease", // i18n-ignore: Compose @Preview sample data, not shipped UI
|
||||
modifier = Modifier.padding(32.dp)
|
||||
)
|
||||
}
|
||||
|
||||
+5
-1
@@ -14,6 +14,10 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import honeydue.composeapp.generated.resources.Res
|
||||
import honeydue.composeapp.generated.resources.auth_requirement_met
|
||||
import honeydue.composeapp.generated.resources.auth_requirement_not_met
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
|
||||
@Composable
|
||||
fun RequirementItem(text: String, satisfied: Boolean) {
|
||||
@@ -22,7 +26,7 @@ fun RequirementItem(text: String, satisfied: Boolean) {
|
||||
) {
|
||||
Icon(
|
||||
if (satisfied) Icons.Default.CheckCircle else Icons.Default.Circle,
|
||||
contentDescription = if (satisfied) "Requirement met" else "Requirement not met",
|
||||
contentDescription = if (satisfied) stringResource(Res.string.auth_requirement_met) else stringResource(Res.string.auth_requirement_not_met),
|
||||
tint = if (satisfied) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.size(16.dp)
|
||||
)
|
||||
|
||||
@@ -49,7 +49,7 @@ fun ErrorCard(
|
||||
fun ErrorCardPreview() {
|
||||
MaterialTheme {
|
||||
ErrorCard(
|
||||
message = "Invalid username or password. Please try again.",
|
||||
message = "Invalid username or password. Please try again.", // i18n-ignore: Compose @Preview sample data, not shipped UI
|
||||
modifier = Modifier.padding(16.dp)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -59,11 +59,11 @@ fun InfoCardPreview() {
|
||||
MaterialTheme {
|
||||
InfoCard(
|
||||
icon = Icons.Default.Info,
|
||||
title = "Sample Information"
|
||||
title = "Sample Information" // i18n-ignore: Compose @Preview sample data, not shipped UI
|
||||
) {
|
||||
Text("This is sample content")
|
||||
Text("Line 2 of content")
|
||||
Text("Line 3 of content")
|
||||
Text("This is sample content") // i18n-ignore: Compose @Preview sample data, not shipped UI
|
||||
Text("Line 2 of content") // i18n-ignore: Compose @Preview sample data, not shipped UI
|
||||
Text("Line 3 of content") // i18n-ignore: Compose @Preview sample data, not shipped UI
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+7
-5
@@ -11,6 +11,8 @@ import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tt.honeyDue.ui.theme.AppSpacing
|
||||
import honeydue.composeapp.generated.resources.*
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
|
||||
/**
|
||||
* StandardErrorState - Inline error state with retry button
|
||||
@@ -34,9 +36,9 @@ fun StandardErrorState(
|
||||
message: String,
|
||||
onRetry: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
title: String = "Something went wrong",
|
||||
title: String = stringResource(Res.string.error_something_wrong),
|
||||
icon: ImageVector = Icons.Default.ErrorOutline,
|
||||
retryLabel: String = "Retry"
|
||||
retryLabel: String = stringResource(Res.string.common_retry)
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
@@ -47,7 +49,7 @@ fun StandardErrorState(
|
||||
) {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = "Error",
|
||||
contentDescription = stringResource(Res.string.common_error),
|
||||
modifier = Modifier.size(48.dp),
|
||||
tint = MaterialTheme.colorScheme.error
|
||||
)
|
||||
@@ -79,7 +81,7 @@ fun CompactErrorState(
|
||||
message: String,
|
||||
onRetry: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
retryLabel: String = "Retry"
|
||||
retryLabel: String = stringResource(Res.string.common_retry)
|
||||
) {
|
||||
Card(
|
||||
colors = CardDefaults.cardColors(
|
||||
@@ -99,7 +101,7 @@ fun CompactErrorState(
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.ErrorOutline,
|
||||
contentDescription = "Error",
|
||||
contentDescription = stringResource(Res.string.common_error),
|
||||
tint = MaterialTheme.colorScheme.onErrorContainer
|
||||
)
|
||||
Text(
|
||||
|
||||
@@ -56,7 +56,7 @@ fun StatItemPreview() {
|
||||
StatItem(
|
||||
icon = Icons.Default.Home,
|
||||
value = "5",
|
||||
label = "Properties"
|
||||
label = "Properties" // i18n-ignore: Compose @Preview sample data, not shipped UI
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -61,7 +61,7 @@ fun DeleteAccountDialog(
|
||||
// Warning Icon
|
||||
Icon(
|
||||
imageVector = Icons.Default.Warning,
|
||||
contentDescription = "Warning",
|
||||
contentDescription = stringResource(Res.string.common_warning),
|
||||
tint = MaterialTheme.colorScheme.error,
|
||||
modifier = Modifier.size(48.dp)
|
||||
)
|
||||
|
||||
+11
-9
@@ -21,6 +21,8 @@ import com.tt.honeyDue.models.DocumentType
|
||||
import com.tt.honeyDue.testing.AccessibilityIds
|
||||
import com.tt.honeyDue.ui.theme.AppRadius
|
||||
import com.tt.honeyDue.ui.theme.AppSpacing
|
||||
import honeydue.composeapp.generated.resources.*
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
|
||||
@Composable
|
||||
fun DocumentCard(document: Document, isWarrantyCard: Boolean = false, onClick: () -> Unit) {
|
||||
@@ -83,10 +85,10 @@ private fun WarrantyCardContent(document: Document, onClick: () -> Unit) {
|
||||
) {
|
||||
Text(
|
||||
when {
|
||||
!document.isActive -> "Inactive"
|
||||
daysUntilExpiration < 0 -> "Expired"
|
||||
daysUntilExpiration < 30 -> "Expiring soon"
|
||||
else -> "Active"
|
||||
!document.isActive -> stringResource(Res.string.documents_inactive)
|
||||
daysUntilExpiration < 0 -> stringResource(Res.string.documents_expired)
|
||||
daysUntilExpiration < 30 -> stringResource(Res.string.documents_expiring_soon)
|
||||
else -> stringResource(Res.string.documents_active)
|
||||
},
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = statusColor,
|
||||
@@ -102,19 +104,19 @@ private fun WarrantyCardContent(document: Document, onClick: () -> Unit) {
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Column {
|
||||
Text("Provider", style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||
Text(document.provider ?: "N/A", style = MaterialTheme.typography.bodyMedium, fontWeight = FontWeight.Medium)
|
||||
Text(stringResource(Res.string.documents_provider), style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||
Text(document.provider ?: stringResource(Res.string.tasks_card_not_available), style = MaterialTheme.typography.bodyMedium, fontWeight = FontWeight.Medium)
|
||||
}
|
||||
Column(horizontalAlignment = Alignment.End) {
|
||||
Text("Expires", style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||
Text(document.endDate ?: "N/A", style = MaterialTheme.typography.bodyMedium, fontWeight = FontWeight.Medium)
|
||||
Text(stringResource(Res.string.documents_expires_label), style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||
Text(document.endDate ?: stringResource(Res.string.tasks_card_not_available), style = MaterialTheme.typography.bodyMedium, fontWeight = FontWeight.Medium)
|
||||
}
|
||||
}
|
||||
|
||||
if (document.isActive && daysUntilExpiration >= 0) {
|
||||
Spacer(modifier = Modifier.height(AppSpacing.sm))
|
||||
Text(
|
||||
"$daysUntilExpiration days remaining",
|
||||
stringResource(Res.string.documents_days_remaining_count, daysUntilExpiration),
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = statusColor
|
||||
)
|
||||
|
||||
+4
-2
@@ -10,6 +10,8 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.unit.dp
|
||||
import honeydue.composeapp.generated.resources.*
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
|
||||
@Composable
|
||||
fun EmptyState(
|
||||
@@ -35,12 +37,12 @@ fun ErrorState(message: String, onRetry: () -> Unit) {
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
Icon(Icons.Default.Error, contentDescription = "Error", modifier = Modifier.size(64.dp) , tint = MaterialTheme.colorScheme.error)
|
||||
Icon(Icons.Default.Error, contentDescription = stringResource(Res.string.common_error), modifier = Modifier.size(64.dp) , tint = MaterialTheme.colorScheme.error)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Text(message, style = MaterialTheme.typography.bodyLarge, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
OutlinedButton(onClick = onRetry) {
|
||||
Text("Retry")
|
||||
Text(stringResource(Res.string.common_retry))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+5
-1
@@ -19,6 +19,10 @@ import com.tt.honeyDue.network.ApiResult
|
||||
import com.tt.honeyDue.testing.AccessibilityIds
|
||||
import com.tt.honeyDue.ui.subscription.UpgradeFeatureScreen
|
||||
import com.tt.honeyDue.utils.SubscriptionHelper
|
||||
import honeydue.composeapp.generated.resources.Res
|
||||
import honeydue.composeapp.generated.resources.documents_empty_no_warranties
|
||||
import honeydue.composeapp.generated.resources.documents_empty_no_documents
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
@@ -65,7 +69,7 @@ fun DocumentsTabContent(
|
||||
EmptyState(
|
||||
modifier = Modifier.testTag(AccessibilityIds.Document.emptyStateView),
|
||||
icon = if (isWarrantyTab) Icons.Default.ReceiptLong else Icons.Default.Description,
|
||||
message = if (isWarrantyTab) "No warranties found" else "No documents found"
|
||||
message = if (isWarrantyTab) stringResource(Res.string.documents_empty_no_warranties) else stringResource(Res.string.documents_empty_no_documents)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
|
||||
+2
-2
@@ -48,8 +48,8 @@ fun DetailRowPreview() {
|
||||
MaterialTheme {
|
||||
DetailRow(
|
||||
icon = Icons.Default.SquareFoot,
|
||||
label = "Square Footage",
|
||||
value = "1800 sq ft"
|
||||
label = "Square Footage", // i18n-ignore: Compose @Preview sample data, not shipped UI
|
||||
value = "1800 sq ft" // i18n-ignore: Compose @Preview sample data, not shipped UI
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -48,7 +48,7 @@ fun PropertyDetailItemPreview() {
|
||||
PropertyDetailItem(
|
||||
icon = Icons.Default.Bed,
|
||||
value = "3",
|
||||
label = "Bedrooms"
|
||||
label = "Bedrooms" // i18n-ignore: Compose @Preview sample data, not shipped UI
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -51,7 +51,7 @@ fun TaskStatChipPreview() {
|
||||
TaskStatChip(
|
||||
icon = Icons.Default.CheckCircle,
|
||||
value = "12",
|
||||
label = "Completed",
|
||||
label = "Completed", // i18n-ignore: Compose @Preview sample data, not shipped UI
|
||||
color = MaterialTheme.colorScheme.tertiary
|
||||
)
|
||||
}
|
||||
|
||||
+14
-10
@@ -17,6 +17,8 @@ import com.tt.honeyDue.models.TaskCompletion
|
||||
import com.tt.honeyDue.network.ApiResult
|
||||
import com.tt.honeyDue.network.APILayer
|
||||
import com.tt.honeyDue.util.DateUtils
|
||||
import honeydue.composeapp.generated.resources.*
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
@@ -66,7 +68,7 @@ fun CompletionHistorySheet(
|
||||
) {
|
||||
// Header
|
||||
Text(
|
||||
text = "Completion History",
|
||||
text = stringResource(Res.string.completion_history_title),
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
fontWeight = FontWeight.Bold,
|
||||
modifier = Modifier.padding(bottom = 8.dp)
|
||||
@@ -98,7 +100,8 @@ fun CompletionHistorySheet(
|
||||
shape = RoundedCornerShape(12.dp)
|
||||
) {
|
||||
Text(
|
||||
text = "${completions.size} ${if (completions.size == 1) "completion" else "completions"}",
|
||||
text = if (completions.size == 1) stringResource(Res.string.completion_history_count_one, completions.size)
|
||||
else stringResource(Res.string.completion_history_count_other, completions.size),
|
||||
modifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp),
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = MaterialTheme.colorScheme.onPrimaryContainer
|
||||
@@ -125,7 +128,7 @@ fun CompletionHistorySheet(
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Text(
|
||||
text = "Loading completions...",
|
||||
text = stringResource(Res.string.completion_history_loading),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
@@ -148,7 +151,7 @@ fun CompletionHistorySheet(
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Text(
|
||||
text = "Failed to load completions",
|
||||
text = stringResource(Res.string.completion_history_load_failed),
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
@@ -182,7 +185,7 @@ fun CompletionHistorySheet(
|
||||
) {
|
||||
Icon(Icons.Default.Refresh, contentDescription = null) // decorative
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text("Retry")
|
||||
Text(stringResource(Res.string.common_retry))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -203,13 +206,13 @@ fun CompletionHistorySheet(
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Text(
|
||||
text = "No Completions Yet",
|
||||
text = stringResource(Res.string.completion_history_empty_title),
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
Text(
|
||||
text = "This task has not been completed.",
|
||||
text = stringResource(Res.string.completion_history_empty_message),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
@@ -275,7 +278,7 @@ private fun CompletionHistoryCard(completion: TaskCompletionResponse) {
|
||||
)
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
Text(
|
||||
text = "Completed by ${user.displayName}",
|
||||
text = stringResource(Res.string.completion_history_completed_by, user.displayName),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
@@ -308,7 +311,7 @@ private fun CompletionHistoryCard(completion: TaskCompletionResponse) {
|
||||
if (completion.notes.isNotEmpty()) {
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
Text(
|
||||
text = "Notes",
|
||||
text = stringResource(Res.string.completions_notes_label),
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
@@ -360,7 +363,8 @@ private fun CompletionHistoryCard(completion: TaskCompletionResponse) {
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text(
|
||||
text = if (completion.images.size == 1) "View Photo" else "View Photos (${completion.images.size})",
|
||||
text = if (completion.images.size == 1) stringResource(Res.string.completion_history_view_photo)
|
||||
else stringResource(Res.string.tasks_card_view_photos, completion.images.size),
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
|
||||
+6
-4
@@ -28,6 +28,8 @@ import com.tt.honeyDue.network.ApiClient
|
||||
import com.tt.honeyDue.ui.components.AuthenticatedImage
|
||||
import com.tt.honeyDue.ui.theme.AppRadius
|
||||
import com.tt.honeyDue.ui.theme.AppSpacing
|
||||
import honeydue.composeapp.generated.resources.*
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
|
||||
@Composable
|
||||
fun PhotoViewerDialog(
|
||||
@@ -69,7 +71,7 @@ fun PhotoViewerDialog(
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = if (selectedImage != null) "Photo" else "Completion Photos",
|
||||
text = if (selectedImage != null) stringResource(Res.string.common_photo) else stringResource(Res.string.photos_completion_title),
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
@@ -82,7 +84,7 @@ fun PhotoViewerDialog(
|
||||
}) {
|
||||
Icon(
|
||||
Icons.Default.Close,
|
||||
contentDescription = "Close"
|
||||
contentDescription = stringResource(Res.string.common_close)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -101,7 +103,7 @@ fun PhotoViewerDialog(
|
||||
) {
|
||||
AuthenticatedImage(
|
||||
mediaUrl = selectedImage!!.mediaUrl,
|
||||
contentDescription = selectedImage!!.caption ?: "Task completion photo",
|
||||
contentDescription = selectedImage!!.caption ?: stringResource(Res.string.photos_task_completion_cd),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.weight(1f),
|
||||
@@ -143,7 +145,7 @@ fun PhotoViewerDialog(
|
||||
Column {
|
||||
AuthenticatedImage(
|
||||
mediaUrl = image.mediaUrl,
|
||||
contentDescription = image.caption ?: "Task completion photo",
|
||||
contentDescription = image.caption ?: stringResource(Res.string.photos_task_completion_cd),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.aspectRatio(1f),
|
||||
|
||||
@@ -660,8 +660,8 @@ fun TaskCardPreview() {
|
||||
id = 1,
|
||||
residenceId = 1,
|
||||
createdById = 1,
|
||||
title = "Clean Gutters",
|
||||
description = "Remove all debris from gutters and downspouts",
|
||||
title = "Clean Gutters", // i18n-ignore: Compose @Preview sample data, not shipped UI
|
||||
description = "Remove all debris from gutters and downspouts", // i18n-ignore: Compose @Preview sample data, not shipped UI
|
||||
category = TaskCategory(id = 1, name = "maintenance"),
|
||||
priority = TaskPriority(id = 2, name = "medium"),
|
||||
frequency = TaskFrequency(
|
||||
@@ -670,8 +670,8 @@ fun TaskCardPreview() {
|
||||
inProgress = false,
|
||||
dueDate = "2024-12-15",
|
||||
estimatedCost = 150.00,
|
||||
createdAt = "2024-01-01T00:00:00Z",
|
||||
updatedAt = "2024-01-01T00:00:00Z",
|
||||
createdAt = "2024-01-01T00:00:00Z", // i18n-ignore: Compose @Preview sample data, not shipped UI
|
||||
updatedAt = "2024-01-01T00:00:00Z", // i18n-ignore: Compose @Preview sample data, not shipped UI
|
||||
completions = emptyList()
|
||||
),
|
||||
onCompleteClick = {},
|
||||
|
||||
+9
-7
@@ -26,6 +26,8 @@ import com.tt.honeyDue.models.TaskDetail
|
||||
import com.tt.honeyDue.testing.AccessibilityIds
|
||||
import com.tt.honeyDue.ui.theme.AppRadius
|
||||
import com.tt.honeyDue.ui.theme.AppSpacing
|
||||
import honeydue.composeapp.generated.resources.*
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
@@ -53,7 +55,7 @@ fun TaskKanbanView(
|
||||
) { page ->
|
||||
when (page) {
|
||||
0 -> TaskColumn(
|
||||
title = "Upcoming",
|
||||
title = stringResource(Res.string.tasks_column_upcoming),
|
||||
icon = Icons.Default.CalendarToday,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
count = upcomingTasks.size,
|
||||
@@ -67,7 +69,7 @@ fun TaskKanbanView(
|
||||
onUnarchiveTask = null
|
||||
)
|
||||
1 -> TaskColumn(
|
||||
title = "In Progress",
|
||||
title = stringResource(Res.string.tasks_column_in_progress),
|
||||
icon = Icons.Default.PlayCircle,
|
||||
color = MaterialTheme.colorScheme.tertiary,
|
||||
count = inProgressTasks.size,
|
||||
@@ -81,7 +83,7 @@ fun TaskKanbanView(
|
||||
onUnarchiveTask = null
|
||||
)
|
||||
2 -> TaskColumn(
|
||||
title = "Done",
|
||||
title = stringResource(Res.string.tasks_column_done),
|
||||
icon = Icons.Default.CheckCircle,
|
||||
color = MaterialTheme.colorScheme.secondary,
|
||||
count = doneTasks.size,
|
||||
@@ -95,7 +97,7 @@ fun TaskKanbanView(
|
||||
onUnarchiveTask = null
|
||||
)
|
||||
3 -> TaskColumn(
|
||||
title = "Archived",
|
||||
title = stringResource(Res.string.tasks_column_archived),
|
||||
icon = Icons.Default.Archive,
|
||||
color = MaterialTheme.colorScheme.outline,
|
||||
count = archivedTasks.size,
|
||||
@@ -193,7 +195,7 @@ private fun TaskColumn(
|
||||
)
|
||||
Spacer(modifier = Modifier.height(AppSpacing.sm))
|
||||
Text(
|
||||
text = "No tasks",
|
||||
text = stringResource(Res.string.tasks_column_empty),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
@@ -314,7 +316,7 @@ private fun DynamicTaskColumn(
|
||||
bottomPadding: androidx.compose.ui.unit.Dp = 0.dp
|
||||
) {
|
||||
// Get icon from API response, with fallback
|
||||
val columnIcon = getIconFromName(column.icons["android"] ?: "List")
|
||||
val columnIcon = getIconFromName(column.icons["android"] ?: "List") // i18n-ignore: icon name identifier, non-UI
|
||||
|
||||
val columnColor = hexToColor(column.color)
|
||||
|
||||
@@ -378,7 +380,7 @@ private fun DynamicTaskColumn(
|
||||
)
|
||||
Spacer(modifier = Modifier.height(AppSpacing.sm))
|
||||
Text(
|
||||
text = "No tasks",
|
||||
text = stringResource(Res.string.tasks_column_empty),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
|
||||
@@ -157,9 +157,9 @@ class BlobShape(
|
||||
object Close : PathOp()
|
||||
|
||||
fun toSerialized(): String = when (this) {
|
||||
is MoveTo -> "M ${fmt(p.x)},${fmt(p.y)}"
|
||||
is LineTo -> "L ${fmt(p.x)},${fmt(p.y)}"
|
||||
is CubicTo -> "C ${fmt(c1.x)},${fmt(c1.y)} ${fmt(c2.x)},${fmt(c2.y)} ${fmt(end.x)},${fmt(end.y)}"
|
||||
is MoveTo -> "M ${fmt(p.x)},${fmt(p.y)}" // i18n-ignore: SVG path command, non-UI geometry
|
||||
is LineTo -> "L ${fmt(p.x)},${fmt(p.y)}" // i18n-ignore: SVG path command, non-UI geometry
|
||||
is CubicTo -> "C ${fmt(c1.x)},${fmt(c1.y)} ${fmt(c2.x)},${fmt(c2.y)} ${fmt(end.x)},${fmt(end.y)}" // i18n-ignore: SVG path command, non-UI geometry
|
||||
Close -> "Z"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,8 @@ import com.tt.honeyDue.viewmodel.TaskViewModel
|
||||
import com.tt.honeyDue.models.TaskDetail
|
||||
import com.tt.honeyDue.network.ApiResult
|
||||
import com.tt.honeyDue.ui.theme.*
|
||||
import honeydue.composeapp.generated.resources.*
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
@@ -102,7 +104,7 @@ fun AllTasksScreen(
|
||||
// Handle errors for task creation
|
||||
createTaskState.HandleErrors(
|
||||
onRetry = { /* Retry handled in dialog */ },
|
||||
errorTitle = "Failed to Create Task"
|
||||
errorTitle = stringResource(Res.string.tasks_create_failed_title)
|
||||
)
|
||||
|
||||
LaunchedEffect(createTaskState) {
|
||||
@@ -125,7 +127,7 @@ fun AllTasksScreen(
|
||||
TopAppBar(
|
||||
title = {
|
||||
Text(
|
||||
"All Tasks",
|
||||
stringResource(Res.string.tasks_all_title),
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
},
|
||||
@@ -136,7 +138,7 @@ fun AllTasksScreen(
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Refresh,
|
||||
contentDescription = "Refresh"
|
||||
contentDescription = stringResource(Res.string.common_refresh)
|
||||
)
|
||||
}
|
||||
IconButton(
|
||||
@@ -147,7 +149,7 @@ fun AllTasksScreen(
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Add,
|
||||
contentDescription = "Add Task"
|
||||
contentDescription = stringResource(Res.string.tasks_add)
|
||||
)
|
||||
}
|
||||
},
|
||||
@@ -161,7 +163,7 @@ fun AllTasksScreen(
|
||||
state = tasksState,
|
||||
onRetry = { viewModel.loadTasks(forceRefresh = true) },
|
||||
modifier = Modifier.padding(paddingValues),
|
||||
errorTitle = "Failed to Load Tasks"
|
||||
errorTitle = stringResource(Res.string.tasks_load_failed_title)
|
||||
) { taskData ->
|
||||
val hasNoTasks = taskData.columns.all { it.tasks.isEmpty() }
|
||||
|
||||
@@ -184,19 +186,19 @@ fun AllTasksScreen(
|
||||
iconColor = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
Text(
|
||||
"No tasks yet",
|
||||
stringResource(Res.string.tasks_empty_title),
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = MaterialTheme.colorScheme.textPrimary
|
||||
)
|
||||
Text(
|
||||
"Create your first task to get started",
|
||||
stringResource(Res.string.tasks_all_empty_subtitle),
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.textSecondary
|
||||
)
|
||||
Spacer(modifier = Modifier.height(OrganicSpacing.compact))
|
||||
OrganicPrimaryButton(
|
||||
text = "Add Task",
|
||||
text = stringResource(Res.string.tasks_add),
|
||||
onClick = { showNewTaskDialog = true },
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(0.7f)
|
||||
@@ -207,7 +209,7 @@ fun AllTasksScreen(
|
||||
if (myResidencesState is ApiResult.Success &&
|
||||
(myResidencesState as ApiResult.Success).data.residences.isEmpty()) {
|
||||
Text(
|
||||
"Add a property first from the Residences tab",
|
||||
stringResource(Res.string.tasks_add_property_first),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.error
|
||||
)
|
||||
|
||||
@@ -47,6 +47,7 @@ fun BiometricLockScreen(
|
||||
|
||||
val promptTitle = stringResource(Res.string.biometric_prompt_title)
|
||||
val promptSubtitle = stringResource(Res.string.biometric_prompt_subtitle)
|
||||
val incorrectPinMessage = stringResource(Res.string.biometric_incorrect_pin)
|
||||
|
||||
// Callback that maps platform result back onto the state machine.
|
||||
fun triggerPrompt() {
|
||||
@@ -134,7 +135,7 @@ fun BiometricLockScreen(
|
||||
if (showFallback) {
|
||||
// ----- Fallback PIN UI (after 3 biometric failures) -----
|
||||
Text(
|
||||
text = "Enter PIN to unlock",
|
||||
text = stringResource(Res.string.biometric_enter_pin),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
textAlign = TextAlign.Center
|
||||
@@ -148,7 +149,7 @@ fun BiometricLockScreen(
|
||||
pinError = null
|
||||
}
|
||||
},
|
||||
label = { Text("4-digit PIN") },
|
||||
label = { Text(stringResource(Res.string.biometric_pin_label)) },
|
||||
singleLine = true,
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.NumberPassword),
|
||||
isError = pinError != null,
|
||||
@@ -164,7 +165,7 @@ fun BiometricLockScreen(
|
||||
// storage. See BiometricLockScreen.kt:line
|
||||
// for the PIN constant below.
|
||||
if (!lockState.onPinEntered(pinInput, TODO_FALLBACK_PIN)) {
|
||||
pinError = "Incorrect PIN"
|
||||
pinError = incorrectPinMessage
|
||||
}
|
||||
},
|
||||
enabled = pinInput.length == 4,
|
||||
@@ -174,7 +175,7 @@ fun BiometricLockScreen(
|
||||
shape = MaterialTheme.shapes.medium,
|
||||
) {
|
||||
Text(
|
||||
text = "Unlock",
|
||||
text = stringResource(Res.string.biometric_unlock),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
|
||||
@@ -165,7 +165,7 @@ fun CompleteTaskScreen(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = OrganicSpacing.lg)
|
||||
.clickableWithRipple(onClickLabel = "Select contractor") {
|
||||
.clickableWithRipple(onClickLabel = stringResource(Res.string.completions_select_contractor)) {
|
||||
showContractorPicker = true
|
||||
}
|
||||
) {
|
||||
@@ -305,7 +305,7 @@ fun CompleteTaskScreen(
|
||||
targetValue = if (isSelected) Color(0xFFFFD700)
|
||||
else MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.3f),
|
||||
animationSpec = tween(durationMillis = 150),
|
||||
label = "starColor"
|
||||
label = "starColor" // i18n-ignore: animation label, non-UI
|
||||
)
|
||||
|
||||
IconButton(
|
||||
@@ -317,7 +317,7 @@ fun CompleteTaskScreen(
|
||||
) {
|
||||
Icon(
|
||||
imageVector = if (isSelected) Icons.Default.Star else Icons.Default.StarOutline,
|
||||
contentDescription = "$star stars",
|
||||
contentDescription = stringResource(Res.string.completions_star_rating_cd, star),
|
||||
tint = starColor,
|
||||
modifier = Modifier.size(40.dp)
|
||||
)
|
||||
@@ -395,6 +395,8 @@ fun CompleteTaskScreen(
|
||||
Spacer(modifier = Modifier.height(OrganicSpacing.xl))
|
||||
|
||||
// Complete Button
|
||||
val contractorPrefix = stringResource(Res.string.completions_contractor_prefix)
|
||||
val completedByPrefix = stringResource(Res.string.completions_completed_by_prefix)
|
||||
OrganicPrimaryButton(
|
||||
text = stringResource(Res.string.completions_complete_button),
|
||||
onClick = {
|
||||
@@ -402,12 +404,12 @@ fun CompleteTaskScreen(
|
||||
isSubmitting = true
|
||||
val notesWithContractor = buildString {
|
||||
selectedContractor?.let {
|
||||
append("Contractor: ${it.name}")
|
||||
append("$contractorPrefix ${it.name}")
|
||||
it.company?.let { company -> append(" ($company)") }
|
||||
append("\n")
|
||||
}
|
||||
if (completedByName.isNotBlank()) {
|
||||
append("Completed by: $completedByName\n")
|
||||
append("$completedByPrefix $completedByName\n") // i18n-ignore: localized prefix var + data concatenation, no literal prose
|
||||
}
|
||||
if (notes.isNotBlank()) {
|
||||
append(notes)
|
||||
@@ -527,7 +529,7 @@ private fun ImageThumbnailCard(
|
||||
modifier = Modifier
|
||||
.align(Alignment.TopEnd)
|
||||
.minTouchTarget()
|
||||
.clickableWithRipple(onClickLabel = "Remove photo", onClick = onRemove),
|
||||
.clickableWithRipple(onClickLabel = stringResource(Res.string.completions_remove_photo), onClick = onRemove),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Box(
|
||||
@@ -585,7 +587,7 @@ private fun ContractorPickerSheet(
|
||||
if (selectedContractor == null) {
|
||||
Icon(
|
||||
Icons.Default.Check,
|
||||
contentDescription = "Selected",
|
||||
contentDescription = stringResource(Res.string.common_selected),
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
@@ -617,7 +619,7 @@ private fun ContractorPickerSheet(
|
||||
if (selectedContractor?.id == contractor.id) {
|
||||
Icon(
|
||||
Icons.Default.Check,
|
||||
contentDescription = "Selected",
|
||||
contentDescription = stringResource(Res.string.common_selected),
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
|
||||
+9
-8
@@ -157,6 +157,7 @@ fun ContractorDetailScreen(
|
||||
CircularProgressIndicator(color = MaterialTheme.colorScheme.primary)
|
||||
}
|
||||
) { contractor ->
|
||||
val propertyRefLabel = stringResource(Res.string.contractors_property_ref)
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentPadding = PaddingValues(OrganicSpacing.medium),
|
||||
@@ -281,7 +282,7 @@ fun ContractorDetailScreen(
|
||||
.testTag(AccessibilityIds.Contractor.callButton),
|
||||
onClick = {
|
||||
try {
|
||||
uriHandler.openUri("tel:${phone.replace(" ", "")}")
|
||||
uriHandler.openUri("tel:${phone.replace(" ", "")}") // i18n-ignore: tel: URI, non-UI
|
||||
} catch (e: Exception) { /* Handle error */ }
|
||||
}
|
||||
)
|
||||
@@ -297,7 +298,7 @@ fun ContractorDetailScreen(
|
||||
.testTag(AccessibilityIds.Contractor.emailButton),
|
||||
onClick = {
|
||||
try {
|
||||
uriHandler.openUri("mailto:$email")
|
||||
uriHandler.openUri("mailto:$email") // i18n-ignore: mailto: URI, non-UI
|
||||
} catch (e: Exception) { /* Handle error */ }
|
||||
}
|
||||
)
|
||||
@@ -332,7 +333,7 @@ fun ContractorDetailScreen(
|
||||
contractor.stateProvince,
|
||||
contractor.postalCode
|
||||
).joinToString(", ")
|
||||
uriHandler.openUri("geo:0,0?q=$address")
|
||||
uriHandler.openUri("geo:0,0?q=$address") // i18n-ignore: geo: URI, non-UI
|
||||
} catch (e: Exception) { /* Handle error */ }
|
||||
}
|
||||
)
|
||||
@@ -352,7 +353,7 @@ fun ContractorDetailScreen(
|
||||
iconTint = MaterialTheme.colorScheme.primary,
|
||||
onClick = {
|
||||
try {
|
||||
uriHandler.openUri("tel:${phone.replace(" ", "")}")
|
||||
uriHandler.openUri("tel:${phone.replace(" ", "")}") // i18n-ignore: tel: URI, non-UI
|
||||
} catch (e: Exception) { /* Handle error */ }
|
||||
}
|
||||
)
|
||||
@@ -366,7 +367,7 @@ fun ContractorDetailScreen(
|
||||
iconTint = MaterialTheme.colorScheme.secondary,
|
||||
onClick = {
|
||||
try {
|
||||
uriHandler.openUri("mailto:$email")
|
||||
uriHandler.openUri("mailto:$email") // i18n-ignore: mailto: URI, non-UI
|
||||
} catch (e: Exception) { /* Handle error */ }
|
||||
}
|
||||
)
|
||||
@@ -432,7 +433,7 @@ fun ContractorDetailScreen(
|
||||
contractor.stateProvince,
|
||||
contractor.postalCode
|
||||
).joinToString(", ")
|
||||
uriHandler.openUri("geo:0,0?q=$address")
|
||||
uriHandler.openUri("geo:0,0?q=$address") // i18n-ignore: geo: URI, non-UI
|
||||
} catch (e: Exception) { /* Handle error */ }
|
||||
}
|
||||
)
|
||||
@@ -444,7 +445,7 @@ fun ContractorDetailScreen(
|
||||
// Associated Property
|
||||
contractor.residenceId?.let { resId ->
|
||||
val residenceName = residences.find { r -> r.id == resId }?.name
|
||||
?: "Property #$resId"
|
||||
?: "$propertyRefLabel$resId"
|
||||
|
||||
item {
|
||||
DetailSection(title = stringResource(Res.string.contractors_associated_property)) {
|
||||
@@ -683,7 +684,7 @@ fun ClickableDetailRow(
|
||||
}
|
||||
Icon(
|
||||
Icons.Default.OpenInNew,
|
||||
contentDescription = "Open",
|
||||
contentDescription = stringResource(Res.string.common_open),
|
||||
modifier = Modifier.size(16.dp),
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
|
||||
@@ -295,8 +295,8 @@ fun DocumentDetailScreen(
|
||||
)
|
||||
OrganicDivider()
|
||||
|
||||
document.residenceId?.let { DetailRow(stringResource(Res.string.documents_residence), "Residence #$it") }
|
||||
document.taskId?.let { DetailRow(stringResource(Res.string.documents_contractor), "Task #$it") }
|
||||
document.residenceId?.let { DetailRow(stringResource(Res.string.documents_residence), stringResource(Res.string.documents_residence_ref, it)) }
|
||||
document.taskId?.let { DetailRow(stringResource(Res.string.documents_contractor), stringResource(Res.string.documents_task_ref, it)) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -531,7 +531,7 @@ fun DocumentImageViewer(
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = if (showFullImage) "Image ${selectedIndex + 1} of ${images.size}" else "Document Images",
|
||||
text = if (showFullImage) stringResource(Res.string.documents_image_index, selectedIndex + 1, images.size) else stringResource(Res.string.documents_images_title),
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
@@ -544,7 +544,7 @@ fun DocumentImageViewer(
|
||||
}) {
|
||||
Icon(
|
||||
Icons.Default.Close,
|
||||
contentDescription = "Close"
|
||||
contentDescription = stringResource(Res.string.common_close)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -594,17 +594,17 @@ fun DocumentImageViewer(
|
||||
onClick = { selectedIndex = (selectedIndex - 1 + images.size) % images.size },
|
||||
enabled = selectedIndex > 0
|
||||
) {
|
||||
Icon(Icons.AutoMirrored.Filled.ArrowBack, "Previous")
|
||||
Icon(Icons.AutoMirrored.Filled.ArrowBack, stringResource(Res.string.documents_previous))
|
||||
Spacer(modifier = Modifier.width(OrganicSpacing.sm))
|
||||
Text("Previous")
|
||||
Text(stringResource(Res.string.documents_previous))
|
||||
}
|
||||
OutlinedButton(
|
||||
onClick = { selectedIndex = (selectedIndex + 1) % images.size },
|
||||
enabled = selectedIndex < images.size - 1
|
||||
) {
|
||||
Text("Next")
|
||||
Text(stringResource(Res.string.documents_next))
|
||||
Spacer(modifier = Modifier.width(OrganicSpacing.sm))
|
||||
Icon(Icons.AutoMirrored.Filled.ArrowForward, "Next")
|
||||
Icon(Icons.AutoMirrored.Filled.ArrowForward, stringResource(Res.string.documents_next))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -210,7 +210,7 @@ fun EditTaskScreen(
|
||||
selectedFrequency = frequency
|
||||
frequencyExpanded = false
|
||||
// Clear custom interval if not Custom frequency
|
||||
if (!frequency.name.equals("Custom", ignoreCase = true)) {
|
||||
if (!frequency.name.equals("Custom", ignoreCase = true)) { // i18n-ignore: API enum value comparison, non-UI
|
||||
customIntervalDays = ""
|
||||
}
|
||||
}
|
||||
@@ -220,7 +220,7 @@ fun EditTaskScreen(
|
||||
}
|
||||
|
||||
// Custom Interval Days (only for "Custom" frequency)
|
||||
if (selectedFrequency?.name?.equals("Custom", ignoreCase = true) == true) {
|
||||
if (selectedFrequency?.name?.equals("Custom", ignoreCase = true) == true) { // i18n-ignore: API enum value comparison, non-UI
|
||||
OutlinedTextField(
|
||||
value = customIntervalDays,
|
||||
onValueChange = { customIntervalDays = it.filter { char -> char.isDigit() } },
|
||||
@@ -325,7 +325,7 @@ fun EditTaskScreen(
|
||||
description = description.ifBlank { null },
|
||||
categoryId = selectedCategory!!.id,
|
||||
frequencyId = selectedFrequency!!.id,
|
||||
customIntervalDays = if (selectedFrequency?.name?.equals("Custom", ignoreCase = true) == true && customIntervalDays.isNotBlank()) {
|
||||
customIntervalDays = if (selectedFrequency?.name?.equals("Custom", ignoreCase = true) == true && customIntervalDays.isNotBlank()) { // i18n-ignore: API enum value comparison, non-UI
|
||||
customIntervalDays.toIntOrNull()
|
||||
} else null,
|
||||
priorityId = selectedPriority!!.id,
|
||||
|
||||
@@ -40,7 +40,7 @@ fun ForgotPasswordScreen(
|
||||
viewModel.setEmail(email)
|
||||
viewModel.requestPasswordReset(email)
|
||||
},
|
||||
errorTitle = "Failed to Send Reset Code"
|
||||
errorTitle = stringResource(Res.string.forgot_send_failed_title)
|
||||
)
|
||||
|
||||
// Handle automatic navigation to next step
|
||||
@@ -138,7 +138,7 @@ fun ForgotPasswordScreen(
|
||||
)
|
||||
|
||||
Text(
|
||||
"We'll send a 6-digit verification code to this address",
|
||||
stringResource(Res.string.forgot_send_code_hint),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.textSecondary,
|
||||
textAlign = TextAlign.Center
|
||||
@@ -161,11 +161,11 @@ fun ForgotPasswordScreen(
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.CheckCircle,
|
||||
contentDescription = "Success",
|
||||
contentDescription = stringResource(Res.string.common_success),
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
Text(
|
||||
"Check your email for a 6-digit verification code",
|
||||
stringResource(Res.string.forgot_check_email_msg),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.textPrimary
|
||||
)
|
||||
@@ -192,7 +192,7 @@ fun ForgotPasswordScreen(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text(
|
||||
"Remember your password? Back to Login",
|
||||
stringResource(Res.string.forgot_back_to_login),
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
}
|
||||
|
||||
@@ -228,7 +228,7 @@ fun LoginScreen(
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
Text(
|
||||
text = "or",
|
||||
text = stringResource(Res.string.common_or),
|
||||
modifier = Modifier.padding(horizontal = OrganicSpacing.lg),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
|
||||
@@ -50,6 +50,7 @@ fun ManageUsersScreen(
|
||||
val scope = rememberCoroutineScope()
|
||||
val clipboardManager = LocalClipboardManager.current
|
||||
val snackbarHostState = remember { SnackbarHostState() }
|
||||
val codeCopiedMessage = stringResource(Res.string.manage_users_code_copied)
|
||||
|
||||
// Extracted so retry can reuse it.
|
||||
suspend fun loadUsers() {
|
||||
@@ -115,8 +116,8 @@ fun ManageUsersScreen(
|
||||
}
|
||||
} else if (error != null) {
|
||||
StandardErrorState(
|
||||
title = "Couldn't load users",
|
||||
message = error ?: "Unknown error",
|
||||
title = stringResource(Res.string.manage_users_load_failed),
|
||||
message = error ?: stringResource(Res.string.error_generic),
|
||||
onRetry = { scope.launch { loadUsers() } },
|
||||
modifier = Modifier.padding(paddingValues)
|
||||
)
|
||||
@@ -245,13 +246,13 @@ fun ManageUsersScreen(
|
||||
onClick = {
|
||||
clipboardManager.setText(AnnotatedString(shareCode!!.code))
|
||||
scope.launch {
|
||||
snackbarHostState.showSnackbar("Code copied to clipboard")
|
||||
snackbarHostState.showSnackbar(codeCopiedMessage)
|
||||
}
|
||||
}
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.ContentCopy,
|
||||
contentDescription = "Copy code",
|
||||
contentDescription = stringResource(Res.string.manage_users_copy_code),
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
@@ -341,7 +342,7 @@ fun ManageUsersScreen(
|
||||
AlertDialog(
|
||||
onDismissRequest = { showRemoveConfirmation = null },
|
||||
title = { Text(stringResource(Res.string.manage_users_remove)) },
|
||||
text = { Text("Remove ${user.username} from this property?") },
|
||||
text = { Text(stringResource(Res.string.manage_users_remove_confirm, user.username)) },
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
|
||||
+4
-4
@@ -296,7 +296,7 @@ fun NotificationPreferencesScreen(
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Error,
|
||||
contentDescription = "Error",
|
||||
contentDescription = stringResource(Res.string.common_error),
|
||||
tint = MaterialTheme.colorScheme.error,
|
||||
)
|
||||
Text(
|
||||
@@ -702,13 +702,13 @@ private fun HourPickerDialog(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||
) {
|
||||
HourColumn(label = "AM", range = 6..11, selectedHour = selectedHour) {
|
||||
HourColumn(label = stringResource(Res.string.time_am), range = 6..11, selectedHour = selectedHour) {
|
||||
selectedHour = it
|
||||
}
|
||||
HourColumn(label = "PM", range = 12..17, selectedHour = selectedHour) {
|
||||
HourColumn(label = stringResource(Res.string.time_pm), range = 12..17, selectedHour = selectedHour) {
|
||||
selectedHour = it
|
||||
}
|
||||
HourColumn(label = "EVE", range = 18..23, selectedHour = selectedHour) {
|
||||
HourColumn(label = stringResource(Res.string.time_eve), range = 18..23, selectedHour = selectedHour) {
|
||||
selectedHour = it
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.tt.honeyDue.ui.components.HandleErrors
|
||||
import com.tt.honeyDue.ui.screens.theme.themeDisplayName
|
||||
import com.tt.honeyDue.ui.components.common.ErrorCard
|
||||
import com.tt.honeyDue.ui.components.dialogs.DeleteAccountDialog
|
||||
import com.tt.honeyDue.utils.SubscriptionHelper
|
||||
@@ -267,7 +268,7 @@ fun ProfileScreen(
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
Text(
|
||||
text = currentTheme.displayName,
|
||||
text = themeDisplayName(currentTheme.id),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
@@ -379,7 +380,7 @@ fun ProfileScreen(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable {
|
||||
uriHandler.openUri("mailto:honeydueSupport@treymail.com?subject=honeyDue%20Support%20Request")
|
||||
uriHandler.openUri("mailto:honeydueSupport@treymail.com?subject=honeyDue%20Support%20Request") // i18n-ignore: mailto: support URI, non-UI
|
||||
}
|
||||
.naturalShadow()
|
||||
) {
|
||||
@@ -471,7 +472,7 @@ fun ProfileScreen(
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Star,
|
||||
contentDescription = "Subscription",
|
||||
contentDescription = stringResource(Res.string.profile_subscription_cd),
|
||||
tint = if (SubscriptionHelper.currentTier == "pro") MaterialTheme.colorScheme.tertiary else MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
|
||||
@@ -636,7 +637,7 @@ fun ProfileScreen(
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Error,
|
||||
contentDescription = "Error",
|
||||
contentDescription = stringResource(Res.string.common_error),
|
||||
tint = MaterialTheme.colorScheme.error
|
||||
)
|
||||
Text(
|
||||
@@ -666,7 +667,7 @@ fun ProfileScreen(
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.CheckCircle,
|
||||
contentDescription = "Success",
|
||||
contentDescription = stringResource(Res.string.common_success),
|
||||
tint = MaterialTheme.colorScheme.onPrimaryContainer
|
||||
)
|
||||
Text(
|
||||
|
||||
+11
-11
@@ -43,7 +43,7 @@ fun ResetPasswordScreen(
|
||||
// Handle errors for password reset
|
||||
resetPasswordState.HandleErrors(
|
||||
onRetry = { viewModel.resetPassword(newPassword, confirmPassword) },
|
||||
errorTitle = "Password Reset Failed"
|
||||
errorTitle = stringResource(Res.string.reset_pw_failed_title)
|
||||
)
|
||||
|
||||
val errorMessage = when (resetPasswordState) {
|
||||
@@ -116,7 +116,7 @@ fun ResetPasswordScreen(
|
||||
)
|
||||
|
||||
Text(
|
||||
text = "Success!",
|
||||
text = stringResource(Res.string.common_success),
|
||||
style = MaterialTheme.typography.headlineMedium,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.textPrimary,
|
||||
@@ -124,7 +124,7 @@ fun ResetPasswordScreen(
|
||||
)
|
||||
|
||||
Text(
|
||||
text = "Your password has been reset successfully",
|
||||
text = stringResource(Res.string.reset_pw_success_msg),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.textSecondary,
|
||||
textAlign = TextAlign.Center
|
||||
@@ -136,7 +136,7 @@ fun ResetPasswordScreen(
|
||||
showBlob = false
|
||||
) {
|
||||
Text(
|
||||
"You can now log in with your new password",
|
||||
stringResource(Res.string.reset_pw_can_login),
|
||||
modifier = Modifier.padding(OrganicSpacing.cozy),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.textPrimary,
|
||||
@@ -149,7 +149,7 @@ fun ResetPasswordScreen(
|
||||
)
|
||||
|
||||
OrganicPrimaryButton(
|
||||
text = "Return to Login",
|
||||
text = stringResource(Res.string.reset_return_to_login),
|
||||
onClick = onPasswordResetSuccess
|
||||
)
|
||||
} else {
|
||||
@@ -163,7 +163,7 @@ fun ResetPasswordScreen(
|
||||
)
|
||||
|
||||
Text(
|
||||
text = "Set New Password",
|
||||
text = stringResource(Res.string.reset_set_new_pw_title),
|
||||
style = MaterialTheme.typography.headlineMedium,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.textPrimary,
|
||||
@@ -171,7 +171,7 @@ fun ResetPasswordScreen(
|
||||
)
|
||||
|
||||
Text(
|
||||
text = "Create a strong password to secure your account",
|
||||
text = stringResource(Res.string.reset_create_strong_pw),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.textSecondary,
|
||||
textAlign = TextAlign.Center
|
||||
@@ -190,7 +190,7 @@ fun ResetPasswordScreen(
|
||||
verticalArrangement = Arrangement.spacedBy(OrganicSpacing.compact)
|
||||
) {
|
||||
Text(
|
||||
"Password Requirements",
|
||||
stringResource(Res.string.reset_pw_requirements),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = MaterialTheme.colorScheme.textPrimary
|
||||
@@ -233,7 +233,7 @@ fun ResetPasswordScreen(
|
||||
IconButton(onClick = { newPasswordVisible = !newPasswordVisible }) {
|
||||
Icon(
|
||||
if (newPasswordVisible) Icons.Default.Visibility else Icons.Default.VisibilityOff,
|
||||
contentDescription = if (newPasswordVisible) "Hide password" else "Show password"
|
||||
contentDescription = if (newPasswordVisible) stringResource(Res.string.auth_hide_password) else stringResource(Res.string.auth_show_password)
|
||||
)
|
||||
}
|
||||
},
|
||||
@@ -257,7 +257,7 @@ fun ResetPasswordScreen(
|
||||
IconButton(onClick = { confirmPasswordVisible = !confirmPasswordVisible }) {
|
||||
Icon(
|
||||
if (confirmPasswordVisible) Icons.Default.Visibility else Icons.Default.VisibilityOff,
|
||||
contentDescription = if (confirmPasswordVisible) "Hide password" else "Show password"
|
||||
contentDescription = if (confirmPasswordVisible) stringResource(Res.string.auth_hide_password) else stringResource(Res.string.auth_show_password)
|
||||
)
|
||||
}
|
||||
},
|
||||
@@ -274,7 +274,7 @@ fun ResetPasswordScreen(
|
||||
)
|
||||
|
||||
OrganicPrimaryButton(
|
||||
text = if (isLoggingIn) "Logging in..." else stringResource(Res.string.auth_reset_button),
|
||||
text = if (isLoggingIn) stringResource(Res.string.auth_logging_in) else stringResource(Res.string.auth_reset_button),
|
||||
onClick = {
|
||||
viewModel.resetPassword(newPassword, confirmPassword)
|
||||
},
|
||||
|
||||
+5
-5
@@ -402,7 +402,7 @@ fun ResidenceDetailScreen(
|
||||
AlertDialog(
|
||||
onDismissRequest = { showShareError = false },
|
||||
title = { Text(stringResource(Res.string.common_error)) },
|
||||
text = { Text(shareState.error ?: "Failed to share residence") },
|
||||
text = { Text(shareState.error ?: stringResource(Res.string.residence_share_failed)) },
|
||||
confirmButton = {
|
||||
TextButton(onClick = { showShareError = false }) {
|
||||
Text(stringResource(Res.string.common_ok))
|
||||
@@ -666,13 +666,13 @@ fun ResidenceDetailScreen(
|
||||
}
|
||||
if (residence.apartmentUnit != null) {
|
||||
Text(
|
||||
text = "Unit: ${residence.apartmentUnit}",
|
||||
text = stringResource(Res.string.residence_unit_label, residence.apartmentUnit),
|
||||
color = MaterialTheme.colorScheme.textSecondary
|
||||
)
|
||||
}
|
||||
if (residence.city != null || residence.stateProvince != null || residence.postalCode != null) {
|
||||
Text(
|
||||
text = "${residence.city ?: ""}, ${residence.stateProvince ?: ""} ${residence.postalCode ?: ""}",
|
||||
text = "${residence.city ?: ""}, ${residence.stateProvince ?: ""} ${residence.postalCode ?: ""}", // i18n-ignore: address data concatenation, no translatable prose
|
||||
color = MaterialTheme.colorScheme.textSecondary
|
||||
)
|
||||
}
|
||||
@@ -876,7 +876,7 @@ fun ResidenceDetailScreen(
|
||||
is ApiResult.Error -> {
|
||||
item {
|
||||
CompactErrorState(
|
||||
message = "Error loading tasks: ${com.tt.honeyDue.util.ErrorMessageParser.parse((tasksState as ApiResult.Error).message)}",
|
||||
message = stringResource(Res.string.residence_error_loading_tasks, com.tt.honeyDue.util.ErrorMessageParser.parse((tasksState as ApiResult.Error).message)),
|
||||
onRetry = { residenceViewModel.loadResidenceTasks(residenceId) }
|
||||
)
|
||||
}
|
||||
@@ -1011,7 +1011,7 @@ fun ResidenceDetailScreen(
|
||||
is ApiResult.Error -> {
|
||||
item {
|
||||
CompactErrorState(
|
||||
message = "Error loading contractors: ${com.tt.honeyDue.util.ErrorMessageParser.parse((contractorsState as ApiResult.Error).message)}",
|
||||
message = stringResource(Res.string.residence_error_loading_contractors, com.tt.honeyDue.util.ErrorMessageParser.parse((contractorsState as ApiResult.Error).message)),
|
||||
onRetry = { residenceViewModel.loadResidenceContractors(residenceId) }
|
||||
)
|
||||
}
|
||||
|
||||
@@ -390,7 +390,7 @@ fun ResidenceFormScreen(
|
||||
if (isEditMode && isCurrentUserOwner) {
|
||||
OrganicDivider()
|
||||
Text(
|
||||
text = "Shared Users (${users.size})",
|
||||
text = stringResource(Res.string.properties_shared_users_count, users.size),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
@@ -404,7 +404,7 @@ fun ResidenceFormScreen(
|
||||
}
|
||||
} else if (users.isEmpty()) {
|
||||
Text(
|
||||
text = "No shared users",
|
||||
text = stringResource(Res.string.properties_no_shared_users),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(vertical = OrganicSpacing.compact)
|
||||
@@ -422,7 +422,7 @@ fun ResidenceFormScreen(
|
||||
}
|
||||
|
||||
Text(
|
||||
text = "Users with access to this residence. Use the share button to invite others.",
|
||||
text = stringResource(Res.string.properties_shared_users_helper),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(top = 4.dp)
|
||||
@@ -485,9 +485,9 @@ fun ResidenceFormScreen(
|
||||
showRemoveUserConfirmation = false
|
||||
userToRemove = null
|
||||
},
|
||||
title = { Text("Remove User") },
|
||||
title = { Text(stringResource(Res.string.properties_remove_user)) },
|
||||
text = {
|
||||
Text("Are you sure you want to remove ${userToRemove?.username} from this residence?")
|
||||
Text(stringResource(Res.string.properties_remove_user_confirm, userToRemove?.username ?: ""))
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
@@ -514,7 +514,7 @@ fun ResidenceFormScreen(
|
||||
contentColor = MaterialTheme.colorScheme.error
|
||||
)
|
||||
) {
|
||||
Text("Remove")
|
||||
Text(stringResource(Res.string.properties_remove_button))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
@@ -524,7 +524,7 @@ fun ResidenceFormScreen(
|
||||
userToRemove = null
|
||||
}
|
||||
) {
|
||||
Text("Cancel")
|
||||
Text(stringResource(Res.string.common_cancel))
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -572,7 +572,7 @@ private fun UserListItem(
|
||||
IconButton(onClick = onRemove) {
|
||||
Icon(
|
||||
Icons.Default.Delete,
|
||||
contentDescription = "Remove user",
|
||||
contentDescription = stringResource(Res.string.manage_users_remove_user),
|
||||
tint = MaterialTheme.colorScheme.error
|
||||
)
|
||||
}
|
||||
|
||||
@@ -327,7 +327,7 @@ fun ResidencesScreen(
|
||||
) {
|
||||
Icon(Icons.Default.Star, contentDescription = null) // decorative
|
||||
Text(
|
||||
"Upgrade to Add",
|
||||
stringResource(Res.string.residences_upgrade_to_add),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
@@ -462,7 +462,7 @@ fun ResidencesScreen(
|
||||
animation = tween(800, easing = EaseInOut),
|
||||
repeatMode = RepeatMode.Reverse
|
||||
),
|
||||
label = "pulseScale"
|
||||
label = "pulseScale" // i18n-ignore: animation label, non-UI
|
||||
)
|
||||
|
||||
OrganicCard(
|
||||
@@ -570,7 +570,7 @@ fun ResidencesScreen(
|
||||
if (residence.isPrimary) {
|
||||
Icon(
|
||||
Icons.Default.Star,
|
||||
contentDescription = "Primary residence",
|
||||
contentDescription = stringResource(Res.string.residences_primary_cd),
|
||||
modifier = Modifier.size(16.dp),
|
||||
tint = Color(0xFFFFD700) // Gold color
|
||||
)
|
||||
|
||||
@@ -237,7 +237,7 @@ fun TasksScreen(
|
||||
verticalAlignment = androidx.compose.ui.Alignment.CenterVertically
|
||||
) {
|
||||
OrganicIconContainer(
|
||||
icon = getIconFromName(column.icons["android"] ?: "List"),
|
||||
icon = getIconFromName(column.icons["android"] ?: "List"), // i18n-ignore: icon name identifier, non-UI
|
||||
size = 40.dp,
|
||||
iconScale = 0.5f,
|
||||
backgroundColor = hexToColor(column.color).copy(alpha = 0.2f),
|
||||
@@ -251,7 +251,7 @@ fun TasksScreen(
|
||||
}
|
||||
Icon(
|
||||
if (isExpanded) Icons.Default.KeyboardArrowUp else Icons.Default.KeyboardArrowDown,
|
||||
contentDescription = if (isExpanded) "Collapse" else "Expand",
|
||||
contentDescription = if (isExpanded) stringResource(Res.string.common_collapse) else stringResource(Res.string.common_expand),
|
||||
tint = MaterialTheme.colorScheme.textSecondary
|
||||
)
|
||||
}
|
||||
|
||||
@@ -41,10 +41,12 @@ fun VerifyEmailScreen(
|
||||
|
||||
val verifyState by viewModel.verifyEmailState.collectAsStateWithLifecycle()
|
||||
|
||||
val invalidCodeMessage = stringResource(Res.string.verify_email_invalid_code)
|
||||
|
||||
// Handle errors for email verification
|
||||
verifyState.HandleErrors(
|
||||
onRetry = { viewModel.verifyEmail(code) },
|
||||
errorTitle = "Verification Failed"
|
||||
errorTitle = stringResource(Res.string.verify_email_failed_title)
|
||||
)
|
||||
|
||||
LaunchedEffect(verifyState) {
|
||||
@@ -147,11 +149,11 @@ fun VerifyEmailScreen(
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Info,
|
||||
contentDescription = "Info",
|
||||
contentDescription = stringResource(Res.string.common_info),
|
||||
tint = MaterialTheme.colorScheme.error
|
||||
)
|
||||
Text(
|
||||
text = "Email verification is required. Check your inbox for a 6-digit code.",
|
||||
text = stringResource(Res.string.verify_email_required_msg),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.textPrimary,
|
||||
textAlign = TextAlign.Center,
|
||||
@@ -196,7 +198,7 @@ fun VerifyEmailScreen(
|
||||
isLoading = true
|
||||
viewModel.verifyEmail(code)
|
||||
} else {
|
||||
errorMessage = "Please enter a valid 6-digit code"
|
||||
errorMessage = invalidCodeMessage
|
||||
}
|
||||
},
|
||||
modifier = Modifier.testTag(AccessibilityIds.Authentication.verifyButton),
|
||||
@@ -207,7 +209,7 @@ fun VerifyEmailScreen(
|
||||
Spacer(modifier = Modifier.height(OrganicSpacing.compact))
|
||||
|
||||
Text(
|
||||
text = "Didn't receive the code? Check your spam folder or contact support.",
|
||||
text = stringResource(Res.string.verify_email_didnt_receive),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.textSecondary,
|
||||
textAlign = TextAlign.Center
|
||||
|
||||
+9
-9
@@ -38,7 +38,7 @@ fun VerifyResetCodeScreen(
|
||||
// Handle errors for code verification
|
||||
verifyCodeState.HandleErrors(
|
||||
onRetry = { viewModel.verifyResetCode(email, code) },
|
||||
errorTitle = "Code Verification Failed"
|
||||
errorTitle = stringResource(Res.string.reset_verify_failed_title)
|
||||
)
|
||||
|
||||
// Handle automatic navigation to next step
|
||||
@@ -102,7 +102,7 @@ fun VerifyResetCodeScreen(
|
||||
)
|
||||
|
||||
Text(
|
||||
text = "Check Your Email",
|
||||
text = stringResource(Res.string.reset_check_email_title),
|
||||
style = MaterialTheme.typography.headlineMedium,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.textPrimary,
|
||||
@@ -110,7 +110,7 @@ fun VerifyResetCodeScreen(
|
||||
)
|
||||
|
||||
Text(
|
||||
text = "We sent a 6-digit code to",
|
||||
text = stringResource(Res.string.reset_sent_code_to),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.textSecondary,
|
||||
textAlign = TextAlign.Center
|
||||
@@ -142,7 +142,7 @@ fun VerifyResetCodeScreen(
|
||||
tint = MaterialTheme.colorScheme.secondary
|
||||
)
|
||||
Text(
|
||||
"Code expires in 15 minutes",
|
||||
stringResource(Res.string.reset_code_expires),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = MaterialTheme.colorScheme.textPrimary
|
||||
@@ -173,7 +173,7 @@ fun VerifyResetCodeScreen(
|
||||
)
|
||||
|
||||
Text(
|
||||
"Enter the 6-digit code from your email",
|
||||
stringResource(Res.string.reset_enter_code_hint),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.textSecondary,
|
||||
textAlign = TextAlign.Center
|
||||
@@ -196,11 +196,11 @@ fun VerifyResetCodeScreen(
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.CheckCircle,
|
||||
contentDescription = "Verified",
|
||||
contentDescription = stringResource(Res.string.common_verified),
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
Text(
|
||||
"Code verified! Now set your new password",
|
||||
stringResource(Res.string.reset_code_verified_msg),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.textPrimary
|
||||
)
|
||||
@@ -226,7 +226,7 @@ fun VerifyResetCodeScreen(
|
||||
verticalArrangement = Arrangement.spacedBy(OrganicSpacing.compact)
|
||||
) {
|
||||
Text(
|
||||
"Didn't receive the code?",
|
||||
stringResource(Res.string.reset_didnt_receive),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.textSecondary
|
||||
)
|
||||
@@ -245,7 +245,7 @@ fun VerifyResetCodeScreen(
|
||||
}
|
||||
|
||||
Text(
|
||||
"Check your spam folder if you don't see it",
|
||||
stringResource(Res.string.reset_check_spam),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.textSecondary,
|
||||
textAlign = TextAlign.Center
|
||||
|
||||
+3
-2
@@ -260,7 +260,7 @@ fun OnboardingCreateAccountContent(
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Error,
|
||||
contentDescription = "Error",
|
||||
contentDescription = stringResource(Res.string.common_error),
|
||||
tint = MaterialTheme.colorScheme.error
|
||||
)
|
||||
Text(
|
||||
@@ -275,13 +275,14 @@ fun OnboardingCreateAccountContent(
|
||||
Spacer(modifier = Modifier.height(OrganicSpacing.sm))
|
||||
|
||||
// Create Account button
|
||||
val passwordsDontMatchMsg = stringResource(Res.string.auth_passwords_dont_match)
|
||||
OrganicPrimaryButton(
|
||||
text = stringResource(Res.string.auth_register_button),
|
||||
onClick = {
|
||||
if (password == confirmPassword) {
|
||||
viewModel.register(username, email, password)
|
||||
} else {
|
||||
localErrorMessage = "Passwords don't match"
|
||||
localErrorMessage = passwordsDontMatchMsg
|
||||
}
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
|
||||
+21
-20
@@ -25,6 +25,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.tt.honeyDue.analytics.AnalyticsEvents
|
||||
import com.tt.honeyDue.analytics.PostHogAnalytics
|
||||
import com.tt.honeyDue.data.LocalDataManager
|
||||
import com.tt.honeyDue.i18n.ClientStrings
|
||||
import com.tt.honeyDue.models.TaskCreateRequest
|
||||
import com.tt.honeyDue.models.TaskSuggestionResponse
|
||||
import com.tt.honeyDue.models.TaskSuggestionsResponse
|
||||
@@ -206,7 +207,7 @@ fun OnboardingFirstTaskContent(
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
Text(
|
||||
text = "$totalSelectedCount task${if (totalSelectedCount == 1) "" else "s"} selected",
|
||||
text = stringResource(Res.string.onboarding_first_task_selected_count, totalSelectedCount),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
@@ -321,7 +322,7 @@ fun OnboardingFirstTaskContent(
|
||||
) {
|
||||
OrganicPrimaryButton(
|
||||
text = if (totalSelectedCount > 0) {
|
||||
"Add $totalSelectedCount Task${if (totalSelectedCount == 1) "" else "s"} & Continue"
|
||||
stringResource(Res.string.onboarding_first_task_add_continue, totalSelectedCount)
|
||||
} else {
|
||||
stringResource(Res.string.onboarding_tasks_skip)
|
||||
},
|
||||
@@ -410,7 +411,7 @@ private fun ForYouTabContent(
|
||||
when (suggestionsState) {
|
||||
is ApiResult.Loading, ApiResult.Idle -> {
|
||||
LoadingPane(
|
||||
message = "Finding tasks for your home...",
|
||||
message = stringResource(Res.string.onboarding_first_task_finding),
|
||||
modifier = modifier
|
||||
)
|
||||
}
|
||||
@@ -418,8 +419,8 @@ private fun ForYouTabContent(
|
||||
val suggestions = suggestionsState.data.suggestions
|
||||
if (suggestions.isEmpty()) {
|
||||
EmptyPane(
|
||||
message = "No personalised suggestions yet — browse the full catalog or skip this step.",
|
||||
primaryLabel = if (hasBrowseFallback) "Browse All" else "Skip",
|
||||
message = stringResource(Res.string.onboarding_first_task_no_suggestions),
|
||||
primaryLabel = if (hasBrowseFallback) stringResource(Res.string.onboarding_first_task_browse_all) else stringResource(Res.string.onboarding_first_task_skip),
|
||||
onPrimary = if (hasBrowseFallback) onSwitchToBrowse else onSkip,
|
||||
modifier = modifier
|
||||
)
|
||||
@@ -444,9 +445,9 @@ private fun ForYouTabContent(
|
||||
}
|
||||
is ApiResult.Error -> {
|
||||
ErrorPane(
|
||||
headline = "Couldn't load your suggestions",
|
||||
headline = stringResource(Res.string.onboarding_first_task_suggestions_error),
|
||||
body = suggestionsState.message.takeIf { it.isNotBlank() }
|
||||
?: "Check your connection and try again.",
|
||||
?: stringResource(Res.string.onboarding_first_task_connection_error),
|
||||
onRetry = onRetry,
|
||||
onSkip = onSkip,
|
||||
modifier = modifier
|
||||
@@ -488,7 +489,7 @@ private fun SuggestionRow(
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
if (isSelected) {
|
||||
Icon(Icons.Default.Check, contentDescription = "Selected", tint = Color.White, modifier = Modifier.size(16.dp) )
|
||||
Icon(Icons.Default.Check, contentDescription = stringResource(Res.string.onboarding_first_task_selected), tint = Color.White, modifier = Modifier.size(16.dp) )
|
||||
}
|
||||
}
|
||||
|
||||
@@ -549,13 +550,13 @@ private fun BrowseTabContent(
|
||||
) {
|
||||
when (templatesGroupedState) {
|
||||
is ApiResult.Loading, ApiResult.Idle -> {
|
||||
LoadingPane(message = "Loading the task catalog...", modifier = modifier)
|
||||
LoadingPane(message = stringResource(Res.string.onboarding_first_task_loading_catalog), modifier = modifier)
|
||||
}
|
||||
is ApiResult.Error -> {
|
||||
ErrorPane(
|
||||
headline = "Couldn't load the task catalog",
|
||||
headline = stringResource(Res.string.onboarding_first_task_catalog_error),
|
||||
body = templatesGroupedState.message.takeIf { it.isNotBlank() }
|
||||
?: "Check your connection and try again.",
|
||||
?: stringResource(Res.string.onboarding_first_task_connection_error),
|
||||
onRetry = onRetry,
|
||||
onSkip = onSkip,
|
||||
modifier = modifier
|
||||
@@ -564,8 +565,8 @@ private fun BrowseTabContent(
|
||||
is ApiResult.Success -> {
|
||||
if (browseCategories.isEmpty()) {
|
||||
EmptyPane(
|
||||
message = "No templates available right now.",
|
||||
primaryLabel = "Skip",
|
||||
message = stringResource(Res.string.onboarding_first_task_no_templates),
|
||||
primaryLabel = stringResource(Res.string.onboarding_first_task_skip),
|
||||
onPrimary = onSkip,
|
||||
modifier = modifier
|
||||
)
|
||||
@@ -653,7 +654,7 @@ private fun TaskCategorySection(
|
||||
|
||||
Icon(
|
||||
imageVector = if (isExpanded) Icons.Default.ExpandLess else Icons.Default.ExpandMore,
|
||||
contentDescription = if (isExpanded) "Collapse" else "Expand",
|
||||
contentDescription = if (isExpanded) stringResource(Res.string.templates_collapse) else stringResource(Res.string.templates_expand),
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
@@ -712,7 +713,7 @@ private fun TaskTemplateRow(
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
if (isSelected) {
|
||||
Icon(Icons.Default.Check, contentDescription = "Selected", tint = Color.White, modifier = Modifier.size(16.dp) )
|
||||
Icon(Icons.Default.Check, contentDescription = stringResource(Res.string.onboarding_first_task_selected), tint = Color.White, modifier = Modifier.size(16.dp) )
|
||||
}
|
||||
}
|
||||
|
||||
@@ -773,7 +774,7 @@ private fun ErrorPane(
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.CloudOff,
|
||||
contentDescription = "Offline",
|
||||
contentDescription = stringResource(Res.string.onboarding_first_task_offline),
|
||||
modifier = Modifier.size(48.dp),
|
||||
tint = MaterialTheme.colorScheme.error
|
||||
)
|
||||
@@ -795,12 +796,12 @@ private fun ErrorPane(
|
||||
Spacer(modifier = Modifier.height(OrganicSpacing.lg))
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.md)) {
|
||||
OutlinedButton(onClick = onSkip) {
|
||||
Text("Skip for now")
|
||||
Text(stringResource(Res.string.onboarding_first_task_skip_for_now))
|
||||
}
|
||||
OutlinedButton(onClick = onRetry) {
|
||||
Icon(Icons.Default.Refresh, contentDescription = null, modifier = Modifier.size(18.dp) ) // decorative
|
||||
Spacer(modifier = Modifier.width(OrganicSpacing.xs))
|
||||
Text("Retry")
|
||||
Text(stringResource(Res.string.common_retry))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -841,7 +842,7 @@ private fun EmptyPane(
|
||||
// ==================== Mapping: server → adapter ====================
|
||||
|
||||
private fun TaskTemplateCategoryGroup.toAdapter(): OnboardingTaskCategory {
|
||||
val categoryName = categoryName.ifBlank { "Uncategorized" }
|
||||
val categoryName = categoryName.ifBlank { ClientStrings.t("task.category.uncategorized") }
|
||||
return OnboardingTaskCategory(
|
||||
id = categoryId ?: stableFallbackIdFor(categoryName),
|
||||
name = categoryName,
|
||||
@@ -852,7 +853,7 @@ private fun TaskTemplateCategoryGroup.toAdapter(): OnboardingTaskCategory {
|
||||
}
|
||||
|
||||
private fun TaskTemplate.toAdapter(categoryName: String): OnboardingTaskTemplate {
|
||||
val label = frequency?.displayName ?: frequency?.name?.replaceFirstChar { it.uppercase() } ?: "One time"
|
||||
val label = frequency?.displayName ?: frequency?.name?.replaceFirstChar { it.uppercase() } ?: ClientStrings.t("task.frequency.one_time")
|
||||
return OnboardingTaskTemplate(
|
||||
id = id,
|
||||
title = title,
|
||||
|
||||
+19
-18
@@ -16,6 +16,7 @@ import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.tt.honeyDue.i18n.ClientStrings
|
||||
import com.tt.honeyDue.models.HomeProfileOptions
|
||||
import com.tt.honeyDue.ui.theme.*
|
||||
import com.tt.honeyDue.viewmodel.OnboardingViewModel
|
||||
@@ -100,7 +101,7 @@ fun OnboardingHomeProfileContent(
|
||||
|
||||
item {
|
||||
OptionDropdownChips(
|
||||
label = "Heating",
|
||||
label = stringResource(Res.string.home_profile_heating),
|
||||
options = HomeProfileOptions.heatingTypes,
|
||||
selectedValue = heatingType,
|
||||
onSelect = { viewModel.setHeatingType(it) }
|
||||
@@ -110,7 +111,7 @@ fun OnboardingHomeProfileContent(
|
||||
|
||||
item {
|
||||
OptionDropdownChips(
|
||||
label = "Cooling",
|
||||
label = stringResource(Res.string.home_profile_cooling),
|
||||
options = HomeProfileOptions.coolingTypes,
|
||||
selectedValue = coolingType,
|
||||
onSelect = { viewModel.setCoolingType(it) }
|
||||
@@ -120,7 +121,7 @@ fun OnboardingHomeProfileContent(
|
||||
|
||||
item {
|
||||
OptionDropdownChips(
|
||||
label = "Water Heater",
|
||||
label = stringResource(Res.string.home_profile_water_heater),
|
||||
options = HomeProfileOptions.waterHeaterTypes,
|
||||
selectedValue = waterHeaterType,
|
||||
onSelect = { viewModel.setWaterHeaterType(it) }
|
||||
@@ -143,13 +144,13 @@ fun OnboardingHomeProfileContent(
|
||||
horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.sm),
|
||||
verticalArrangement = Arrangement.spacedBy(OrganicSpacing.sm)
|
||||
) {
|
||||
ToggleChip(label = "Pool", selected = hasPool, onToggle = { viewModel.setHasPool(!hasPool) })
|
||||
ToggleChip(label = "Sprinkler System", selected = hasSprinklerSystem, onToggle = { viewModel.setHasSprinklerSystem(!hasSprinklerSystem) })
|
||||
ToggleChip(label = "Fireplace", selected = hasFireplace, onToggle = { viewModel.setHasFireplace(!hasFireplace) })
|
||||
ToggleChip(label = "Garage", selected = hasGarage, onToggle = { viewModel.setHasGarage(!hasGarage) })
|
||||
ToggleChip(label = "Basement", selected = hasBasement, onToggle = { viewModel.setHasBasement(!hasBasement) })
|
||||
ToggleChip(label = "Attic", selected = hasAttic, onToggle = { viewModel.setHasAttic(!hasAttic) })
|
||||
ToggleChip(label = "Septic", selected = hasSeptic, onToggle = { viewModel.setHasSeptic(!hasSeptic) })
|
||||
ToggleChip(label = stringResource(Res.string.home_profile_pool), selected = hasPool, onToggle = { viewModel.setHasPool(!hasPool) })
|
||||
ToggleChip(label = stringResource(Res.string.home_profile_sprinkler_system), selected = hasSprinklerSystem, onToggle = { viewModel.setHasSprinklerSystem(!hasSprinklerSystem) })
|
||||
ToggleChip(label = stringResource(Res.string.home_profile_fireplace), selected = hasFireplace, onToggle = { viewModel.setHasFireplace(!hasFireplace) })
|
||||
ToggleChip(label = stringResource(Res.string.home_profile_garage), selected = hasGarage, onToggle = { viewModel.setHasGarage(!hasGarage) })
|
||||
ToggleChip(label = stringResource(Res.string.home_profile_basement), selected = hasBasement, onToggle = { viewModel.setHasBasement(!hasBasement) })
|
||||
ToggleChip(label = stringResource(Res.string.home_profile_attic), selected = hasAttic, onToggle = { viewModel.setHasAttic(!hasAttic) })
|
||||
ToggleChip(label = stringResource(Res.string.home_profile_septic), selected = hasSeptic, onToggle = { viewModel.setHasSeptic(!hasSeptic) })
|
||||
}
|
||||
Spacer(modifier = Modifier.height(OrganicSpacing.xl))
|
||||
}
|
||||
@@ -165,7 +166,7 @@ fun OnboardingHomeProfileContent(
|
||||
|
||||
item {
|
||||
OptionDropdownChips(
|
||||
label = "Roof Type",
|
||||
label = stringResource(Res.string.home_profile_roof_type),
|
||||
options = HomeProfileOptions.roofTypes,
|
||||
selectedValue = roofType,
|
||||
onSelect = { viewModel.setRoofType(it) }
|
||||
@@ -175,7 +176,7 @@ fun OnboardingHomeProfileContent(
|
||||
|
||||
item {
|
||||
OptionDropdownChips(
|
||||
label = "Exterior",
|
||||
label = stringResource(Res.string.home_profile_exterior),
|
||||
options = HomeProfileOptions.exteriorTypes,
|
||||
selectedValue = exteriorType,
|
||||
onSelect = { viewModel.setExteriorType(it) }
|
||||
@@ -194,7 +195,7 @@ fun OnboardingHomeProfileContent(
|
||||
|
||||
item {
|
||||
OptionDropdownChips(
|
||||
label = "Flooring",
|
||||
label = stringResource(Res.string.home_profile_flooring),
|
||||
options = HomeProfileOptions.flooringTypes,
|
||||
selectedValue = flooringPrimary,
|
||||
onSelect = { viewModel.setFlooringPrimary(it) }
|
||||
@@ -204,7 +205,7 @@ fun OnboardingHomeProfileContent(
|
||||
|
||||
item {
|
||||
OptionDropdownChips(
|
||||
label = "Landscaping",
|
||||
label = stringResource(Res.string.home_profile_landscaping),
|
||||
options = HomeProfileOptions.landscapingTypes,
|
||||
selectedValue = landscapingType,
|
||||
onSelect = { viewModel.setLandscapingType(it) }
|
||||
@@ -290,7 +291,7 @@ private fun OptionDropdownChips(
|
||||
horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.xs),
|
||||
verticalArrangement = Arrangement.spacedBy(OrganicSpacing.xs)
|
||||
) {
|
||||
options.forEach { (apiValue, displayLabel) ->
|
||||
options.forEach { (apiValue, displayLabelKey) ->
|
||||
val isSelected = selectedValue == apiValue
|
||||
FilterChip(
|
||||
selected = isSelected,
|
||||
@@ -299,7 +300,7 @@ private fun OptionDropdownChips(
|
||||
},
|
||||
label = {
|
||||
Text(
|
||||
text = displayLabel,
|
||||
text = ClientStrings.t(displayLabelKey),
|
||||
style = MaterialTheme.typography.bodySmall
|
||||
)
|
||||
},
|
||||
@@ -331,7 +332,7 @@ private fun ToggleChip(
|
||||
} else {
|
||||
MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f)
|
||||
},
|
||||
label = "toggleChipColor"
|
||||
label = "toggleChipColor" // i18n-ignore: animation label, non-UI
|
||||
)
|
||||
val contentColor by animateColorAsState(
|
||||
targetValue = if (selected) {
|
||||
@@ -339,7 +340,7 @@ private fun ToggleChip(
|
||||
} else {
|
||||
MaterialTheme.colorScheme.onSurfaceVariant
|
||||
},
|
||||
label = "toggleChipContentColor"
|
||||
label = "toggleChipContentColor" // i18n-ignore: animation label, non-UI
|
||||
)
|
||||
|
||||
FilterChip(
|
||||
|
||||
+2
-2
@@ -139,7 +139,7 @@ fun OnboardingJoinResidenceContent(
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Error,
|
||||
contentDescription = "Error",
|
||||
contentDescription = stringResource(Res.string.common_error),
|
||||
tint = MaterialTheme.colorScheme.error
|
||||
)
|
||||
Text(
|
||||
@@ -163,7 +163,7 @@ fun OnboardingJoinResidenceContent(
|
||||
strokeWidth = 2.dp
|
||||
)
|
||||
Text(
|
||||
text = "Joining residence...",
|
||||
text = stringResource(Res.string.onboarding_joining_residence),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
|
||||
+4
-2
@@ -19,6 +19,8 @@ import com.tt.honeyDue.ui.theme.*
|
||||
import com.tt.honeyDue.viewmodel.OnboardingStep
|
||||
import com.tt.honeyDue.viewmodel.OnboardingViewModel
|
||||
import com.tt.honeyDue.viewmodel.OnboardingIntent
|
||||
import honeydue.composeapp.generated.resources.*
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
|
||||
@OptIn(ExperimentalAnimationApi::class)
|
||||
@Composable
|
||||
@@ -221,7 +223,7 @@ private fun OnboardingNavigationBar(
|
||||
IconButton(onClick = onBack) {
|
||||
Icon(
|
||||
Icons.AutoMirrored.Filled.ArrowBack,
|
||||
contentDescription = "Back",
|
||||
contentDescription = stringResource(Res.string.common_back),
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
@@ -246,7 +248,7 @@ private fun OnboardingNavigationBar(
|
||||
if (showSkipButton) {
|
||||
TextButton(onClick = onSkip) {
|
||||
Text(
|
||||
"Skip",
|
||||
stringResource(Res.string.common_skip),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
|
||||
+3
-3
@@ -299,13 +299,13 @@ fun OnboardingSubscriptionContent(
|
||||
|
||||
// Legal text
|
||||
Text(
|
||||
text = "7-day free trial, then ${if (selectedPlan == SubscriptionPlan.YEARLY) "$23.99/year" else "$2.99/month"}",
|
||||
text = stringResource(Res.string.onboarding_sub_trial_legal, if (selectedPlan == SubscriptionPlan.YEARLY) "$23.99/year" else "$2.99/month"),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
|
||||
Text(
|
||||
text = "Cancel anytime in Settings • No commitment",
|
||||
text = stringResource(Res.string.onboarding_sub_cancel_note),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f)
|
||||
)
|
||||
@@ -352,7 +352,7 @@ private fun BenefitRow(benefit: SubscriptionBenefit) {
|
||||
|
||||
Icon(
|
||||
Icons.Default.Check,
|
||||
contentDescription = "Included",
|
||||
contentDescription = stringResource(Res.string.common_included),
|
||||
modifier = Modifier.size(20.dp),
|
||||
tint = benefit.gradientColors.first()
|
||||
)
|
||||
|
||||
+2
-2
@@ -146,7 +146,7 @@ fun OnboardingVerifyEmailContent(
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Error,
|
||||
contentDescription = "Error",
|
||||
contentDescription = stringResource(Res.string.common_error),
|
||||
tint = MaterialTheme.colorScheme.error
|
||||
)
|
||||
Text(
|
||||
@@ -170,7 +170,7 @@ fun OnboardingVerifyEmailContent(
|
||||
strokeWidth = 2.dp
|
||||
)
|
||||
Text(
|
||||
text = "Verifying...",
|
||||
text = stringResource(Res.string.common_verifying),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
|
||||
+12
-10
@@ -52,6 +52,8 @@ import com.tt.honeyDue.ui.components.common.StandardCard
|
||||
import com.tt.honeyDue.ui.components.forms.FormTextField
|
||||
import com.tt.honeyDue.ui.theme.AppRadius
|
||||
import com.tt.honeyDue.ui.theme.AppSpacing
|
||||
import honeydue.composeapp.generated.resources.*
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
|
||||
/**
|
||||
* Full-screen residence-join UI matching iOS
|
||||
@@ -91,7 +93,7 @@ fun JoinResidenceScreen(
|
||||
TopAppBar(
|
||||
title = {
|
||||
Text(
|
||||
text = "Join Property",
|
||||
text = stringResource(Res.string.join_property_title),
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
)
|
||||
},
|
||||
@@ -102,7 +104,7 @@ fun JoinResidenceScreen(
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||
contentDescription = "Back",
|
||||
contentDescription = stringResource(Res.string.common_back),
|
||||
)
|
||||
}
|
||||
},
|
||||
@@ -129,13 +131,13 @@ fun JoinResidenceScreen(
|
||||
modifier = Modifier.size(72.dp),
|
||||
)
|
||||
Text(
|
||||
text = "Join a Shared Property",
|
||||
text = stringResource(Res.string.join_shared_property_header),
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.onBackground,
|
||||
)
|
||||
Text(
|
||||
text = "Enter the 6-character share code provided by the owner.",
|
||||
text = stringResource(Res.string.join_enter_code_desc),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
@@ -146,12 +148,12 @@ fun JoinResidenceScreen(
|
||||
FormTextField(
|
||||
value = code,
|
||||
onValueChange = { viewModel.updateCode(it) },
|
||||
label = "Share Code",
|
||||
label = stringResource(Res.string.join_share_code_label),
|
||||
modifier = Modifier.testTag(AccessibilityIds.Residence.joinShareCodeField),
|
||||
placeholder = "ABC123",
|
||||
placeholder = stringResource(Res.string.join_share_code_placeholder),
|
||||
enabled = !isLoading,
|
||||
error = error,
|
||||
helperText = if (error == null) "Codes are 6 uppercase characters" else null,
|
||||
helperText = if (error == null) stringResource(Res.string.join_code_helper) else null,
|
||||
keyboardOptions = KeyboardOptions(
|
||||
capitalization = KeyboardCapitalization.Characters,
|
||||
keyboardType = KeyboardType.Ascii,
|
||||
@@ -173,7 +175,7 @@ fun JoinResidenceScreen(
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Error,
|
||||
contentDescription = "Error",
|
||||
contentDescription = stringResource(Res.string.common_error),
|
||||
tint = MaterialTheme.colorScheme.error,
|
||||
)
|
||||
Text(
|
||||
@@ -206,7 +208,7 @@ fun JoinResidenceScreen(
|
||||
)
|
||||
Spacer(modifier = Modifier.size(AppSpacing.sm))
|
||||
Text(
|
||||
text = "Joining…",
|
||||
text = stringResource(Res.string.join_joining),
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
)
|
||||
} else {
|
||||
@@ -216,7 +218,7 @@ fun JoinResidenceScreen(
|
||||
)
|
||||
Spacer(modifier = Modifier.size(AppSpacing.sm))
|
||||
Text(
|
||||
text = "Join Property",
|
||||
text = stringResource(Res.string.join_property_title),
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
)
|
||||
}
|
||||
|
||||
+2
-2
@@ -4,6 +4,7 @@ import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.tt.honeyDue.analytics.AnalyticsEvents
|
||||
import com.tt.honeyDue.analytics.PostHogAnalytics
|
||||
import com.tt.honeyDue.i18n.ClientStrings
|
||||
import com.tt.honeyDue.models.JoinResidenceResponse
|
||||
import com.tt.honeyDue.network.APILayer
|
||||
import com.tt.honeyDue.network.ApiResult
|
||||
@@ -69,7 +70,7 @@ class JoinResidenceViewModel(
|
||||
*/
|
||||
fun submit() {
|
||||
if (_code.value.length != REQUIRED_LENGTH) {
|
||||
_errorMessage.value = ERROR_LENGTH
|
||||
_errorMessage.value = ClientStrings.t("validation.share_code_length")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -108,6 +109,5 @@ class JoinResidenceViewModel(
|
||||
|
||||
companion object {
|
||||
const val REQUIRED_LENGTH = 6
|
||||
const val ERROR_LENGTH = "Share code must be 6 characters"
|
||||
}
|
||||
}
|
||||
|
||||
+10
-9
@@ -1,5 +1,6 @@
|
||||
package com.tt.honeyDue.ui.screens.residence
|
||||
|
||||
import com.tt.honeyDue.i18n.ClientStrings
|
||||
import kotlin.time.Clock
|
||||
import kotlin.time.ExperimentalTime
|
||||
import kotlinx.datetime.Instant
|
||||
@@ -34,9 +35,9 @@ object ResidenceFormValidation {
|
||||
private const val YEAR_BUILT_MIN = 1800
|
||||
|
||||
fun validateName(value: String): String? {
|
||||
if (value.isBlank()) return "Name is required"
|
||||
if (value.isBlank()) return ClientStrings.t("validation.name_required")
|
||||
if (value.length > NAME_MAX_LENGTH) {
|
||||
return "Name must be $NAME_MAX_LENGTH characters or fewer"
|
||||
return ClientStrings.t("validation.name_too_long", NAME_MAX_LENGTH)
|
||||
}
|
||||
return null
|
||||
}
|
||||
@@ -49,7 +50,7 @@ object ResidenceFormValidation {
|
||||
if (value.isBlank()) return null
|
||||
val n = value.toIntOrNull()
|
||||
if (n == null || n < 0) {
|
||||
return "Bedrooms must be a non-negative whole number"
|
||||
return ClientStrings.t("validation.bedrooms_invalid")
|
||||
}
|
||||
return null
|
||||
}
|
||||
@@ -59,7 +60,7 @@ object ResidenceFormValidation {
|
||||
if (value.isBlank()) return null
|
||||
val n = value.toDoubleOrNull()
|
||||
if (n == null || n < 0.0) {
|
||||
return "Bathrooms must be a non-negative number"
|
||||
return ClientStrings.t("validation.bathrooms_invalid")
|
||||
}
|
||||
return null
|
||||
}
|
||||
@@ -69,7 +70,7 @@ object ResidenceFormValidation {
|
||||
if (value.isBlank()) return null
|
||||
val n = value.toIntOrNull()
|
||||
if (n == null || n <= 0) {
|
||||
return "Square footage must be a positive whole number"
|
||||
return ClientStrings.t("validation.sqft_invalid")
|
||||
}
|
||||
return null
|
||||
}
|
||||
@@ -79,7 +80,7 @@ object ResidenceFormValidation {
|
||||
if (value.isBlank()) return null
|
||||
val n = value.toDoubleOrNull()
|
||||
if (n == null || n <= 0.0) {
|
||||
return "Lot size must be a positive number"
|
||||
return ClientStrings.t("validation.lot_size_invalid")
|
||||
}
|
||||
return null
|
||||
}
|
||||
@@ -91,10 +92,10 @@ object ResidenceFormValidation {
|
||||
*/
|
||||
fun validateYearBuilt(value: String, currentYear: Int = defaultCurrentYear()): String? {
|
||||
if (value.isBlank()) return null
|
||||
if (value.length != 4) return "Year built must be a 4-digit year"
|
||||
val n = value.toIntOrNull() ?: return "Year built must be a 4-digit year"
|
||||
if (value.length != 4) return ClientStrings.t("validation.year_built_format")
|
||||
val n = value.toIntOrNull() ?: return ClientStrings.t("validation.year_built_format")
|
||||
if (n < YEAR_BUILT_MIN || n > currentYear) {
|
||||
return "Year built must be between $YEAR_BUILT_MIN and the current year"
|
||||
return ClientStrings.t("validation.year_built_range", YEAR_BUILT_MIN)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
+50
-25
@@ -44,6 +44,8 @@ import com.tt.honeyDue.data.LocalDataManager
|
||||
import com.tt.honeyDue.models.FeatureBenefit
|
||||
import com.tt.honeyDue.ui.components.common.StandardCard
|
||||
import com.tt.honeyDue.ui.theme.AppSpacing
|
||||
import honeydue.composeapp.generated.resources.*
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
|
||||
/**
|
||||
* FeatureComparisonScreen — full-screen Free vs. Pro comparison matching
|
||||
@@ -82,7 +84,7 @@ fun FeatureComparisonScreen(
|
||||
TopAppBar(
|
||||
title = {
|
||||
Text(
|
||||
text = "Choose Your Plan",
|
||||
text = stringResource(Res.string.paywall_choose_plan_title),
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
)
|
||||
},
|
||||
@@ -92,7 +94,7 @@ fun FeatureComparisonScreen(
|
||||
}) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||
contentDescription = "Close",
|
||||
contentDescription = stringResource(Res.string.common_close),
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
}
|
||||
@@ -117,13 +119,13 @@ fun FeatureComparisonScreen(
|
||||
verticalArrangement = Arrangement.spacedBy(AppSpacing.sm),
|
||||
) {
|
||||
Text(
|
||||
text = "Choose Your Plan",
|
||||
text = stringResource(Res.string.paywall_choose_plan_title),
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.onBackground,
|
||||
)
|
||||
Text(
|
||||
text = "Upgrade to Pro for unlimited access",
|
||||
text = stringResource(Res.string.paywall_choose_plan_subtitle),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
textAlign = TextAlign.Center,
|
||||
@@ -140,9 +142,9 @@ fun FeatureComparisonScreen(
|
||||
|
||||
rows.forEachIndexed { index, row ->
|
||||
ComparisonRow(
|
||||
featureName = row.featureName,
|
||||
freeText = row.freeTierText,
|
||||
proText = row.proTierText,
|
||||
featureName = localizedFeatureText(row.featureName),
|
||||
freeText = localizedFeatureText(row.freeTierText),
|
||||
proText = localizedFeatureText(row.proTierText),
|
||||
freeHas = FeatureComparisonScreenState.freeHasFeature(row),
|
||||
proHas = FeatureComparisonScreenState.premiumHasFeature(row),
|
||||
)
|
||||
@@ -171,7 +173,7 @@ fun FeatureComparisonScreen(
|
||||
),
|
||||
) {
|
||||
Text(
|
||||
text = "Upgrade to Pro",
|
||||
text = stringResource(Res.string.paywall_upgrade_to_pro),
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.onPrimary,
|
||||
)
|
||||
@@ -195,14 +197,14 @@ private fun ComparisonHeaderRow() {
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
text = "Feature",
|
||||
text = stringResource(Res.string.paywall_col_feature),
|
||||
modifier = Modifier.weight(1f),
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
Text(
|
||||
text = "Free",
|
||||
text = stringResource(Res.string.paywall_col_free),
|
||||
modifier = Modifier.width(80.dp),
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
fontWeight = FontWeight.Bold,
|
||||
@@ -210,7 +212,7 @@ private fun ComparisonHeaderRow() {
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
Text(
|
||||
text = "Pro",
|
||||
text = stringResource(Res.string.paywall_col_pro),
|
||||
modifier = Modifier.width(80.dp),
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
fontWeight = FontWeight.Bold,
|
||||
@@ -283,7 +285,7 @@ private fun TierCell(
|
||||
) {
|
||||
Icon(
|
||||
imageVector = if (hasFeature) Icons.Default.Check else Icons.Default.Close,
|
||||
contentDescription = if (hasFeature) "Included" else "Not included",
|
||||
contentDescription = if (hasFeature) stringResource(Res.string.common_included) else stringResource(Res.string.paywall_feat_not_included),
|
||||
modifier = Modifier.size(18.dp),
|
||||
tint = if (hasFeature) {
|
||||
if (emphasize) MaterialTheme.colorScheme.primary
|
||||
@@ -306,6 +308,29 @@ private fun TierCell(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps the known default-row English values to their localized
|
||||
* [stringResource]. Server-driven benefit text (already localized by the
|
||||
* API) and any unrecognized value falls through unchanged.
|
||||
*
|
||||
* IMPORTANT: this only affects DISPLAY. The "Not available" sentinel
|
||||
* comparison in [FeatureComparisonScreenState.isUnavailable] runs against
|
||||
* the raw, non-localized [FeatureBenefit] fields — so localizing the
|
||||
* display here does not break the free/pro availability logic.
|
||||
*/
|
||||
@Composable
|
||||
private fun localizedFeatureText(raw: String): String = when (raw) {
|
||||
"Properties" -> stringResource(Res.string.paywall_feat_properties) // i18n-ignore: stable English lookup key for localizedFeatureText, non-UI
|
||||
"Tasks" -> stringResource(Res.string.paywall_feat_tasks) // i18n-ignore: stable English lookup key for localizedFeatureText, non-UI
|
||||
"Contractors" -> stringResource(Res.string.paywall_feat_contractors) // i18n-ignore: stable English lookup key for localizedFeatureText, non-UI
|
||||
"Documents" -> stringResource(Res.string.paywall_feat_documents) // i18n-ignore: stable English lookup key for localizedFeatureText, non-UI
|
||||
"1 property" -> stringResource(Res.string.paywall_val_1_property) // i18n-ignore: stable English lookup key for localizedFeatureText, non-UI
|
||||
"10 tasks" -> stringResource(Res.string.paywall_val_10_tasks) // i18n-ignore: stable English lookup key for localizedFeatureText, non-UI
|
||||
"Unlimited" -> stringResource(Res.string.paywall_val_unlimited) // i18n-ignore: stable English lookup key for localizedFeatureText, non-UI
|
||||
"Not available" -> stringResource(Res.string.paywall_val_not_available) // i18n-ignore: stable English lookup key for localizedFeatureText, non-UI
|
||||
else -> raw
|
||||
}
|
||||
|
||||
/**
|
||||
* State helper for [FeatureComparisonScreen].
|
||||
*
|
||||
@@ -332,24 +357,24 @@ object FeatureComparisonScreenState {
|
||||
|
||||
fun defaultFeatureRows(): List<FeatureBenefit> = listOf(
|
||||
FeatureBenefit(
|
||||
featureName = "Properties",
|
||||
freeTierText = "1 property",
|
||||
proTierText = "Unlimited",
|
||||
featureName = "Properties", // i18n-ignore: stable sentinel; displayed via localizedFeatureText
|
||||
freeTierText = "1 property", // i18n-ignore: stable sentinel; displayed via localizedFeatureText
|
||||
proTierText = "Unlimited", // i18n-ignore: stable sentinel; displayed via localizedFeatureText
|
||||
),
|
||||
FeatureBenefit(
|
||||
featureName = "Tasks",
|
||||
freeTierText = "10 tasks",
|
||||
proTierText = "Unlimited",
|
||||
featureName = "Tasks", // i18n-ignore: stable sentinel; displayed via localizedFeatureText
|
||||
freeTierText = "10 tasks", // i18n-ignore: stable sentinel; displayed via localizedFeatureText
|
||||
proTierText = "Unlimited", // i18n-ignore: stable sentinel; displayed via localizedFeatureText
|
||||
),
|
||||
FeatureBenefit(
|
||||
featureName = "Contractors",
|
||||
freeTierText = "Not available",
|
||||
proTierText = "Unlimited",
|
||||
featureName = "Contractors", // i18n-ignore: stable sentinel; displayed via localizedFeatureText
|
||||
freeTierText = "Not available", // i18n-ignore: stable sentinel; displayed via localizedFeatureText
|
||||
proTierText = "Unlimited", // i18n-ignore: stable sentinel; displayed via localizedFeatureText
|
||||
),
|
||||
FeatureBenefit(
|
||||
featureName = "Documents",
|
||||
freeTierText = "Not available",
|
||||
proTierText = "Unlimited",
|
||||
featureName = "Documents", // i18n-ignore: stable sentinel; displayed via localizedFeatureText
|
||||
freeTierText = "Not available", // i18n-ignore: stable sentinel; displayed via localizedFeatureText
|
||||
proTierText = "Unlimited", // i18n-ignore: stable sentinel; displayed via localizedFeatureText
|
||||
),
|
||||
)
|
||||
|
||||
@@ -364,7 +389,7 @@ object FeatureComparisonScreenState {
|
||||
!isUnavailable(benefit.proTierText)
|
||||
|
||||
private fun isUnavailable(text: String): Boolean =
|
||||
text.trim().equals("Not available", ignoreCase = true)
|
||||
text.trim().equals("Not available", ignoreCase = true) // i18n-ignore: availability sentinel; compared raw, must stay English
|
||||
|
||||
/**
|
||||
* CTA handler. Fires the paywall analytics event and navigates to
|
||||
|
||||
+16
-14
@@ -45,6 +45,8 @@ import com.tt.honeyDue.ui.components.forms.FormTextField
|
||||
import com.tt.honeyDue.ui.theme.AppRadius
|
||||
import com.tt.honeyDue.ui.theme.AppSpacing
|
||||
import com.tt.honeyDue.util.ErrorMessageParser
|
||||
import honeydue.composeapp.generated.resources.*
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
|
||||
/**
|
||||
* Android port of iOS AddTaskWithResidenceView (P2 Stream I).
|
||||
@@ -88,13 +90,13 @@ fun AddTaskWithResidenceScreen(
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text("New Task", fontWeight = FontWeight.SemiBold) },
|
||||
title = { Text(stringResource(Res.string.tasks_new_title), fontWeight = FontWeight.SemiBold) },
|
||||
navigationIcon = {
|
||||
IconButton(
|
||||
onClick = onNavigateBack,
|
||||
modifier = Modifier.testTag(AccessibilityIds.Task.formCancelButton)
|
||||
) {
|
||||
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back")
|
||||
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = stringResource(Res.string.a11y_back))
|
||||
}
|
||||
},
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
@@ -116,9 +118,9 @@ fun AddTaskWithResidenceScreen(
|
||||
FormTextField(
|
||||
value = title,
|
||||
onValueChange = viewModel::onTitleChange,
|
||||
label = "Title",
|
||||
label = stringResource(Res.string.tasks_title_field_label),
|
||||
modifier = Modifier.testTag(AccessibilityIds.Task.titleField),
|
||||
placeholder = "e.g. Flush water heater",
|
||||
placeholder = stringResource(Res.string.tasks_title_placeholder),
|
||||
error = titleError,
|
||||
enabled = !isSubmitting
|
||||
)
|
||||
@@ -128,9 +130,9 @@ fun AddTaskWithResidenceScreen(
|
||||
FormTextField(
|
||||
value = description,
|
||||
onValueChange = viewModel::onDescriptionChange,
|
||||
label = "Description",
|
||||
label = stringResource(Res.string.tasks_description_label),
|
||||
modifier = Modifier.testTag(AccessibilityIds.Task.descriptionField),
|
||||
placeholder = "Optional details",
|
||||
placeholder = stringResource(Res.string.tasks_description_placeholder),
|
||||
singleLine = false,
|
||||
maxLines = 4,
|
||||
enabled = !isSubmitting
|
||||
@@ -140,7 +142,7 @@ fun AddTaskWithResidenceScreen(
|
||||
// Priority chips
|
||||
StandardCard(modifier = Modifier.fillMaxWidth()) {
|
||||
Text(
|
||||
text = "Priority",
|
||||
text = stringResource(Res.string.tasks_priority_label),
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
@@ -165,7 +167,7 @@ fun AddTaskWithResidenceScreen(
|
||||
// Category chips
|
||||
StandardCard(modifier = Modifier.fillMaxWidth()) {
|
||||
Text(
|
||||
text = "Category",
|
||||
text = stringResource(Res.string.tasks_category_label),
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
@@ -189,7 +191,7 @@ fun AddTaskWithResidenceScreen(
|
||||
// Frequency chips
|
||||
StandardCard(modifier = Modifier.fillMaxWidth()) {
|
||||
Text(
|
||||
text = "Frequency",
|
||||
text = stringResource(Res.string.tasks_frequency_label),
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
@@ -215,10 +217,10 @@ fun AddTaskWithResidenceScreen(
|
||||
FormTextField(
|
||||
value = dueDate,
|
||||
onValueChange = viewModel::onDueDateChange,
|
||||
label = "Due date (optional)",
|
||||
placeholder = "yyyy-MM-dd",
|
||||
label = stringResource(Res.string.tasks_due_date_optional_label),
|
||||
placeholder = stringResource(Res.string.tasks_due_date_placeholder_format),
|
||||
enabled = !isSubmitting,
|
||||
helperText = "Leave blank for no due date"
|
||||
helperText = stringResource(Res.string.tasks_due_date_blank_helper)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -227,7 +229,7 @@ fun AddTaskWithResidenceScreen(
|
||||
FormTextField(
|
||||
value = estimatedCost,
|
||||
onValueChange = viewModel::onEstimatedCostChange,
|
||||
label = "Estimated cost (optional)",
|
||||
label = stringResource(Res.string.tasks_estimated_cost_optional_label),
|
||||
placeholder = "0.00",
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal),
|
||||
enabled = !isSubmitting
|
||||
@@ -265,7 +267,7 @@ fun AddTaskWithResidenceScreen(
|
||||
} else {
|
||||
Icon(Icons.Default.Save, contentDescription = null) // decorative
|
||||
Text(
|
||||
text = "Create Task",
|
||||
text = stringResource(Res.string.tasks_create),
|
||||
modifier = Modifier.padding(start = AppSpacing.sm),
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
|
||||
+2
-1
@@ -2,6 +2,7 @@ package com.tt.honeyDue.ui.screens.task
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.tt.honeyDue.i18n.ClientStrings
|
||||
import com.tt.honeyDue.models.TaskCreateRequest
|
||||
import com.tt.honeyDue.models.TaskResponse
|
||||
import com.tt.honeyDue.network.APILayer
|
||||
@@ -105,7 +106,7 @@ class AddTaskWithResidenceViewModel(
|
||||
fun submit(onSuccess: () -> Unit) {
|
||||
val currentTitle = _title.value.trim()
|
||||
if (currentTitle.isEmpty()) {
|
||||
_titleError.value = "Title is required"
|
||||
_titleError.value = ClientStrings.t("validation.title_required")
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
+8
-6
@@ -1,5 +1,7 @@
|
||||
package com.tt.honeyDue.ui.screens.task
|
||||
|
||||
import com.tt.honeyDue.i18n.ClientStrings
|
||||
|
||||
/**
|
||||
* Pure-function validators for the task create/edit form.
|
||||
*
|
||||
@@ -15,16 +17,16 @@ package com.tt.honeyDue.ui.screens.task
|
||||
object TaskFormValidation {
|
||||
|
||||
fun validateTitle(value: String): String? =
|
||||
if (value.isBlank()) "Title is required" else null
|
||||
if (value.isBlank()) ClientStrings.t("validation.title_required") else null
|
||||
|
||||
fun validatePriorityId(value: Int?): String? =
|
||||
if (value == null) "Please select a priority" else null
|
||||
if (value == null) ClientStrings.t("validation.priority_required") else null
|
||||
|
||||
fun validateCategoryId(value: Int?): String? =
|
||||
if (value == null) "Please select a category" else null
|
||||
if (value == null) ClientStrings.t("validation.category_required") else null
|
||||
|
||||
fun validateFrequencyId(value: Int?): String? =
|
||||
if (value == null) "Please select a frequency" else null
|
||||
if (value == null) ClientStrings.t("validation.frequency_required") else null
|
||||
|
||||
/**
|
||||
* Estimated cost is optional — an empty/blank string is valid. A non-empty
|
||||
@@ -35,12 +37,12 @@ object TaskFormValidation {
|
||||
fun validateEstimatedCost(value: String): String? {
|
||||
if (value.isBlank()) return null
|
||||
return if (value.toDoubleOrNull() == null) {
|
||||
"Estimated cost must be a valid number"
|
||||
ClientStrings.t("validation.est_cost_invalid")
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun validateResidenceId(value: Int?): String? =
|
||||
if (value == null) "Property is required" else null
|
||||
if (value == null) ClientStrings.t("validation.property_required") else null
|
||||
}
|
||||
|
||||
+12
-10
@@ -51,6 +51,8 @@ import com.tt.honeyDue.ui.components.common.StandardCard
|
||||
import com.tt.honeyDue.ui.theme.AppRadius
|
||||
import com.tt.honeyDue.ui.theme.AppSpacing
|
||||
import com.tt.honeyDue.util.ErrorMessageParser
|
||||
import honeydue.composeapp.generated.resources.*
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
|
||||
/**
|
||||
* Standalone screen that lets users pick personalized task suggestions
|
||||
@@ -104,18 +106,18 @@ fun TaskSuggestionsScreen(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = {
|
||||
Text(text = "Suggested Tasks", fontWeight = FontWeight.SemiBold)
|
||||
Text(text = stringResource(Res.string.suggestions_title), fontWeight = FontWeight.SemiBold)
|
||||
},
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onNavigateBack) {
|
||||
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back")
|
||||
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = stringResource(Res.string.a11y_back))
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
OutlinedButton(
|
||||
onClick = onNavigateBack,
|
||||
modifier = Modifier.padding(end = AppSpacing.md)
|
||||
) { Text("Skip") }
|
||||
) { Text(stringResource(Res.string.suggestions_skip)) }
|
||||
},
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = MaterialTheme.colorScheme.surface
|
||||
@@ -173,7 +175,7 @@ fun TaskSuggestionsScreen(
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.ErrorOutline,
|
||||
contentDescription = "Error",
|
||||
contentDescription = stringResource(Res.string.common_error),
|
||||
tint = MaterialTheme.colorScheme.error
|
||||
)
|
||||
Text(
|
||||
@@ -275,7 +277,7 @@ private fun SuggestionRow(
|
||||
} else {
|
||||
Icon(Icons.Default.Check, contentDescription = null) // decorative
|
||||
Spacer(Modifier.width(AppSpacing.sm))
|
||||
Text("Accept", fontWeight = FontWeight.SemiBold)
|
||||
Text(stringResource(Res.string.suggestions_accept), fontWeight = FontWeight.SemiBold)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -291,17 +293,17 @@ private fun ErrorView(message: String, onRetry: () -> Unit) {
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.ErrorOutline,
|
||||
contentDescription = "Error",
|
||||
contentDescription = stringResource(Res.string.common_error),
|
||||
modifier = Modifier.size(48.dp),
|
||||
tint = MaterialTheme.colorScheme.error
|
||||
)
|
||||
Text(text = "Couldn't load suggestions", style = MaterialTheme.typography.titleMedium)
|
||||
Text(text = stringResource(Res.string.suggestions_load_failed), style = MaterialTheme.typography.titleMedium)
|
||||
Text(
|
||||
text = message,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
OutlinedButton(onClick = onRetry) { Text("Retry") }
|
||||
OutlinedButton(onClick = onRetry) { Text(stringResource(Res.string.common_retry)) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -318,9 +320,9 @@ private fun EmptyView() {
|
||||
modifier = Modifier.size(48.dp),
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f)
|
||||
)
|
||||
Text(text = "No suggestions yet", style = MaterialTheme.typography.titleMedium)
|
||||
Text(text = stringResource(Res.string.suggestions_empty_title), style = MaterialTheme.typography.titleMedium)
|
||||
Text(
|
||||
text = "Complete your home profile to see personalized recommendations.",
|
||||
text = stringResource(Res.string.suggestions_empty_subtitle),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
|
||||
+6
-3
@@ -49,6 +49,9 @@ import com.tt.honeyDue.ui.theme.AppSpacing
|
||||
import com.tt.honeyDue.util.ErrorMessageParser
|
||||
import honeydue.composeapp.generated.resources.Res
|
||||
import honeydue.composeapp.generated.resources.common_back
|
||||
import honeydue.composeapp.generated.resources.common_error
|
||||
import honeydue.composeapp.generated.resources.common_selected
|
||||
import honeydue.composeapp.generated.resources.common_not_selected
|
||||
import honeydue.composeapp.generated.resources.templates_all_categories
|
||||
import honeydue.composeapp.generated.resources.templates_apply_count
|
||||
import honeydue.composeapp.generated.resources.templates_create_failed
|
||||
@@ -386,7 +389,7 @@ private fun TemplateCard(
|
||||
Icon(
|
||||
imageVector = if (selected) Icons.Default.CheckCircle
|
||||
else Icons.Default.RadioButtonUnchecked,
|
||||
contentDescription = if (selected) "Selected" else "Not selected",
|
||||
contentDescription = if (selected) stringResource(Res.string.common_selected) else stringResource(Res.string.common_not_selected),
|
||||
tint = if (selected) MaterialTheme.colorScheme.primary
|
||||
else MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier
|
||||
@@ -452,7 +455,7 @@ private fun LoadErrorView(
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.ErrorOutline,
|
||||
contentDescription = "Error",
|
||||
contentDescription = stringResource(Res.string.common_error),
|
||||
modifier = Modifier.size(48.dp),
|
||||
tint = MaterialTheme.colorScheme.error
|
||||
)
|
||||
@@ -560,7 +563,7 @@ private fun categoryBubbleColor(category: String): Color = when (category.lowerc
|
||||
"hvac" -> Color(0xFF07A0C3)
|
||||
"appliances" -> Color(0xFF7B1FA2)
|
||||
"exterior" -> Color(0xFF34C759)
|
||||
"lawn & garden" -> Color(0xFF2E7D32)
|
||||
"lawn & garden" -> Color(0xFF2E7D32) // i18n-ignore: category key color lookup, non-UI
|
||||
"interior" -> Color(0xFFAF52DE)
|
||||
"general", "seasonal" -> Color(0xFFFF9500)
|
||||
else -> Color(0xFF455A64)
|
||||
|
||||
+53
-11
@@ -54,6 +54,8 @@ import com.tt.honeyDue.ui.theme.AppThemes
|
||||
import com.tt.honeyDue.ui.theme.ThemeColors
|
||||
import com.tt.honeyDue.ui.theme.ThemeManager
|
||||
import com.tt.honeyDue.ui.theme.isDynamicColorSupported
|
||||
import honeydue.composeapp.generated.resources.*
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
|
||||
/**
|
||||
* ThemeSelectionScreen — full-screen theme picker matching iOS
|
||||
@@ -88,7 +90,7 @@ fun ThemeSelectionScreen(
|
||||
TopAppBar(
|
||||
title = {
|
||||
Text(
|
||||
text = "Appearance",
|
||||
text = stringResource(Res.string.theme_appearance_title),
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
)
|
||||
},
|
||||
@@ -98,7 +100,7 @@ fun ThemeSelectionScreen(
|
||||
}) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||
contentDescription = "Back",
|
||||
contentDescription = stringResource(Res.string.common_back),
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
}
|
||||
@@ -108,7 +110,7 @@ fun ThemeSelectionScreen(
|
||||
ThemeSelectionScreenState.onConfirm(onBack = onNavigateBack)
|
||||
}) {
|
||||
Text(
|
||||
text = "Done",
|
||||
text = stringResource(Res.string.common_done),
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
@@ -196,19 +198,19 @@ private fun LivePreviewHeader(theme: ThemeColors) {
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Text(
|
||||
text = "Aa",
|
||||
text = "Aa", // i18n-ignore: font preview glyph, non-translatable
|
||||
color = textOnPrimary,
|
||||
fontWeight = FontWeight.Bold,
|
||||
)
|
||||
}
|
||||
Column {
|
||||
Text(
|
||||
text = theme.displayName,
|
||||
text = themeDisplayName(theme.id),
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = textPrimary,
|
||||
)
|
||||
Text(
|
||||
text = theme.description,
|
||||
text = themeDescription(theme.id),
|
||||
color = textSecondary,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
)
|
||||
@@ -241,12 +243,12 @@ private fun DynamicColorToggleRow(
|
||||
) {
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = "Use system colors",
|
||||
text = stringResource(Res.string.theme_use_system_colors),
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
Text(
|
||||
text = "Follow Android 12+ Material You (wallpaper colors)",
|
||||
text = stringResource(Res.string.theme_material_you_desc),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
@@ -309,12 +311,12 @@ private fun ThemeRowCard(
|
||||
verticalArrangement = Arrangement.spacedBy(AppSpacing.xs),
|
||||
) {
|
||||
Text(
|
||||
text = theme.displayName,
|
||||
text = themeDisplayName(theme.id),
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
Text(
|
||||
text = theme.description,
|
||||
text = themeDescription(theme.id),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
@@ -334,7 +336,7 @@ private fun ThemeRowCard(
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Check,
|
||||
contentDescription = "Selected",
|
||||
contentDescription = stringResource(Res.string.common_selected),
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.size(16.dp),
|
||||
)
|
||||
@@ -407,3 +409,43 @@ object ThemeSelectionScreenState {
|
||||
onBack()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves a theme's localized display name from its stable [ThemeColors.id].
|
||||
* The English [ThemeColors.displayName] remains a stable iOS-parity identifier;
|
||||
* only the rendered label is localized here.
|
||||
*/
|
||||
@Composable
|
||||
fun themeDisplayName(themeId: String): String = when (themeId) {
|
||||
"default" -> stringResource(Res.string.theme_default)
|
||||
"teal" -> stringResource(Res.string.theme_teal)
|
||||
"ocean" -> stringResource(Res.string.theme_ocean)
|
||||
"forest" -> stringResource(Res.string.theme_forest)
|
||||
"sunset" -> stringResource(Res.string.theme_sunset)
|
||||
"monochrome" -> stringResource(Res.string.theme_monochrome)
|
||||
"lavender" -> stringResource(Res.string.theme_lavender)
|
||||
"crimson" -> stringResource(Res.string.theme_crimson)
|
||||
"midnight" -> stringResource(Res.string.theme_midnight)
|
||||
"desert" -> stringResource(Res.string.theme_desert)
|
||||
"mint" -> stringResource(Res.string.theme_mint)
|
||||
else -> themeId
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves a theme's localized description from its stable [ThemeColors.id].
|
||||
*/
|
||||
@Composable
|
||||
fun themeDescription(themeId: String): String = when (themeId) {
|
||||
"default" -> stringResource(Res.string.theme_default_desc)
|
||||
"teal" -> stringResource(Res.string.theme_teal_desc)
|
||||
"ocean" -> stringResource(Res.string.theme_ocean_desc)
|
||||
"forest" -> stringResource(Res.string.theme_forest_desc)
|
||||
"sunset" -> stringResource(Res.string.theme_sunset_desc)
|
||||
"monochrome" -> stringResource(Res.string.theme_monochrome_desc)
|
||||
"lavender" -> stringResource(Res.string.theme_lavender_desc)
|
||||
"crimson" -> stringResource(Res.string.theme_crimson_desc)
|
||||
"midnight" -> stringResource(Res.string.theme_midnight_desc)
|
||||
"desert" -> stringResource(Res.string.theme_desert_desc)
|
||||
"mint" -> stringResource(Res.string.theme_mint_desc)
|
||||
else -> ""
|
||||
}
|
||||
|
||||
+21
-19
@@ -19,6 +19,8 @@ import com.tt.honeyDue.ui.screens.subscription.FeatureComparisonScreen
|
||||
import com.tt.honeyDue.ui.theme.AppRadius
|
||||
import com.tt.honeyDue.ui.theme.AppSpacing
|
||||
import com.tt.honeyDue.utils.SubscriptionProducts
|
||||
import honeydue.composeapp.generated.resources.*
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
|
||||
/**
|
||||
* Full inline paywall screen for upgrade prompts.
|
||||
@@ -42,9 +44,9 @@ fun UpgradeFeatureScreen(
|
||||
} }
|
||||
|
||||
// Fallback values if trigger not found
|
||||
val title = triggerData?.title ?: "Upgrade Required"
|
||||
val message = triggerData?.message ?: "This feature is available with a Pro subscription."
|
||||
val buttonText = triggerData?.buttonText ?: "Upgrade to Pro"
|
||||
val title = triggerData?.title ?: stringResource(Res.string.upgrade_feature_required_title)
|
||||
val message = triggerData?.message ?: stringResource(Res.string.upgrade_feature_required_message)
|
||||
val buttonText = triggerData?.buttonText ?: stringResource(Res.string.profile_upgrade_to_pro)
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
@@ -52,7 +54,7 @@ fun UpgradeFeatureScreen(
|
||||
title = { Text(title, fontWeight = FontWeight.SemiBold) },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onNavigateBack) {
|
||||
Icon(Icons.AutoMirrored.Filled.ArrowBack, "Back")
|
||||
Icon(Icons.AutoMirrored.Filled.ArrowBack, stringResource(Res.string.a11y_back))
|
||||
}
|
||||
},
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
@@ -114,10 +116,10 @@ fun UpgradeFeatureScreen(
|
||||
modifier = Modifier.padding(AppSpacing.lg),
|
||||
verticalArrangement = Arrangement.spacedBy(AppSpacing.md)
|
||||
) {
|
||||
FeatureRow(Icons.Default.Home, "Unlimited properties")
|
||||
FeatureRow(Icons.Default.CheckCircle, "Unlimited tasks")
|
||||
FeatureRow(Icons.Default.People, "Contractor management")
|
||||
FeatureRow(Icons.Default.Description, "Document & warranty storage")
|
||||
FeatureRow(Icons.Default.Home, stringResource(Res.string.upgrade_feature_unlimited_properties))
|
||||
FeatureRow(Icons.Default.CheckCircle, stringResource(Res.string.upgrade_feature_unlimited_tasks))
|
||||
FeatureRow(Icons.Default.People, stringResource(Res.string.upgrade_feature_contractor_management))
|
||||
FeatureRow(Icons.Default.Description, stringResource(Res.string.upgrade_feature_document_warranty_storage))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -157,7 +159,7 @@ fun UpgradeFeatureScreen(
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Warning,
|
||||
contentDescription = "Warning",
|
||||
contentDescription = stringResource(Res.string.upgrade_warning),
|
||||
tint = MaterialTheme.colorScheme.error
|
||||
)
|
||||
Text(
|
||||
@@ -173,7 +175,7 @@ fun UpgradeFeatureScreen(
|
||||
|
||||
// Compare Plans
|
||||
TextButton(onClick = { showFeatureComparison = true }) {
|
||||
Text("Compare Free vs Pro")
|
||||
Text(stringResource(Res.string.upgrade_compare_free_vs_pro))
|
||||
}
|
||||
|
||||
// Restore Purchases
|
||||
@@ -183,7 +185,7 @@ fun UpgradeFeatureScreen(
|
||||
errorMessage = null
|
||||
}) {
|
||||
Text(
|
||||
"Restore Purchases",
|
||||
stringResource(Res.string.upgrade_restore_purchases),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
@@ -205,14 +207,14 @@ fun UpgradeFeatureScreen(
|
||||
if (showSuccessAlert) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { showSuccessAlert = false },
|
||||
title = { Text("Subscription Active") },
|
||||
text = { Text("You now have full access to all Pro features!") },
|
||||
title = { Text(stringResource(Res.string.upgrade_subscription_active)) },
|
||||
text = { Text(stringResource(Res.string.upgrade_subscription_active_message)) },
|
||||
confirmButton = {
|
||||
TextButton(onClick = {
|
||||
showSuccessAlert = false
|
||||
onNavigateBack()
|
||||
}) {
|
||||
Text("Done")
|
||||
Text(stringResource(Res.string.common_done))
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -256,9 +258,9 @@ private fun SubscriptionProductsSection(
|
||||
// Monthly Option
|
||||
SubscriptionProductCard(
|
||||
productId = SubscriptionProducts.MONTHLY,
|
||||
name = "honeyDue Pro Monthly",
|
||||
name = stringResource(Res.string.upgrade_product_monthly_name),
|
||||
price = "$2.99/month",
|
||||
description = "Billed monthly",
|
||||
description = stringResource(Res.string.upgrade_billed_monthly),
|
||||
savingsBadge = null,
|
||||
isSelected = false,
|
||||
isProcessing = isProcessing,
|
||||
@@ -268,10 +270,10 @@ private fun SubscriptionProductsSection(
|
||||
// Annual Option
|
||||
SubscriptionProductCard(
|
||||
productId = SubscriptionProducts.ANNUAL,
|
||||
name = "honeyDue Pro Annual",
|
||||
name = stringResource(Res.string.upgrade_product_annual_name),
|
||||
price = "$27.99/year",
|
||||
description = "Billed annually",
|
||||
savingsBadge = "Save 22%",
|
||||
description = stringResource(Res.string.upgrade_billed_annually),
|
||||
savingsBadge = stringResource(Res.string.upgrade_save_22),
|
||||
isSelected = false,
|
||||
isProcessing = isProcessing,
|
||||
onSelect = { onProductSelected(SubscriptionProducts.ANNUAL) }
|
||||
|
||||
+11
-9
@@ -15,6 +15,8 @@ import com.tt.honeyDue.data.DataManager
|
||||
import com.tt.honeyDue.ui.screens.subscription.FeatureComparisonScreen
|
||||
import com.tt.honeyDue.ui.theme.AppRadius
|
||||
import com.tt.honeyDue.ui.theme.AppSpacing
|
||||
import honeydue.composeapp.generated.resources.*
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
|
||||
@Composable
|
||||
fun UpgradePromptDialog(
|
||||
@@ -65,7 +67,7 @@ fun UpgradePromptDialog(
|
||||
|
||||
// Title
|
||||
Text(
|
||||
triggerData?.title ?: "Upgrade to Pro",
|
||||
triggerData?.title ?: stringResource(Res.string.profile_upgrade_to_pro),
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
fontWeight = FontWeight.Bold,
|
||||
textAlign = TextAlign.Center
|
||||
@@ -73,7 +75,7 @@ fun UpgradePromptDialog(
|
||||
|
||||
// Message
|
||||
Text(
|
||||
triggerData?.message ?: "Unlock unlimited access to all features",
|
||||
triggerData?.message ?: stringResource(Res.string.upgrade_prompt_default_message),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
textAlign = TextAlign.Center
|
||||
@@ -92,10 +94,10 @@ fun UpgradePromptDialog(
|
||||
.padding(AppSpacing.md),
|
||||
verticalArrangement = Arrangement.spacedBy(AppSpacing.sm)
|
||||
) {
|
||||
FeatureRow(Icons.Default.Home, "Unlimited properties")
|
||||
FeatureRow(Icons.Default.CheckCircle, "Unlimited tasks")
|
||||
FeatureRow(Icons.Default.People, "Contractor management")
|
||||
FeatureRow(Icons.Default.Description, "Document & warranty storage")
|
||||
FeatureRow(Icons.Default.Home, stringResource(Res.string.upgrade_feature_unlimited_properties))
|
||||
FeatureRow(Icons.Default.CheckCircle, stringResource(Res.string.upgrade_feature_unlimited_tasks))
|
||||
FeatureRow(Icons.Default.People, stringResource(Res.string.upgrade_feature_contractor_management))
|
||||
FeatureRow(Icons.Default.Description, stringResource(Res.string.upgrade_feature_document_warranty_storage))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,7 +121,7 @@ fun UpgradePromptDialog(
|
||||
)
|
||||
} else {
|
||||
Text(
|
||||
triggerData?.buttonText ?: "Upgrade to Pro",
|
||||
triggerData?.buttonText ?: stringResource(Res.string.profile_upgrade_to_pro),
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
}
|
||||
@@ -127,12 +129,12 @@ fun UpgradePromptDialog(
|
||||
|
||||
// Compare Plans
|
||||
TextButton(onClick = { showFeatureComparison = true }) {
|
||||
Text("Compare Free vs Pro")
|
||||
Text(stringResource(Res.string.upgrade_compare_free_vs_pro))
|
||||
}
|
||||
|
||||
// Cancel
|
||||
TextButton(onClick = onDismiss) {
|
||||
Text("Maybe Later")
|
||||
Text(stringResource(Res.string.upgrade_maybe_later))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,7 +59,7 @@ fun UpgradeScreen(
|
||||
title = {},
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onNavigateBack) {
|
||||
Icon(Icons.Default.Close, contentDescription = "Close")
|
||||
Icon(Icons.Default.Close, contentDescription = stringResource(Res.string.a11y_close))
|
||||
}
|
||||
},
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
@@ -110,13 +110,13 @@ fun UpgradeScreen(
|
||||
}
|
||||
|
||||
Text(
|
||||
text = "Upgrade to Pro",
|
||||
text = stringResource(Res.string.upgrade_hero_title),
|
||||
style = MaterialTheme.typography.headlineMedium,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
|
||||
Text(
|
||||
text = "Unlock the full potential of honeyDue",
|
||||
text = stringResource(Res.string.upgrade_hero_subtitle),
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
textAlign = TextAlign.Center
|
||||
@@ -132,16 +132,16 @@ fun UpgradeScreen(
|
||||
verticalArrangement = Arrangement.spacedBy(AppSpacing.md)
|
||||
) {
|
||||
Text(
|
||||
text = "Choose Your Plan",
|
||||
text = stringResource(Res.string.upgrade_choose_plan),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
|
||||
// Yearly Plan (Recommended)
|
||||
PlanCard(
|
||||
title = "Yearly",
|
||||
title = stringResource(Res.string.upgrade_plan_yearly),
|
||||
price = "$29.99/year",
|
||||
savings = "Save 50%",
|
||||
savings = stringResource(Res.string.upgrade_plan_save_50),
|
||||
isSelected = selectedPlan == PlanType.YEARLY,
|
||||
isRecommended = true,
|
||||
onClick = { selectedPlan = PlanType.YEARLY }
|
||||
@@ -149,7 +149,7 @@ fun UpgradeScreen(
|
||||
|
||||
// Monthly Plan
|
||||
PlanCard(
|
||||
title = "Monthly",
|
||||
title = stringResource(Res.string.upgrade_plan_monthly),
|
||||
price = "$4.99/month",
|
||||
savings = null,
|
||||
isSelected = selectedPlan == PlanType.MONTHLY,
|
||||
@@ -166,45 +166,45 @@ fun UpgradeScreen(
|
||||
verticalArrangement = Arrangement.spacedBy(AppSpacing.md)
|
||||
) {
|
||||
Text(
|
||||
text = "What's Included",
|
||||
text = stringResource(Res.string.upgrade_whats_included),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
|
||||
FeatureItem(
|
||||
icon = Icons.Default.Home,
|
||||
title = "Unlimited Properties",
|
||||
description = "Track maintenance for all your homes"
|
||||
title = stringResource(Res.string.upgrade_feature_unlimited_properties),
|
||||
description = stringResource(Res.string.upgrade_feature_unlimited_properties_desc)
|
||||
)
|
||||
|
||||
FeatureItem(
|
||||
icon = Icons.Default.CheckCircle,
|
||||
title = "Unlimited Tasks",
|
||||
description = "Never forget a maintenance task again"
|
||||
title = stringResource(Res.string.upgrade_feature_unlimited_tasks),
|
||||
description = stringResource(Res.string.upgrade_feature_unlimited_tasks_desc)
|
||||
)
|
||||
|
||||
FeatureItem(
|
||||
icon = Icons.Default.People,
|
||||
title = "Contractor Management",
|
||||
description = "Save and rate your trusted contractors"
|
||||
title = stringResource(Res.string.upgrade_feature_contractor_management),
|
||||
description = stringResource(Res.string.upgrade_feature_contractor_management_desc)
|
||||
)
|
||||
|
||||
FeatureItem(
|
||||
icon = Icons.Default.Description,
|
||||
title = "Document Vault",
|
||||
description = "Store warranties, receipts, and manuals"
|
||||
title = stringResource(Res.string.upgrade_feature_document_vault),
|
||||
description = stringResource(Res.string.upgrade_feature_document_vault_desc)
|
||||
)
|
||||
|
||||
FeatureItem(
|
||||
icon = Icons.Default.Share,
|
||||
title = "Family Sharing",
|
||||
description = "Invite family members to collaborate"
|
||||
title = stringResource(Res.string.upgrade_feature_family_sharing),
|
||||
description = stringResource(Res.string.upgrade_feature_family_sharing_desc)
|
||||
)
|
||||
|
||||
FeatureItem(
|
||||
icon = Icons.Default.Notifications,
|
||||
title = "Smart Reminders",
|
||||
description = "Get notified when tasks are due"
|
||||
title = stringResource(Res.string.upgrade_feature_smart_reminders),
|
||||
description = stringResource(Res.string.upgrade_feature_smart_reminders_desc)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -232,7 +232,7 @@ fun UpgradeScreen(
|
||||
)
|
||||
} else {
|
||||
Text(
|
||||
text = "Subscribe Now",
|
||||
text = stringResource(Res.string.upgrade_subscribe_now),
|
||||
fontWeight = FontWeight.Bold,
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
@@ -257,12 +257,12 @@ fun UpgradeScreen(
|
||||
)
|
||||
Spacer(modifier = Modifier.width(AppSpacing.sm))
|
||||
}
|
||||
Text("Restore Purchases")
|
||||
Text(stringResource(Res.string.upgrade_restore_purchases))
|
||||
}
|
||||
|
||||
// Terms
|
||||
Text(
|
||||
text = "Subscription automatically renews unless cancelled at least 24 hours before the end of the current period. Manage subscriptions in your device settings.",
|
||||
text = stringResource(Res.string.upgrade_terms_text),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
textAlign = TextAlign.Center,
|
||||
@@ -281,7 +281,7 @@ fun UpgradeScreen(
|
||||
) {
|
||||
TextButton(onClick = { /* Open terms */ }) {
|
||||
Text(
|
||||
"Terms of Use",
|
||||
stringResource(Res.string.upgrade_terms_of_use),
|
||||
style = MaterialTheme.typography.bodySmall
|
||||
)
|
||||
}
|
||||
@@ -291,7 +291,7 @@ fun UpgradeScreen(
|
||||
)
|
||||
TextButton(onClick = { /* Open privacy */ }) {
|
||||
Text(
|
||||
"Privacy Policy",
|
||||
stringResource(Res.string.profile_privacy),
|
||||
style = MaterialTheme.typography.bodySmall
|
||||
)
|
||||
}
|
||||
@@ -316,13 +316,13 @@ private fun PlanCard(
|
||||
val borderColor by animateColorAsState(
|
||||
targetValue = if (isSelected) MaterialTheme.colorScheme.primary
|
||||
else MaterialTheme.colorScheme.outline.copy(alpha = 0.3f),
|
||||
label = "borderColor"
|
||||
label = "borderColor" // i18n-ignore: animation label, non-UI
|
||||
)
|
||||
|
||||
val backgroundColor by animateColorAsState(
|
||||
targetValue = if (isSelected) MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.3f)
|
||||
else MaterialTheme.colorScheme.surface,
|
||||
label = "backgroundColor"
|
||||
label = "backgroundColor" // i18n-ignore: animation label, non-UI
|
||||
)
|
||||
|
||||
Card(
|
||||
@@ -375,7 +375,7 @@ private fun PlanCard(
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
) {
|
||||
Text(
|
||||
text = "BEST VALUE",
|
||||
text = stringResource(Res.string.upgrade_best_value),
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.onPrimary,
|
||||
modifier = Modifier.padding(horizontal = AppSpacing.sm, vertical = 2.dp)
|
||||
@@ -453,7 +453,7 @@ private fun FeatureItem(
|
||||
|
||||
Icon(
|
||||
Icons.Default.Check,
|
||||
contentDescription = "Included",
|
||||
contentDescription = stringResource(Res.string.upgrade_included),
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.size(20.dp)
|
||||
)
|
||||
|
||||
@@ -656,7 +656,7 @@ fun FloatingLeaf(
|
||||
),
|
||||
repeatMode = RepeatMode.Reverse
|
||||
),
|
||||
label = "leafRotation"
|
||||
label = "leafRotation" // i18n-ignore: animation label, non-UI
|
||||
)
|
||||
|
||||
val offsetY by infiniteTransition.animateFloat(
|
||||
@@ -670,7 +670,7 @@ fun FloatingLeaf(
|
||||
),
|
||||
repeatMode = RepeatMode.Reverse
|
||||
),
|
||||
label = "leafOffset"
|
||||
label = "leafOffset" // i18n-ignore: animation label, non-UI
|
||||
)
|
||||
|
||||
Icon(
|
||||
|
||||
@@ -51,8 +51,8 @@ data class ThemeColors(
|
||||
object AppThemes {
|
||||
val Default = ThemeColors(
|
||||
id = "default",
|
||||
displayName = "Default",
|
||||
description = "Vibrant iOS system colors",
|
||||
displayName = "Default", // i18n-ignore: iOS-parity identifier; localized display resolved at render via themeDisplayName/themeDescription
|
||||
description = "Vibrant iOS system colors", // i18n-ignore: iOS-parity identifier; localized display resolved at render via themeDisplayName/themeDescription
|
||||
|
||||
// Light mode — iOS Default
|
||||
lightPrimary = Color(0xFF007AFF),
|
||||
@@ -81,8 +81,8 @@ object AppThemes {
|
||||
|
||||
val Teal = ThemeColors(
|
||||
id = "teal",
|
||||
displayName = "Teal",
|
||||
description = "Blue-green with warm accents",
|
||||
displayName = "Teal", // i18n-ignore: iOS-parity identifier; localized display resolved at render via themeDisplayName/themeDescription
|
||||
description = "Blue-green with warm accents", // i18n-ignore: iOS-parity identifier; localized display resolved at render via themeDisplayName/themeDescription
|
||||
|
||||
lightPrimary = Color(0xFF07A0C3),
|
||||
lightSecondary = Color(0xFF0055A5),
|
||||
@@ -107,8 +107,8 @@ object AppThemes {
|
||||
|
||||
val Ocean = ThemeColors(
|
||||
id = "ocean",
|
||||
displayName = "Ocean",
|
||||
description = "Deep blues and coral tones",
|
||||
displayName = "Ocean", // i18n-ignore: iOS-parity identifier; localized display resolved at render via themeDisplayName/themeDescription
|
||||
description = "Deep blues and coral tones", // i18n-ignore: iOS-parity identifier; localized display resolved at render via themeDisplayName/themeDescription
|
||||
|
||||
lightPrimary = Color(0xFF006B8F),
|
||||
lightSecondary = Color(0xFF008B8B),
|
||||
@@ -133,8 +133,8 @@ object AppThemes {
|
||||
|
||||
val Forest = ThemeColors(
|
||||
id = "forest",
|
||||
displayName = "Forest",
|
||||
description = "Earth greens and golden hues",
|
||||
displayName = "Forest", // i18n-ignore: iOS-parity identifier; localized display resolved at render via themeDisplayName/themeDescription
|
||||
description = "Earth greens and golden hues", // i18n-ignore: iOS-parity identifier; localized display resolved at render via themeDisplayName/themeDescription
|
||||
|
||||
lightPrimary = Color(0xFF2D5016),
|
||||
lightSecondary = Color(0xFF6B8E23),
|
||||
@@ -159,8 +159,8 @@ object AppThemes {
|
||||
|
||||
val Sunset = ThemeColors(
|
||||
id = "sunset",
|
||||
displayName = "Sunset",
|
||||
description = "Warm oranges and reds",
|
||||
displayName = "Sunset", // i18n-ignore: iOS-parity identifier; localized display resolved at render via themeDisplayName/themeDescription
|
||||
description = "Warm oranges and reds", // i18n-ignore: iOS-parity identifier; localized display resolved at render via themeDisplayName/themeDescription
|
||||
|
||||
lightPrimary = Color(0xFFFF4500),
|
||||
lightSecondary = Color(0xFFFF6347),
|
||||
@@ -185,8 +185,8 @@ object AppThemes {
|
||||
|
||||
val Monochrome = ThemeColors(
|
||||
id = "monochrome",
|
||||
displayName = "Monochrome",
|
||||
description = "Elegant grayscale",
|
||||
displayName = "Monochrome", // i18n-ignore: iOS-parity identifier; localized display resolved at render via themeDisplayName/themeDescription
|
||||
description = "Elegant grayscale", // i18n-ignore: iOS-parity identifier; localized display resolved at render via themeDisplayName/themeDescription
|
||||
|
||||
lightPrimary = Color(0xFF333333),
|
||||
lightSecondary = Color(0xFF666666),
|
||||
@@ -211,8 +211,8 @@ object AppThemes {
|
||||
|
||||
val Lavender = ThemeColors(
|
||||
id = "lavender",
|
||||
displayName = "Lavender",
|
||||
description = "Soft purple with pink accents",
|
||||
displayName = "Lavender", // i18n-ignore: iOS-parity identifier; localized display resolved at render via themeDisplayName/themeDescription
|
||||
description = "Soft purple with pink accents", // i18n-ignore: iOS-parity identifier; localized display resolved at render via themeDisplayName/themeDescription
|
||||
|
||||
lightPrimary = Color(0xFF6B418B),
|
||||
lightSecondary = Color(0xFF8B61B0),
|
||||
@@ -237,8 +237,8 @@ object AppThemes {
|
||||
|
||||
val Crimson = ThemeColors(
|
||||
id = "crimson",
|
||||
displayName = "Crimson",
|
||||
description = "Bold red with warm highlights",
|
||||
displayName = "Crimson", // i18n-ignore: iOS-parity identifier; localized display resolved at render via themeDisplayName/themeDescription
|
||||
description = "Bold red with warm highlights", // i18n-ignore: iOS-parity identifier; localized display resolved at render via themeDisplayName/themeDescription
|
||||
|
||||
lightPrimary = Color(0xFFB51E28),
|
||||
lightSecondary = Color(0xFF992E38),
|
||||
@@ -263,8 +263,8 @@ object AppThemes {
|
||||
|
||||
val Midnight = ThemeColors(
|
||||
id = "midnight",
|
||||
displayName = "Midnight",
|
||||
description = "Deep navy with sky blue",
|
||||
displayName = "Midnight", // i18n-ignore: iOS-parity identifier; localized display resolved at render via themeDisplayName/themeDescription
|
||||
description = "Deep navy with sky blue", // i18n-ignore: iOS-parity identifier; localized display resolved at render via themeDisplayName/themeDescription
|
||||
|
||||
lightPrimary = Color(0xFF1E4A94),
|
||||
lightSecondary = Color(0xFF2E61B0),
|
||||
@@ -289,8 +289,8 @@ object AppThemes {
|
||||
|
||||
val Desert = ThemeColors(
|
||||
id = "desert",
|
||||
displayName = "Desert",
|
||||
description = "Warm terracotta and sand tones",
|
||||
displayName = "Desert", // i18n-ignore: iOS-parity identifier; localized display resolved at render via themeDisplayName/themeDescription
|
||||
description = "Warm terracotta and sand tones", // i18n-ignore: iOS-parity identifier; localized display resolved at render via themeDisplayName/themeDescription
|
||||
|
||||
lightPrimary = Color(0xFFB0614A),
|
||||
lightSecondary = Color(0xFF9E7D61),
|
||||
@@ -315,8 +315,8 @@ object AppThemes {
|
||||
|
||||
val Mint = ThemeColors(
|
||||
id = "mint",
|
||||
displayName = "Mint",
|
||||
description = "Fresh green with turquoise",
|
||||
displayName = "Mint", // i18n-ignore: iOS-parity identifier; localized display resolved at render via themeDisplayName/themeDescription
|
||||
description = "Fresh green with turquoise", // i18n-ignore: iOS-parity identifier; localized display resolved at render via themeDisplayName/themeDescription
|
||||
|
||||
lightPrimary = Color(0xFF38B094),
|
||||
lightSecondary = Color(0xFF61C7B0),
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.tt.honeyDue.util
|
||||
|
||||
import com.tt.honeyDue.i18n.ClientStrings
|
||||
import kotlin.time.Clock
|
||||
import kotlin.time.ExperimentalTime
|
||||
import kotlinx.datetime.DateTimeUnit
|
||||
@@ -46,9 +47,9 @@ object DateUtils {
|
||||
val today = getToday()
|
||||
|
||||
when {
|
||||
date == today -> "Today"
|
||||
date == today.plus(1, DateTimeUnit.DAY) -> "Tomorrow"
|
||||
date == today.minus(1, DateTimeUnit.DAY) -> "Yesterday"
|
||||
date == today -> ClientStrings.t("date.today")
|
||||
date == today.plus(1, DateTimeUnit.DAY) -> ClientStrings.t("date.tomorrow")
|
||||
date == today.minus(1, DateTimeUnit.DAY) -> ClientStrings.t("date.yesterday")
|
||||
else -> formatDateMedium(date)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
@@ -75,7 +76,7 @@ object DateUtils {
|
||||
* Format a LocalDate to medium format (e.g., "Dec 15, 2024")
|
||||
*/
|
||||
private fun formatDateMedium(date: LocalDate): String {
|
||||
val monthName = date.month.name.lowercase().replaceFirstChar { it.uppercase() }.take(3)
|
||||
val monthName = ClientStrings.t("month.${date.monthNumber}")
|
||||
return "$monthName ${date.dayOfMonth}, ${date.year}"
|
||||
}
|
||||
|
||||
@@ -94,9 +95,9 @@ object DateUtils {
|
||||
val today = getToday()
|
||||
|
||||
when {
|
||||
date == today -> "Today"
|
||||
date == today.plus(1, DateTimeUnit.DAY) -> "Tomorrow"
|
||||
date == today.minus(1, DateTimeUnit.DAY) -> "Yesterday"
|
||||
date == today -> ClientStrings.t("date.today")
|
||||
date == today.plus(1, DateTimeUnit.DAY) -> ClientStrings.t("date.tomorrow")
|
||||
date == today.minus(1, DateTimeUnit.DAY) -> ClientStrings.t("date.yesterday")
|
||||
else -> formatDateMedium(date)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
@@ -118,10 +119,10 @@ object DateUtils {
|
||||
val time = localDateTime.time
|
||||
|
||||
val hour = if (time.hour == 0) 12 else if (time.hour > 12) time.hour - 12 else time.hour
|
||||
val amPm = if (time.hour < 12) "AM" else "PM"
|
||||
val amPm = if (time.hour < 12) ClientStrings.t("time.am") else ClientStrings.t("time.pm")
|
||||
val minuteStr = time.minute.toString().padStart(2, '0')
|
||||
|
||||
"${formatDateMedium(date)} at $hour:$minuteStr $amPm"
|
||||
"${formatDateMedium(date)} ${ClientStrings.t("date.at")} $hour:$minuteStr $amPm" // i18n-ignore: interpolated time format; label localized via ClientStrings
|
||||
} catch (e: Exception) {
|
||||
formatDate(dateTimeString)
|
||||
}
|
||||
@@ -140,11 +141,11 @@ object DateUtils {
|
||||
val daysDiff = (date.toEpochDays() - today.toEpochDays()).toInt()
|
||||
|
||||
when (daysDiff) {
|
||||
0 -> "Today"
|
||||
1 -> "Tomorrow"
|
||||
-1 -> "Yesterday"
|
||||
in 2..7 -> "in $daysDiff days"
|
||||
in -7..-2 -> "${-daysDiff} days ago"
|
||||
0 -> ClientStrings.t("date.today")
|
||||
1 -> ClientStrings.t("date.tomorrow")
|
||||
-1 -> ClientStrings.t("date.yesterday")
|
||||
in 2..7 -> ClientStrings.t("date.in_days", daysDiff)
|
||||
in -7..-2 -> ClientStrings.t("date.days_ago", -daysDiff)
|
||||
else -> formatDateMedium(date)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
@@ -237,11 +238,13 @@ object DateUtils {
|
||||
* @return Formatted string like "8:00 AM" or "2:00 PM"
|
||||
*/
|
||||
fun formatHour(hour: Int): String {
|
||||
val am = ClientStrings.t("time.am")
|
||||
val pm = ClientStrings.t("time.pm")
|
||||
return when {
|
||||
hour == 0 -> "12:00 AM"
|
||||
hour < 12 -> "$hour:00 AM"
|
||||
hour == 12 -> "12:00 PM"
|
||||
else -> "${hour - 12}:00 PM"
|
||||
hour == 0 -> "12:00 $am"
|
||||
hour < 12 -> "$hour:00 $am" // i18n-ignore: interpolated time format; am/pm localized via ClientStrings
|
||||
hour == 12 -> "12:00 $pm"
|
||||
else -> "${hour - 12}:00 $pm" // i18n-ignore: interpolated time format; pm localized via ClientStrings
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.tt.honeyDue.util
|
||||
|
||||
import com.tt.honeyDue.i18n.ClientStrings
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.jsonObject
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
@@ -9,27 +10,27 @@ import kotlinx.serialization.json.jsonPrimitive
|
||||
*/
|
||||
object ErrorMessageParser {
|
||||
|
||||
// Network/connection error patterns to detect
|
||||
// Network/connection error patterns to detect, mapped to a localized message key.
|
||||
private val networkErrorPatterns = listOf(
|
||||
"Could not connect to the server" to "Unable to connect to the server. Please check your internet connection.",
|
||||
"NSURLErrorDomain" to "Unable to connect to the server. Please check your internet connection.",
|
||||
"UnresolvedAddressException" to "Unable to connect to the server. Please check your internet connection.",
|
||||
"ConnectException" to "Unable to connect to the server. Please check your internet connection.",
|
||||
"SocketTimeoutException" to "Request timed out. Please try again.",
|
||||
"TimeoutException" to "Request timed out. Please try again.",
|
||||
"No address associated" to "Unable to connect to the server. Please check your internet connection.",
|
||||
"Network is unreachable" to "No internet connection. Please check your network settings.",
|
||||
"Connection refused" to "Unable to connect to the server. The server may be down.",
|
||||
"Connection reset" to "Connection was interrupted. Please try again.",
|
||||
"SSLHandshakeException" to "Secure connection failed. Please try again.",
|
||||
"CertificateException" to "Secure connection failed. Please try again.",
|
||||
"UnknownHostException" to "Unable to connect to the server. Please check your internet connection.",
|
||||
"java.net.SocketException" to "Connection error. Please try again.",
|
||||
"CFNetwork" to "Unable to connect to the server. Please check your internet connection.",
|
||||
"kCFStreamError" to "Unable to connect to the server. Please check your internet connection.",
|
||||
"Code=-1004" to "Unable to connect to the server. Please check your internet connection.",
|
||||
"Code=-1009" to "No internet connection. Please check your network settings.",
|
||||
"Code=-1001" to "Request timed out. Please try again."
|
||||
"Could not connect to the server" to "err.net.no_connection",
|
||||
"NSURLErrorDomain" to "err.net.no_connection",
|
||||
"UnresolvedAddressException" to "err.net.no_connection",
|
||||
"ConnectException" to "err.net.no_connection",
|
||||
"SocketTimeoutException" to "err.net.timeout",
|
||||
"TimeoutException" to "err.net.timeout",
|
||||
"No address associated" to "err.net.no_connection",
|
||||
"Network is unreachable" to "err.net.no_internet",
|
||||
"Connection refused" to "err.net.server_down",
|
||||
"Connection reset" to "err.net.interrupted",
|
||||
"SSLHandshakeException" to "err.net.ssl",
|
||||
"CertificateException" to "err.net.ssl",
|
||||
"UnknownHostException" to "err.net.no_connection",
|
||||
"java.net.SocketException" to "err.net.generic",
|
||||
"CFNetwork" to "err.net.no_connection",
|
||||
"kCFStreamError" to "err.net.no_connection",
|
||||
"Code=-1004" to "err.net.no_connection",
|
||||
"Code=-1009" to "err.net.no_internet",
|
||||
"Code=-1001" to "err.net.timeout"
|
||||
)
|
||||
|
||||
/**
|
||||
@@ -42,22 +43,22 @@ object ErrorMessageParser {
|
||||
val trimmed = rawMessage.trim()
|
||||
|
||||
// Check for network/connection errors first (these are technical messages from exceptions)
|
||||
for ((pattern, friendlyMessage) in networkErrorPatterns) {
|
||||
for ((pattern, messageKey) in networkErrorPatterns) {
|
||||
if (trimmed.contains(pattern, ignoreCase = true)) {
|
||||
return friendlyMessage
|
||||
return ClientStrings.t(messageKey)
|
||||
}
|
||||
}
|
||||
|
||||
// Check if it looks like a technical exception message
|
||||
if (isTechnicalError(trimmed)) {
|
||||
return "Something went wrong. Please try again."
|
||||
return ClientStrings.t("err.generic")
|
||||
}
|
||||
|
||||
// Check if the message looks like JSON (starts with { or [)
|
||||
if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) {
|
||||
// Not JSON - if it's a short, readable message, return it
|
||||
// Otherwise return a generic message
|
||||
return if (isUserFriendly(trimmed)) trimmed else "Something went wrong. Please try again."
|
||||
return if (isUserFriendly(trimmed)) trimmed else ClientStrings.t("err.generic")
|
||||
}
|
||||
|
||||
// If it's JSON, try to parse and extract meaningful error info
|
||||
@@ -75,15 +76,15 @@ object ErrorMessageParser {
|
||||
// Check if this looks like a data object (has id, title/name, etc)
|
||||
// rather than an error response
|
||||
if (json.containsKey("id") && (json.containsKey("title") || json.containsKey("name"))) {
|
||||
return "Request failed. Please check your input and try again."
|
||||
return ClientStrings.t("err.request_failed")
|
||||
}
|
||||
}
|
||||
|
||||
// If we couldn't extract a message, return a generic error
|
||||
"An error occurred. Please try again."
|
||||
ClientStrings.t("err.generic_retry")
|
||||
} catch (e: Exception) {
|
||||
// JSON parsing failed, return generic error
|
||||
"An error occurred. Please try again."
|
||||
ClientStrings.t("err.generic_retry")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,22 +93,22 @@ object ErrorMessageParser {
|
||||
*/
|
||||
private fun isTechnicalError(message: String): Boolean {
|
||||
val technicalIndicators = listOf(
|
||||
"Exception",
|
||||
"Error Domain=",
|
||||
"UserInfo=",
|
||||
"at com.",
|
||||
"at org.",
|
||||
"at java.",
|
||||
"at kotlin.",
|
||||
"at io.",
|
||||
"Caused by:",
|
||||
"Stack trace:",
|
||||
".kt:",
|
||||
".java:",
|
||||
"Exception", // i18n-ignore: error-detection substring, non-UI
|
||||
"Error Domain=", // i18n-ignore: error-detection substring, non-UI
|
||||
"UserInfo=", // i18n-ignore: error-detection substring, non-UI
|
||||
"at com.", // i18n-ignore: error-detection substring, non-UI
|
||||
"at org.", // i18n-ignore: error-detection substring, non-UI
|
||||
"at java.", // i18n-ignore: error-detection substring, non-UI
|
||||
"at kotlin.", // i18n-ignore: error-detection substring, non-UI
|
||||
"at io.", // i18n-ignore: error-detection substring, non-UI
|
||||
"Caused by:", // i18n-ignore: error-detection substring, non-UI
|
||||
"Stack trace:", // i18n-ignore: error-detection substring, non-UI
|
||||
".kt:", // i18n-ignore: error-detection substring, non-UI
|
||||
".java:", // i18n-ignore: error-detection substring, non-UI
|
||||
"0x",
|
||||
"Code=",
|
||||
"interface:",
|
||||
"LocalDataTask"
|
||||
"Code=", // i18n-ignore: error-detection substring, non-UI
|
||||
"interface:", // i18n-ignore: error-detection substring, non-UI
|
||||
"LocalDataTask" // i18n-ignore: error-detection substring, non-UI
|
||||
)
|
||||
return technicalIndicators.any { message.contains(it, ignoreCase = true) }
|
||||
}
|
||||
|
||||
@@ -385,9 +385,9 @@ object SubscriptionHelper {
|
||||
|
||||
// ===== DEPRECATED - Keep for backwards compatibility =====
|
||||
|
||||
@Deprecated("Use isContractorsBlocked() instead", ReplaceWith("isContractorsBlocked()"))
|
||||
@Deprecated("Use isContractorsBlocked() instead", ReplaceWith("isContractorsBlocked()")) // i18n-ignore: deprecation annotation, non-UI
|
||||
fun shouldShowUpgradePromptForContractors(): UsageCheck = isContractorsBlocked()
|
||||
|
||||
@Deprecated("Use isDocumentsBlocked() instead", ReplaceWith("isDocumentsBlocked()"))
|
||||
@Deprecated("Use isDocumentsBlocked() instead", ReplaceWith("isDocumentsBlocked()")) // i18n-ignore: deprecation annotation, non-UI
|
||||
fun shouldShowUpgradePromptForDocuments(): UsageCheck = isDocumentsBlocked()
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package com.tt.honeyDue.viewmodel
|
||||
|
||||
import com.tt.honeyDue.i18n.ClientStrings
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.tt.honeyDue.data.DataManager
|
||||
@@ -83,7 +85,7 @@ class AuthViewModel(
|
||||
_loginState.value = when (result) {
|
||||
is ApiResult.Success -> ApiResult.Success(result.data)
|
||||
is ApiResult.Error -> result
|
||||
else -> ApiResult.Error("Unknown error")
|
||||
else -> ApiResult.Error(ClientStrings.t("err.unknown"))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -106,7 +108,7 @@ class AuthViewModel(
|
||||
ApiResult.Success(result.data)
|
||||
}
|
||||
is ApiResult.Error -> result
|
||||
else -> ApiResult.Error("Unknown error")
|
||||
else -> ApiResult.Error(ClientStrings.t("err.unknown"))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -119,14 +121,14 @@ class AuthViewModel(
|
||||
viewModelScope.launch {
|
||||
_verifyEmailState.value = ApiResult.Loading
|
||||
val token = DataManager.authToken.value ?: run {
|
||||
_verifyEmailState.value = ApiResult.Error("Not authenticated")
|
||||
_verifyEmailState.value = ApiResult.Error(ClientStrings.t("err.not_authenticated"))
|
||||
return@launch
|
||||
}
|
||||
val result = APILayer.verifyEmail(token, VerifyEmailRequest(code = code))
|
||||
_verifyEmailState.value = when (result) {
|
||||
is ApiResult.Success -> ApiResult.Success(result.data)
|
||||
is ApiResult.Error -> result
|
||||
else -> ApiResult.Error("Unknown error")
|
||||
else -> ApiResult.Error(ClientStrings.t("err.unknown"))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -139,7 +141,7 @@ class AuthViewModel(
|
||||
viewModelScope.launch {
|
||||
_updateProfileState.value = ApiResult.Loading
|
||||
val token = DataManager.authToken.value ?: run {
|
||||
_updateProfileState.value = ApiResult.Error("Not authenticated")
|
||||
_updateProfileState.value = ApiResult.Error(ClientStrings.t("err.not_authenticated"))
|
||||
return@launch
|
||||
}
|
||||
val result = APILayer.updateProfile(
|
||||
@@ -153,7 +155,7 @@ class AuthViewModel(
|
||||
_updateProfileState.value = when (result) {
|
||||
is ApiResult.Success -> ApiResult.Success(result.data)
|
||||
is ApiResult.Error -> result
|
||||
else -> ApiResult.Error("Unknown error")
|
||||
else -> ApiResult.Error(ClientStrings.t("err.unknown"))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -184,7 +186,7 @@ class AuthViewModel(
|
||||
_forgotPasswordState.value = when (result) {
|
||||
is ApiResult.Success -> ApiResult.Success(result.data)
|
||||
is ApiResult.Error -> result
|
||||
else -> ApiResult.Error("Unknown error")
|
||||
else -> ApiResult.Error(ClientStrings.t("err.unknown"))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -200,7 +202,7 @@ class AuthViewModel(
|
||||
_verifyResetCodeState.value = when (result) {
|
||||
is ApiResult.Success -> ApiResult.Success(result.data)
|
||||
is ApiResult.Error -> result
|
||||
else -> ApiResult.Error("Unknown error")
|
||||
else -> ApiResult.Error(ClientStrings.t("err.unknown"))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -222,7 +224,7 @@ class AuthViewModel(
|
||||
_resetPasswordState.value = when (result) {
|
||||
is ApiResult.Success -> ApiResult.Success(result.data)
|
||||
is ApiResult.Error -> result
|
||||
else -> ApiResult.Error("Unknown error")
|
||||
else -> ApiResult.Error(ClientStrings.t("err.unknown"))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -253,7 +255,7 @@ class AuthViewModel(
|
||||
_appleSignInState.value = when (result) {
|
||||
is ApiResult.Success -> ApiResult.Success(result.data)
|
||||
is ApiResult.Error -> result
|
||||
else -> ApiResult.Error("Unknown error")
|
||||
else -> ApiResult.Error(ClientStrings.t("err.unknown"))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -272,7 +274,7 @@ class AuthViewModel(
|
||||
_googleSignInState.value = when (result) {
|
||||
is ApiResult.Success -> ApiResult.Success(result.data)
|
||||
is ApiResult.Error -> result
|
||||
else -> ApiResult.Error("Unknown error")
|
||||
else -> ApiResult.Error(ClientStrings.t("err.unknown"))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -295,7 +297,7 @@ class AuthViewModel(
|
||||
_deleteAccountState.value = when (result) {
|
||||
is ApiResult.Success -> ApiResult.Success(result.data)
|
||||
is ApiResult.Error -> result
|
||||
else -> ApiResult.Error("Unknown error")
|
||||
else -> ApiResult.Error(ClientStrings.t("err.unknown"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.tt.honeyDue.data.DataManager
|
||||
import com.tt.honeyDue.data.IDataManager
|
||||
import com.tt.honeyDue.i18n.ClientStrings
|
||||
import com.tt.honeyDue.models.*
|
||||
import com.tt.honeyDue.network.APILayer
|
||||
import com.tt.honeyDue.network.ApiResult
|
||||
@@ -189,12 +190,12 @@ class DocumentViewModel(
|
||||
images.mapIndexed { index, image ->
|
||||
// Always use .jpg extension since we compress to JPEG
|
||||
val baseName = image.fileName.ifBlank { "image_$index" }
|
||||
if (baseName.endsWith(".jpg", ignoreCase = true) ||
|
||||
baseName.endsWith(".jpeg", ignoreCase = true)) {
|
||||
if (baseName.endsWith(".jpg", ignoreCase = true) || // i18n-ignore: file-extension check, non-UI
|
||||
baseName.endsWith(".jpeg", ignoreCase = true)) { // i18n-ignore: file-extension check, non-UI
|
||||
baseName
|
||||
} else {
|
||||
// Remove any existing extension and add .jpg
|
||||
baseName.substringBeforeLast('.', baseName) + ".jpg"
|
||||
baseName.substringBeforeLast('.', baseName) + ".jpg" // i18n-ignore: file-extension suffix, non-UI
|
||||
}
|
||||
}
|
||||
} else null
|
||||
@@ -295,11 +296,11 @@ class DocumentViewModel(
|
||||
// Determine filename with .jpg extension
|
||||
val fileName = if (image.fileName.isNotBlank()) {
|
||||
val baseName = image.fileName
|
||||
if (baseName.endsWith(".jpg", ignoreCase = true) ||
|
||||
baseName.endsWith(".jpeg", ignoreCase = true)) {
|
||||
if (baseName.endsWith(".jpg", ignoreCase = true) || // i18n-ignore: file-extension check, non-UI
|
||||
baseName.endsWith(".jpeg", ignoreCase = true)) { // i18n-ignore: file-extension check, non-UI
|
||||
baseName
|
||||
} else {
|
||||
baseName.substringBeforeLast('.', baseName) + ".jpg"
|
||||
baseName.substringBeforeLast('.', baseName) + ".jpg" // i18n-ignore: file-extension suffix, non-UI
|
||||
}
|
||||
} else {
|
||||
"image_$index.jpg"
|
||||
@@ -315,7 +316,7 @@ class DocumentViewModel(
|
||||
if (uploadResult is ApiResult.Error) {
|
||||
uploadFailed = true
|
||||
_updateState.value = ApiResult.Error(
|
||||
"Document updated but failed to upload image: ${uploadResult.message}",
|
||||
ClientStrings.t("err.vm.document_updated_image_upload_failed", uploadResult.message),
|
||||
uploadResult.code
|
||||
)
|
||||
break
|
||||
@@ -379,11 +380,11 @@ class DocumentViewModel(
|
||||
val compressedBytes = ImageCompressor.compressImage(imageData)
|
||||
val fileName = if (imageData.fileName.isNotBlank()) {
|
||||
val baseName = imageData.fileName
|
||||
if (baseName.endsWith(".jpg", ignoreCase = true) ||
|
||||
baseName.endsWith(".jpeg", ignoreCase = true)) {
|
||||
if (baseName.endsWith(".jpg", ignoreCase = true) || // i18n-ignore: file-extension check, non-UI
|
||||
baseName.endsWith(".jpeg", ignoreCase = true)) { // i18n-ignore: file-extension check, non-UI
|
||||
baseName
|
||||
} else {
|
||||
baseName.substringBeforeLast('.', baseName) + ".jpg"
|
||||
baseName.substringBeforeLast('.', baseName) + ".jpg" // i18n-ignore: file-extension suffix, non-UI
|
||||
}
|
||||
} else {
|
||||
"image.jpg"
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package com.tt.honeyDue.viewmodel
|
||||
|
||||
import com.tt.honeyDue.i18n.ClientStrings
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.tt.honeyDue.data.DataManager
|
||||
@@ -285,7 +287,7 @@ class OnboardingViewModel : ViewModel() {
|
||||
ApiResult.Success(result.data)
|
||||
}
|
||||
is ApiResult.Error -> result
|
||||
else -> ApiResult.Error("Unknown error")
|
||||
else -> ApiResult.Error(ClientStrings.t("err.unknown"))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -303,7 +305,7 @@ class OnboardingViewModel : ViewModel() {
|
||||
ApiResult.Success(result.data)
|
||||
}
|
||||
is ApiResult.Error -> result
|
||||
else -> ApiResult.Error("Unknown error")
|
||||
else -> ApiResult.Error(ClientStrings.t("err.unknown"))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -315,14 +317,14 @@ class OnboardingViewModel : ViewModel() {
|
||||
viewModelScope.launch {
|
||||
_verifyEmailState.value = ApiResult.Loading
|
||||
val token = DataManager.authToken.value ?: run {
|
||||
_verifyEmailState.value = ApiResult.Error("Not authenticated")
|
||||
_verifyEmailState.value = ApiResult.Error(ClientStrings.t("err.not_authenticated"))
|
||||
return@launch
|
||||
}
|
||||
val result = APILayer.verifyEmail(token, VerifyEmailRequest(code = code))
|
||||
_verifyEmailState.value = when (result) {
|
||||
is ApiResult.Success -> ApiResult.Success(Unit)
|
||||
is ApiResult.Error -> result
|
||||
else -> ApiResult.Error("Unknown error")
|
||||
else -> ApiResult.Error(ClientStrings.t("err.unknown"))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -378,7 +380,7 @@ class OnboardingViewModel : ViewModel() {
|
||||
_createResidenceState.value = when (result) {
|
||||
is ApiResult.Success -> ApiResult.Success(Unit)
|
||||
is ApiResult.Error -> result
|
||||
else -> ApiResult.Error("Failed to create residence")
|
||||
else -> ApiResult.Error(ClientStrings.t("err.vm.failed_create_residence"))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -393,7 +395,7 @@ class OnboardingViewModel : ViewModel() {
|
||||
_joinResidenceState.value = when (result) {
|
||||
is ApiResult.Success -> ApiResult.Success(Unit)
|
||||
is ApiResult.Error -> result
|
||||
else -> ApiResult.Error("Failed to join residence")
|
||||
else -> ApiResult.Error(ClientStrings.t("err.vm.failed_join_residence"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+6
-4
@@ -1,5 +1,7 @@
|
||||
package com.tt.honeyDue.viewmodel
|
||||
|
||||
import com.tt.honeyDue.i18n.ClientStrings
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.tt.honeyDue.models.*
|
||||
@@ -64,7 +66,7 @@ class PasswordResetViewModel(
|
||||
ApiResult.Success(result.data)
|
||||
}
|
||||
is ApiResult.Error -> result
|
||||
else -> ApiResult.Error("Unknown error")
|
||||
else -> ApiResult.Error(ClientStrings.t("err.unknown"))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -82,7 +84,7 @@ class PasswordResetViewModel(
|
||||
ApiResult.Success(result.data)
|
||||
}
|
||||
is ApiResult.Error -> result
|
||||
else -> ApiResult.Error("Unknown error")
|
||||
else -> ApiResult.Error(ClientStrings.t("err.unknown"))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -92,7 +94,7 @@ class PasswordResetViewModel(
|
||||
fun resetPassword(newPassword: String, confirmPassword: String) {
|
||||
val token = _resetToken.value
|
||||
if (token == null) {
|
||||
_resetPasswordState.value = ApiResult.Error("Invalid reset token. Please start over.")
|
||||
_resetPasswordState.value = ApiResult.Error(ClientStrings.t("err.vm.invalid_reset_token"))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -115,7 +117,7 @@ class PasswordResetViewModel(
|
||||
ApiResult.Success(result.data)
|
||||
}
|
||||
is ApiResult.Error -> result
|
||||
else -> ApiResult.Error("Unknown error")
|
||||
else -> ApiResult.Error(ClientStrings.t("err.unknown"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+5
-4
@@ -4,6 +4,7 @@ import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.tt.honeyDue.models.TaskCompletionCreateRequest
|
||||
import com.tt.honeyDue.models.TaskCompletionResponse
|
||||
import com.tt.honeyDue.i18n.ClientStrings
|
||||
import com.tt.honeyDue.network.ApiResult
|
||||
import com.tt.honeyDue.network.APILayer
|
||||
import com.tt.honeyDue.util.ImageCompressor
|
||||
@@ -51,9 +52,9 @@ class TaskCompletionViewModel : ViewModel() {
|
||||
val compressed = ImageCompressor.compressImage(image)
|
||||
val fileName = run {
|
||||
val base = image.fileName.ifBlank { "completion_$index" }
|
||||
if (base.endsWith(".jpg", ignoreCase = true) ||
|
||||
base.endsWith(".jpeg", ignoreCase = true)
|
||||
) base else base.substringBeforeLast('.', base) + ".jpg"
|
||||
if (base.endsWith(".jpg", ignoreCase = true) || // i18n-ignore: file-extension check, non-UI
|
||||
base.endsWith(".jpeg", ignoreCase = true) // i18n-ignore: file-extension check, non-UI
|
||||
) base else base.substringBeforeLast('.', base) + ".jpg" // i18n-ignore: file-extension suffix, non-UI
|
||||
}
|
||||
val uploadResult = APILayer.uploadImage(
|
||||
category = "completion",
|
||||
@@ -68,7 +69,7 @@ class TaskCompletionViewModel : ViewModel() {
|
||||
return@launch
|
||||
}
|
||||
else -> {
|
||||
_createCompletionState.value = ApiResult.Error("Upload failed in unexpected state")
|
||||
_createCompletionState.value = ApiResult.Error(ClientStrings.t("err.vm.upload_unexpected_state"))
|
||||
return@launch
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user