test(android): cover settings model localization

This commit is contained in:
Vincent Koc
2026-07-12 07:56:17 +02:00
committed by Vincent Koc
parent 4c6f171b9b
commit bd52ff5f5f
2 changed files with 28 additions and 3 deletions

View File

@@ -272,6 +272,9 @@ const UI_BRANCH_LITERAL_RE = /(?:->|return|\?:)\s*"((?:\\.|[^"\\])+)"/gu;
const UI_IF_BRANCH_LITERAL_RE = /(?:\bif\s*\([^)]*\)|\belse)\s*\{\s*"((?:\\.|[^"\\])+)"/gu;
const UI_MODEL_STRING_NAME_RE =
/^(?:contentDescription|errorText|helperText|onClickLabel|statusText)$/u;
const UI_MODEL_CLASS_FIELDS = new Map<string, ReadonlySet<string>>([
["SettingsToggleRow", new Set(["subtitle", "title"])],
]);
const KOTLIN_STRING_LITERAL_RE = /"((?:\\.|[^"\\])+)"/gu;
// These literals are either technical data or immutable source tokens localized at a typed render edge.
@@ -592,11 +595,14 @@ function collectTypedModelLiteralFindings(
trackTypeArguments: true,
});
const userFacingParameters = new Map<string, number>();
const classFields = UI_MODEL_CLASS_FIELDS.get(className);
parameters.forEach((parameter, index) => {
const field = parameter.value.match(
/\bval\s+([A-Za-z_][A-Za-z0-9_]*)\s*:\s*String\??(?=\s*(?:=|$))/u,
)?.[1];
if (field && UI_MODEL_STRING_NAME_RE.test(field)) userFacingParameters.set(field, index);
if (field && (UI_MODEL_STRING_NAME_RE.test(field) || classFields?.has(field))) {
userFacingParameters.set(field, index);
}
});
if (userFacingParameters.size === 0) continue;
const callPattern = new RegExp(`\\b${className}\\s*\\(`, "gu");
@@ -729,7 +735,11 @@ function renderStringsXml(
manual: readonly ResourceString[],
generated: ReadonlyMap<string, { source: string; value: string }>,
): string {
const lines = ["<resources>"];
const lines = [
generated.size > 0
? '<resources xmlns:tools="http://schemas.android.com/tools">'
: "<resources>",
];
for (const entry of manual) {
lines.push(` <string name="${entry.key}"${entry.attrs}>${entry.rawValue}</string>`);
}
@@ -740,8 +750,10 @@ function renderStringsXml(
)) {
const formatted = INTERPOLATION_RE.test(entry.source) ? "" : ' formatted="false"';
INTERPOLATION_RE.lastIndex = 0;
// Translation-memory text intentionally preserves technical tokens and source punctuation,
// so Android's English dictionary and typography suggestions do not apply to managed keys.
lines.push(
` <string name="${key}"${formatted}>"${renderAndroidResourceValue(entry.source, entry.value)}"</string>`,
` <string name="${key}"${formatted} tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"${renderAndroidResourceValue(entry.source, entry.value)}"</string>`,
);
}
}

View File

@@ -10,6 +10,11 @@ import {
describe("Android app i18n resources", () => {
it("keeps generated resources, runtime coverage, and every locale aligned", async () => {
await expect(checkAndroidAppI18n()).resolves.toBeUndefined();
const base = await readFile("apps/android/app/src/main/res/values/strings.xml", "utf8");
expect(base).toContain('xmlns:tools="http://schemas.android.com/tools"');
expect(base).toMatch(
/<string name="native_[a-f0-9]+"[^>]*tools:ignore="Typos,TypographyDashes,TypographyEllipsis">/u,
);
});
it("preserves the existing Swedish app name", async () => {
@@ -43,6 +48,10 @@ describe("Android app i18n resources", () => {
val connected: Boolean,
val statusText: String,
)
data class SettingsToggleRow(
val title: String,
val subtitle: String,
)
Text("Settings")
Text(text = nativeStringResource("Connected"))
@@ -51,6 +60,8 @@ describe("Android app i18n resources", () => {
SettingsMetric("Gateway", gatewayName)
ConnectionState(connected = false, statusText = "Connecting to $host")
ConnectionState(connected = true, statusText = nativeString("Connected"))
SettingsToggleRow("Phone capability", "Share device data")
SettingsToggleRow(nativeString("Localized capability"), nativeString("Localized detail"))
Text(text = fileName ?: "Attachment")
Modifier.clickable(onClickLabel = "Open detail", onClick = {})
Text(nativeString("First sentence. ") + "Second sentence.")
@@ -73,6 +84,8 @@ describe("Android app i18n resources", () => {
expect.objectContaining({ source: "Working" }),
expect.objectContaining({ source: "Gateway" }),
expect.objectContaining({ source: "Connecting to $host" }),
expect.objectContaining({ source: "Phone capability" }),
expect.objectContaining({ source: "Share device data" }),
expect.objectContaining({ source: "Attachment" }),
expect.objectContaining({ source: "Open detail" }),
expect.objectContaining({ source: "Second sentence." }),