fix: 同步/触发错误全链路本地化(count_times 单位 + Unknown/PartialSync/TriggerError 结构化映射)
This commit is contained in:
parent
039f27da18
commit
f788d57798
@ -252,7 +252,7 @@ class CodexRepository(
|
|||||||
}
|
}
|
||||||
is RefreshResult.Failure -> {
|
is RefreshResult.Failure -> {
|
||||||
DebugLog.e(TAG, "triggerUsage: 二次刷新失败: ${r.reason}")
|
DebugLog.e(TAG, "triggerUsage: 二次刷新失败: ${r.reason}")
|
||||||
return@use Result.failure(TriggerError("token 刷新失败: ${r.reason}", ""))
|
return@use Result.failure(TriggerError("token 刷新失败: ${r.reason}", "", TriggerErrorReason.TOKEN_REFRESH))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -394,8 +394,14 @@ class CodexRepository(
|
|||||||
@Serializable data class OAuthRefreshResponse(@kotlinx.serialization.SerialName("access_token") val accessToken: String, @kotlinx.serialization.SerialName("refresh_token") val refreshToken: String? = null, @kotlinx.serialization.SerialName("expires_in") val expiresIn: Long = 3600, @kotlinx.serialization.SerialName("token_type") val tokenType: String? = null)
|
@Serializable data class OAuthRefreshResponse(@kotlinx.serialization.SerialName("access_token") val accessToken: String, @kotlinx.serialization.SerialName("refresh_token") val refreshToken: String? = null, @kotlinx.serialization.SerialName("expires_in") val expiresIn: Long = 3600, @kotlinx.serialization.SerialName("token_type") val tokenType: String? = null)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** TriggerError 的结构化原因(UI 层据此映射本地化文案;null = HTTP 错误,summary 为 ASCII 可直显) */
|
||||||
|
enum class TriggerErrorReason {
|
||||||
|
/** token 刷新失败(summary 中为中文用户提示,不直接透传 UI) */
|
||||||
|
TOKEN_REFRESH
|
||||||
|
}
|
||||||
|
|
||||||
/** 携带完整响应体的错误类,供 UI 展示服务端返回的详细信息 */
|
/** 携带完整响应体的错误类,供 UI 展示服务端返回的详细信息 */
|
||||||
class TriggerError(val summary: String, val responseBody: String) : Exception(summary)
|
class TriggerError(val summary: String, val responseBody: String, val reason: TriggerErrorReason? = null) : Exception(summary)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 将 SSE 流响应解析为简洁的文本摘要。
|
* 将 SSE 流响应解析为简洁的文本摘要。
|
||||||
|
|||||||
@ -30,6 +30,7 @@ import androidx.compose.ui.unit.dp
|
|||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import com.rainy.token.R
|
import com.rainy.token.R
|
||||||
|
import com.rainy.token.ui.components.asString
|
||||||
import com.rainy.token.data.local.OverviewStats
|
import com.rainy.token.data.local.OverviewStats
|
||||||
import com.rainy.token.ui.theme.InkMuted
|
import com.rainy.token.ui.theme.InkMuted
|
||||||
import com.rainy.token.ui.theme.StrawberryPink
|
import com.rainy.token.ui.theme.StrawberryPink
|
||||||
@ -164,7 +165,7 @@ fun CommandCodeUsageStatsCard(
|
|||||||
uiState.lastSyncError?.let { err ->
|
uiState.lastSyncError?.let { err ->
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
Text(
|
Text(
|
||||||
text = "✗ $err",
|
text = "✗ ${err.asString()}",
|
||||||
style = MaterialTheme.typography.bodySmall,
|
style = MaterialTheme.typography.bodySmall,
|
||||||
color = MaterialTheme.colorScheme.error
|
color = MaterialTheme.colorScheme.error
|
||||||
)
|
)
|
||||||
|
|||||||
@ -212,6 +212,8 @@ private fun errorToUiText(error: Throwable): UiText = when (error) {
|
|||||||
RepositoryError.ParseErrorReason.MODELS_EMPTY -> UiText.Resource(R.string.error_parse_models_empty)
|
RepositoryError.ParseErrorReason.MODELS_EMPTY -> UiText.Resource(R.string.error_parse_models_empty)
|
||||||
RepositoryError.ParseErrorReason.MALFORMED_RESPONSE -> UiText.Resource(R.string.error_parse_malformed)
|
RepositoryError.ParseErrorReason.MALFORMED_RESPONSE -> UiText.Resource(R.string.error_parse_malformed)
|
||||||
}
|
}
|
||||||
|
// Unknown 的 message 以硬编码中文"未知错误"开头,不能透传 UI,统一映射本地化文案
|
||||||
|
is RepositoryError.Unknown -> UiText.Resource(R.string.common_unknown)
|
||||||
else -> error.message?.let { UiText.Dynamic(it) }
|
else -> error.message?.let { UiText.Dynamic(it) }
|
||||||
?: UiText.Resource(R.string.common_unknown)
|
?: UiText.Resource(R.string.common_unknown)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -31,6 +31,7 @@ import androidx.hilt.navigation.compose.hiltViewModel
|
|||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import com.rainy.token.R
|
import com.rainy.token.R
|
||||||
import com.rainy.token.data.local.OverviewStats
|
import com.rainy.token.data.local.OverviewStats
|
||||||
|
import com.rainy.token.ui.components.asString
|
||||||
import com.rainy.token.ui.theme.InkMuted
|
import com.rainy.token.ui.theme.InkMuted
|
||||||
import com.rainy.token.ui.theme.StrawberryPink
|
import com.rainy.token.ui.theme.StrawberryPink
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
@ -163,7 +164,7 @@ fun UsageStatsCard(
|
|||||||
uiState.lastSyncError?.let { err ->
|
uiState.lastSyncError?.let { err ->
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
Text(
|
Text(
|
||||||
text = "✗ $err",
|
text = "✗ ${err.asString()}",
|
||||||
style = MaterialTheme.typography.bodySmall,
|
style = MaterialTheme.typography.bodySmall,
|
||||||
color = MaterialTheme.colorScheme.error
|
color = MaterialTheme.colorScheme.error
|
||||||
)
|
)
|
||||||
|
|||||||
@ -9,10 +9,13 @@ import com.rainy.token.data.local.ModelStats
|
|||||||
import com.rainy.token.data.local.DailyStats
|
import com.rainy.token.data.local.DailyStats
|
||||||
import com.rainy.token.data.local.UsageCache
|
import com.rainy.token.data.local.UsageCache
|
||||||
import com.rainy.token.data.repository.CredentialRepository
|
import com.rainy.token.data.repository.CredentialRepository
|
||||||
|
import com.rainy.token.data.repository.RepositoryError
|
||||||
import com.rainy.token.domain.model.Credential
|
import com.rainy.token.domain.model.Credential
|
||||||
import com.rainy.token.domain.service.ServiceType
|
import com.rainy.token.domain.service.ServiceType
|
||||||
import com.rainy.token.domain.usecase.SyncCommandCodeUsageUseCase
|
import com.rainy.token.domain.usecase.SyncCommandCodeUsageUseCase
|
||||||
|
import com.rainy.token.domain.usecase.SyncError
|
||||||
import com.rainy.token.domain.usecase.SyncUsageUseCase
|
import com.rainy.token.domain.usecase.SyncUsageUseCase
|
||||||
|
import com.rainy.token.ui.components.UiText
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
@ -203,7 +206,7 @@ class UsageViewModel @Inject constructor(
|
|||||||
it.copy(
|
it.copy(
|
||||||
syncing = false,
|
syncing = false,
|
||||||
lastSyncResult = result.getOrNull()?.inserted ?: 0,
|
lastSyncResult = result.getOrNull()?.inserted ?: 0,
|
||||||
lastSyncError = result.exceptionOrNull()?.message
|
lastSyncError = result.exceptionOrNull()?.let { syncErrorToUiText(it) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -224,7 +227,7 @@ class UsageViewModel @Inject constructor(
|
|||||||
it.copy(
|
it.copy(
|
||||||
syncing = false,
|
syncing = false,
|
||||||
lastSyncResult = result.getOrNull()?.inserted ?: 0,
|
lastSyncResult = result.getOrNull()?.inserted ?: 0,
|
||||||
lastSyncError = result.exceptionOrNull()?.message
|
lastSyncError = result.exceptionOrNull()?.let { syncErrorToUiText(it) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -243,7 +246,7 @@ data class UsageUiState(
|
|||||||
val dailyStats: List<DailyStats> = emptyList(),
|
val dailyStats: List<DailyStats> = emptyList(),
|
||||||
val recordCount: Int = 0,
|
val recordCount: Int = 0,
|
||||||
val lastSyncResult: Int = 0,
|
val lastSyncResult: Int = 0,
|
||||||
val lastSyncError: String? = null,
|
val lastSyncError: UiText? = null,
|
||||||
val timeFilter: TimeFilter = TimeFilter.All,
|
val timeFilter: TimeFilter = TimeFilter.All,
|
||||||
val dailyPage: Int = 1,
|
val dailyPage: Int = 1,
|
||||||
val modelFilter: String? = null // null = 全部模型
|
val modelFilter: String? = null // null = 全部模型
|
||||||
@ -256,3 +259,34 @@ private data class LoadResult(
|
|||||||
val dailyStats: List<DailyStats>,
|
val dailyStats: List<DailyStats>,
|
||||||
val totalCount: Int
|
val totalCount: Int
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/** 把同步错误映射为本地化文案:SyncError / RepositoryError 全部走资源,不透传中文 message。 */
|
||||||
|
private fun syncErrorToUiText(error: Throwable): UiText = when (error) {
|
||||||
|
is SyncError.PartialSync ->
|
||||||
|
UiText.Resource(R.string.sync_partial, listOf(error.inserted, error.errors.size))
|
||||||
|
is RepositoryError.InvalidCredential ->
|
||||||
|
UiText.Resource(R.string.error_credential_invalid_reconfigure)
|
||||||
|
is RepositoryError.CredentialChanged ->
|
||||||
|
UiText.Resource(R.string.error_credential_changed_reload)
|
||||||
|
is RepositoryError.RateLimited -> UiText.Resource(
|
||||||
|
R.string.error_rate_limited_retry,
|
||||||
|
listOf(
|
||||||
|
error.retryAfterSeconds?.let {
|
||||||
|
UiText.Resource(R.string.error_rate_limited_retry_suffix, listOf(it))
|
||||||
|
} ?: ""
|
||||||
|
)
|
||||||
|
)
|
||||||
|
is RepositoryError.Network -> UiText.Resource(R.string.error_network_check)
|
||||||
|
is RepositoryError.ServerError ->
|
||||||
|
UiText.Resource(R.string.error_server_http, listOf(error.code))
|
||||||
|
is RepositoryError.ParseError -> when (error.reason) {
|
||||||
|
RepositoryError.ParseErrorReason.EMPTY_BODY -> UiText.Resource(R.string.error_parse_empty_body)
|
||||||
|
RepositoryError.ParseErrorReason.NOT_JSON_OBJECT -> UiText.Resource(R.string.error_parse_not_json)
|
||||||
|
RepositoryError.ParseErrorReason.NO_WINDOWS -> UiText.Resource(R.string.error_parse_no_windows)
|
||||||
|
RepositoryError.ParseErrorReason.NO_MODELS -> UiText.Resource(R.string.error_parse_no_models)
|
||||||
|
RepositoryError.ParseErrorReason.MODELS_EMPTY -> UiText.Resource(R.string.error_parse_models_empty)
|
||||||
|
RepositoryError.ParseErrorReason.MALFORMED_RESPONSE -> UiText.Resource(R.string.error_parse_malformed)
|
||||||
|
}
|
||||||
|
is RepositoryError.Unknown -> UiText.Resource(R.string.common_unknown)
|
||||||
|
else -> UiText.Resource(R.string.common_unknown)
|
||||||
|
}
|
||||||
@ -8,6 +8,7 @@ import com.rainy.token.data.debug.DebugLog
|
|||||||
import com.rainy.token.data.repository.CredentialRepository
|
import com.rainy.token.data.repository.CredentialRepository
|
||||||
import com.rainy.token.data.repository.RepositoryError
|
import com.rainy.token.data.repository.RepositoryError
|
||||||
import com.rainy.token.data.repository.TriggerError
|
import com.rainy.token.data.repository.TriggerError
|
||||||
|
import com.rainy.token.data.repository.TriggerErrorReason
|
||||||
import com.rainy.token.domain.model.Credential
|
import com.rainy.token.domain.model.Credential
|
||||||
import com.rainy.token.domain.model.CredentialStatus
|
import com.rainy.token.domain.model.CredentialStatus
|
||||||
import com.rainy.token.domain.model.ServiceBalance
|
import com.rainy.token.domain.model.ServiceBalance
|
||||||
@ -337,12 +338,10 @@ class ServiceDetailViewModel @Inject constructor(
|
|||||||
val responseBody: String?
|
val responseBody: String?
|
||||||
when (error) {
|
when (error) {
|
||||||
is TriggerError -> {
|
is TriggerError -> {
|
||||||
val prefix = "token 刷新失败: "
|
// reason 结构化:token 刷新失败显示本地化文案(summary 中文提示只进日志);
|
||||||
message = if (error.summary.startsWith(prefix)) {
|
// 其余(HTTP 错误)summary 为 ASCII 可直接展示
|
||||||
UiText.Resource(
|
message = if (error.reason == TriggerErrorReason.TOKEN_REFRESH) {
|
||||||
R.string.error_token_refresh_failed,
|
UiText.Resource(R.string.error_token_refresh_generic)
|
||||||
listOf(error.summary.removePrefix(prefix))
|
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
UiText.Dynamic(error.summary)
|
UiText.Dynamic(error.summary)
|
||||||
}
|
}
|
||||||
@ -544,6 +543,8 @@ class ServiceDetailViewModel @Inject constructor(
|
|||||||
RepositoryError.ParseErrorReason.MODELS_EMPTY -> UiText.Resource(R.string.error_parse_models_empty)
|
RepositoryError.ParseErrorReason.MODELS_EMPTY -> UiText.Resource(R.string.error_parse_models_empty)
|
||||||
RepositoryError.ParseErrorReason.MALFORMED_RESPONSE -> UiText.Resource(R.string.error_parse_malformed)
|
RepositoryError.ParseErrorReason.MALFORMED_RESPONSE -> UiText.Resource(R.string.error_parse_malformed)
|
||||||
}
|
}
|
||||||
|
// Unknown 的 message 以硬编码中文"未知错误"开头,不能透传 UI,统一映射本地化文案
|
||||||
|
is RepositoryError.Unknown -> UiText.Resource(R.string.common_unknown)
|
||||||
else -> error.message?.let { UiText.Dynamic(it) }
|
else -> error.message?.let { UiText.Dynamic(it) }
|
||||||
?: UiText.Resource(R.string.common_unknown)
|
?: UiText.Resource(R.string.common_unknown)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -185,6 +185,8 @@ class CredentialEditViewModel @Inject constructor(
|
|||||||
RepositoryError.ParseErrorReason.MODELS_EMPTY -> UiText.Resource(R.string.error_parse_models_empty)
|
RepositoryError.ParseErrorReason.MODELS_EMPTY -> UiText.Resource(R.string.error_parse_models_empty)
|
||||||
RepositoryError.ParseErrorReason.MALFORMED_RESPONSE -> UiText.Resource(R.string.error_parse_malformed)
|
RepositoryError.ParseErrorReason.MALFORMED_RESPONSE -> UiText.Resource(R.string.error_parse_malformed)
|
||||||
}
|
}
|
||||||
|
// Unknown 的 message 以硬编码中文"未知错误"开头,不能透传 UI,统一映射本地化文案
|
||||||
|
is RepositoryError.Unknown -> UiText.Resource(R.string.common_unknown)
|
||||||
else -> err?.message?.let { UiText.Dynamic(it) }
|
else -> err?.message?.let { UiText.Dynamic(it) }
|
||||||
?: UiText.Resource(R.string.common_unknown)
|
?: UiText.Resource(R.string.common_unknown)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -139,6 +139,7 @@
|
|||||||
<string name="usage_cache_write">缓存写入 %1$s</string>
|
<string name="usage_cache_write">缓存写入 %1$s</string>
|
||||||
<string name="usage_no_data_sync">暂无数据,点击 🔄 同步</string>
|
<string name="usage_no_data_sync">暂无数据,点击 🔄 同步</string>
|
||||||
<string name="usage_new_records">✓ 新增 %1$d 条</string>
|
<string name="usage_new_records">✓ 新增 %1$d 条</string>
|
||||||
|
<string name="sync_partial">部分同步完成:插入 %1$d条,%2$d页失败</string>
|
||||||
<string name="usage_click_to_switch"> 点击切换</string>
|
<string name="usage_click_to_switch"> 点击切换</string>
|
||||||
<string name="usage_utc0_note">所有时间按 UTC+0 计算</string>
|
<string name="usage_utc0_note">所有时间按 UTC+0 计算</string>
|
||||||
<string name="msg_utc8_on">已切换到 UTC+8,图表按北京时间显示</string>
|
<string name="msg_utc8_on">已切换到 UTC+8,图表按北京时间显示</string>
|
||||||
@ -273,7 +274,7 @@
|
|||||||
<string name="trigger_parse_failed">(响应解析失败,但请求已发出)</string>
|
<string name="trigger_parse_failed">(响应解析失败,但请求已发出)</string>
|
||||||
<string name="trigger_usage_prompt_completion_total">用量: prompt=%1$s completion=%2$s total=%3$s tokens</string>
|
<string name="trigger_usage_prompt_completion_total">用量: prompt=%1$s completion=%2$s total=%3$s tokens</string>
|
||||||
<string name="trigger_usage_input_output">用量: input=%1$s output=%2$s tokens</string>
|
<string name="trigger_usage_input_output">用量: input=%1$s output=%2$s tokens</string>
|
||||||
<string name="error_token_refresh_failed">token 刷新失败:%1$s</string>
|
<string name="error_token_refresh_generic">token 刷新失败,请重新登录或重新导入 auth.json</string>
|
||||||
|
|
||||||
<!-- ══════════ WebView 登录 ══════════ -->
|
<!-- ══════════ WebView 登录 ══════════ -->
|
||||||
<string name="title_webview_login">登录 %1$s</string>
|
<string name="title_webview_login">登录 %1$s</string>
|
||||||
|
|||||||
@ -139,6 +139,7 @@
|
|||||||
<string name="usage_cache_write">快取寫入 %1$s</string>
|
<string name="usage_cache_write">快取寫入 %1$s</string>
|
||||||
<string name="usage_no_data_sync">暫無數據,點選 🔄 同步</string>
|
<string name="usage_no_data_sync">暫無數據,點選 🔄 同步</string>
|
||||||
<string name="usage_new_records">✓ 新增 %1$d 條</string>
|
<string name="usage_new_records">✓ 新增 %1$d 條</string>
|
||||||
|
<string name="sync_partial">部分同步完成:插入 %1$d條,%2$d頁失敗</string>
|
||||||
<string name="usage_click_to_switch"> 點選切換</string>
|
<string name="usage_click_to_switch"> 點選切換</string>
|
||||||
<string name="usage_utc0_note">所有時間按 UTC+0 計算</string>
|
<string name="usage_utc0_note">所有時間按 UTC+0 計算</string>
|
||||||
<string name="msg_utc8_on">已切換到 UTC+8,圖表按北京時間顯示</string>
|
<string name="msg_utc8_on">已切換到 UTC+8,圖表按北京時間顯示</string>
|
||||||
@ -273,7 +274,7 @@
|
|||||||
<string name="trigger_parse_failed">(響應解析失敗,但請求已發出)</string>
|
<string name="trigger_parse_failed">(響應解析失敗,但請求已發出)</string>
|
||||||
<string name="trigger_usage_prompt_completion_total">用量: prompt=%1$s completion=%2$s total=%3$s tokens</string>
|
<string name="trigger_usage_prompt_completion_total">用量: prompt=%1$s completion=%2$s total=%3$s tokens</string>
|
||||||
<string name="trigger_usage_input_output">用量: input=%1$s output=%2$s tokens</string>
|
<string name="trigger_usage_input_output">用量: input=%1$s output=%2$s tokens</string>
|
||||||
<string name="error_token_refresh_failed">token 重新整理失敗:%1$s</string>
|
<string name="error_token_refresh_generic">token 重新整理失敗,請重新登入或重新匯入 auth.json</string>
|
||||||
|
|
||||||
<!-- ══════════ WebView 登录 ══════════ -->
|
<!-- ══════════ WebView 登录 ══════════ -->
|
||||||
<string name="title_webview_login">登入 %1$s</string>
|
<string name="title_webview_login">登入 %1$s</string>
|
||||||
|
|||||||
@ -142,6 +142,7 @@
|
|||||||
<string name="usage_cache_write">Cache write %1$s</string>
|
<string name="usage_cache_write">Cache write %1$s</string>
|
||||||
<string name="usage_no_data_sync">No data yet · tap 🔄 to sync</string>
|
<string name="usage_no_data_sync">No data yet · tap 🔄 to sync</string>
|
||||||
<string name="usage_new_records">✓ %1$d new records</string>
|
<string name="usage_new_records">✓ %1$d new records</string>
|
||||||
|
<string name="sync_partial">Partial sync: %1$d records inserted, %2$d pages failed</string>
|
||||||
<string name="usage_click_to_switch"> tap to switch</string>
|
<string name="usage_click_to_switch"> tap to switch</string>
|
||||||
<string name="usage_utc0_note">All times in UTC+0</string>
|
<string name="usage_utc0_note">All times in UTC+0</string>
|
||||||
<string name="msg_utc8_on">Switched to UTC+8, charts shown in Beijing time</string>
|
<string name="msg_utc8_on">Switched to UTC+8, charts shown in Beijing time</string>
|
||||||
@ -276,7 +277,7 @@
|
|||||||
<string name="trigger_parse_failed">(response parsing failed, but the request was sent)</string>
|
<string name="trigger_parse_failed">(response parsing failed, but the request was sent)</string>
|
||||||
<string name="trigger_usage_prompt_completion_total">Usage: prompt=%1$s completion=%2$s total=%3$s tokens</string>
|
<string name="trigger_usage_prompt_completion_total">Usage: prompt=%1$s completion=%2$s total=%3$s tokens</string>
|
||||||
<string name="trigger_usage_input_output">Usage: input=%1$s output=%2$s tokens</string>
|
<string name="trigger_usage_input_output">Usage: input=%1$s output=%2$s tokens</string>
|
||||||
<string name="error_token_refresh_failed">Token refresh failed: %1$s</string>
|
<string name="error_token_refresh_generic">Token refresh failed. Please log in again or import auth.json.</string>
|
||||||
|
|
||||||
<!-- ══════════ WebView 登录 ══════════ -->
|
<!-- ══════════ WebView 登录 ══════════ -->
|
||||||
<string name="title_webview_login">Sign in to %1$s</string>
|
<string name="title_webview_login">Sign in to %1$s</string>
|
||||||
@ -338,7 +339,7 @@
|
|||||||
<string name="tip_13_detail">The widget refreshes balances when you swipe to it, or tap the ↻ button manually.</string>
|
<string name="tip_13_detail">The widget refreshes balances when you swipe to it, or tap the ↻ button manually.</string>
|
||||||
|
|
||||||
<!-- ══════════ 图表/明细 ══════════ -->
|
<!-- ══════════ 图表/明细 ══════════ -->
|
||||||
<string name="count_times">%1$d</string>
|
<string name="count_times">%1$d calls</string>
|
||||||
<string name="date_select_day">Pick a date</string>
|
<string name="date_select_day">Pick a date</string>
|
||||||
<string name="date_select_month">Pick a month (any day)</string>
|
<string name="date_select_month">Pick a month (any day)</string>
|
||||||
<string name="date_pick_start">Start date</string>
|
<string name="date_pick_start">Start date</string>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user