fix(core-crash): file the prefilled crash issue in English
The issue the crash flow prefills took its title and body from string resources, so both followed the device language: a crash on a French phone opened the tracker with a French issue, which is unreadable to whoever has to triage it. The report itself was always English — only the wrapper around it wasn't. That text is posted rather than shown, so it is no longer a resource at all: title and body are fixed English in CrashReportSubmit, and CrashConfig drops issueTitle (apps pass one fewer argument and can delete their own crash_report_issue_title / crash_report_body_* strings). What the reporter sees in the app — the dialog, the toasts, the clipboard label — stays translated, which is the line the kdoc and the resource header now draw.
This commit is contained in:
@@ -10,6 +10,17 @@ are no binary releases; "version" tracks the shared `version` in the root build
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Fixed
|
||||
|
||||
- **`core-crash`** — The prefilled crash issue is filed in English again. Its
|
||||
title and body came from string resources, so a crash on a French phone
|
||||
opened the tracker with a French issue — unreadable to whoever has to triage
|
||||
it. That text is now fixed English in `CrashReportSubmit`, out of resources
|
||||
altogether, and `CrashConfig` no longer takes an `issueTitle` (apps drop the
|
||||
argument, and their own `crash_report_issue_title` / `crash_report_body_*`
|
||||
strings with it). Everything the reporter *sees* — the dialog, the toasts,
|
||||
the clipboard label — stays translated.
|
||||
|
||||
## [0.3.0] - 2026-08-10
|
||||
|
||||
### Added
|
||||
|
||||
@@ -8,6 +8,10 @@ package de.jeanlucmakiola.floret.crash
|
||||
*
|
||||
* URLs are passed as plain strings (resolved from each app's own resources)
|
||||
* rather than kit resources, since they differ per app and are not translated.
|
||||
*
|
||||
* The prefilled issue's title and body are *not* here: they are posted to the
|
||||
* tracker rather than shown in the app, so they are fixed English text owned by
|
||||
* the kit (see [submitCrashReport]) and cannot pick up the device language.
|
||||
*/
|
||||
data class CrashConfig(
|
||||
/** Display name, e.g. "Agendula". Used in the report header and dialog copy. */
|
||||
@@ -16,6 +20,4 @@ data class CrashConfig(
|
||||
val newIssueUrl: String,
|
||||
/** Issue tracker entry point for a manual (non-crash) report — e.g. a chooser. */
|
||||
val chooseIssueUrl: String,
|
||||
/** Title used for the prefilled crash issue, e.g. "Crash report". */
|
||||
val issueTitle: String,
|
||||
)
|
||||
|
||||
@@ -14,12 +14,15 @@ import androidx.core.net.toUri
|
||||
* automatically — the apps have no network access; the user reviews and submits
|
||||
* the issue themselves. Issue-tracker URLs and the app label come from the
|
||||
* installed [CrashReporter.config].
|
||||
*
|
||||
* What the user *sees* here — the dialog, the toasts, the clipboard label — is
|
||||
* translated. What is *posted* is not: see [buildIssueBody].
|
||||
*/
|
||||
fun submitCrashReport(context: Context, report: String) {
|
||||
val config = CrashReporter.config ?: return
|
||||
copyReportToClipboard(context, config.appLabel, report)
|
||||
val opened = runCatching {
|
||||
context.startActivity(Intent(Intent.ACTION_VIEW, buildIssueUri(context, config, report)))
|
||||
context.startActivity(Intent(Intent.ACTION_VIEW, buildIssueUri(config, report)))
|
||||
}.isSuccess
|
||||
val message = if (opened) R.string.crash_report_copied else R.string.crash_report_open_failed
|
||||
Toast.makeText(context, message, Toast.LENGTH_LONG).show()
|
||||
@@ -43,20 +46,33 @@ private fun copyReportToClipboard(context: Context, appLabel: String, report: St
|
||||
* "paste from clipboard" placeholder) — the clipboard copy is the source of
|
||||
* truth in that case.
|
||||
*/
|
||||
private fun buildIssueUri(context: Context, config: CrashConfig, report: String) =
|
||||
private fun buildIssueUri(config: CrashConfig, report: String) =
|
||||
config.newIssueUrl.toUri().buildUpon()
|
||||
.appendQueryParameter("title", config.issueTitle)
|
||||
.appendQueryParameter("body", buildIssueBody(context, config.appLabel, report))
|
||||
.appendQueryParameter("title", ISSUE_TITLE)
|
||||
.appendQueryParameter("body", buildIssueBody(config.appLabel, report))
|
||||
.build()
|
||||
|
||||
private fun buildIssueBody(context: Context, appLabel: String, report: String): String {
|
||||
val block = if (report.length > MAX_URL_REPORT_CHARS) {
|
||||
context.getString(R.string.crash_report_body_paste)
|
||||
} else {
|
||||
"```\n$report\n```"
|
||||
}
|
||||
return context.getString(R.string.crash_report_body_template, appLabel, block)
|
||||
/**
|
||||
* The prefilled issue body. Deliberately hard-coded English rather than a string
|
||||
* resource: this text is filed in the project's issue tracker, where maintainers
|
||||
* read it, so it must not follow the device language — a report from a French
|
||||
* phone used to arrive as a French issue. Everything the reporter sees inside
|
||||
* the app stays translated.
|
||||
*/
|
||||
internal fun buildIssueBody(appLabel: String, report: String): String {
|
||||
val block = if (report.length > MAX_URL_REPORT_CHARS) REPORT_TOO_LONG else "```\n$report\n```"
|
||||
return "Thanks for reporting a crash in $appLabel. Please add anything you remember " +
|
||||
"about what you were doing, then submit.\n\n" +
|
||||
"### What happened\n\n\n" +
|
||||
"### Crash report\n$block\n"
|
||||
}
|
||||
|
||||
/** Title of the prefilled issue. English, for the same reason as the body. */
|
||||
internal const val ISSUE_TITLE = "Crash report"
|
||||
|
||||
/** Stands in for a report too long to carry in the link; it is on the clipboard. */
|
||||
private const val REPORT_TOO_LONG =
|
||||
"_(The report was too long for this link — paste it from your clipboard here.)_"
|
||||
|
||||
/** Keep the prefilled body comfortably under common URL-length ceilings. */
|
||||
private const val MAX_URL_REPORT_CHARS = 6_000
|
||||
|
||||
@@ -3,7 +3,10 @@
|
||||
Generic crash-report UI strings shared by the family. App-agnostic: the
|
||||
app's display name is injected as %1$s from CrashConfig.appLabel, so these
|
||||
read correctly for any app. Issue-tracker URLs are NOT here — they are
|
||||
per-app and come from CrashConfig. Apps may override any of these strings.
|
||||
per-app and come from CrashConfig. Neither is the prefilled issue's title
|
||||
or body: that text is posted to the tracker rather than shown in the app,
|
||||
so it is fixed English in CrashReportSubmit.kt and never translated.
|
||||
Apps may override any of the strings below.
|
||||
-->
|
||||
<string name="crash_dialog_title">%1$s crashed</string>
|
||||
<string name="crash_dialog_message">%1$s closed unexpectedly last time. You can help fix it by sending this report as an issue. It stays on your device until you choose to share it, and includes no personal data or app content — only the technical details below.</string>
|
||||
@@ -12,8 +15,6 @@
|
||||
<string name="crash_report_clip_label">%1$s crash report</string>
|
||||
<string name="crash_report_copied">Report copied to your clipboard</string>
|
||||
<string name="crash_report_open_failed">Couldn\'t open the issue tracker. The report is on your clipboard.</string>
|
||||
<string name="crash_report_body_template">Thanks for reporting a crash in %1$s. Please add anything you remember about what you were doing, then submit.\n\n### What happened\n\n\n### Crash report\n%2$s\n</string>
|
||||
<string name="crash_report_body_paste">_(The report was too long for this link — paste it from your clipboard here.)_</string>
|
||||
<!-- Fallback app label if the dialog is shown before install() (should not happen). -->
|
||||
<string name="crash_app_fallback">The app</string>
|
||||
</resources>
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
package de.jeanlucmakiola.floret.crash
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.util.Locale
|
||||
|
||||
/**
|
||||
* The issue title and body are posted to the tracker, not shown in the app, so
|
||||
* they must stay English whatever language the device is in — a crash from a
|
||||
* French phone once filed a French issue. Pinning them as plain text (rather
|
||||
* than string resources) is what guarantees it; these tests hold that line.
|
||||
*/
|
||||
class IssueBodyTest {
|
||||
|
||||
@Test
|
||||
fun `body is English regardless of the device locale`() {
|
||||
val body = withLocale(Locale.FRANCE) { buildIssueBody("Calendula", "boom") }
|
||||
assertThat(body).contains("Thanks for reporting a crash in Calendula.")
|
||||
assertThat(body).contains("### What happened")
|
||||
assertThat(body).contains("### Crash report")
|
||||
assertThat(ISSUE_TITLE).isEqualTo("Crash report")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a short report rides along in a fenced block`() {
|
||||
val body = buildIssueBody("Agendula", "stack trace here")
|
||||
assertThat(body).contains("```\nstack trace here\n```")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `an over-long report is replaced by the clipboard placeholder`() {
|
||||
val body = buildIssueBody("Clockula", "x".repeat(7_000))
|
||||
assertThat(body).doesNotContain("xxxx")
|
||||
assertThat(body).contains("paste it from your clipboard here")
|
||||
}
|
||||
|
||||
private fun <T> withLocale(locale: Locale, block: () -> T): T {
|
||||
val previous = Locale.getDefault()
|
||||
Locale.setDefault(locale)
|
||||
return try {
|
||||
block()
|
||||
} finally {
|
||||
Locale.setDefault(previous)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user