Skip to content

Commit da25d61

Browse files
committed
Strip control characters from localization fields, update app icon (#30)
1 parent 5c7277c commit da25d61

5 files changed

Lines changed: 36 additions & 2 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
- Fix submitting a new nomination creating a duplicate draft
88
- Fix ASC rate limit errors when saving many locales at once
99
- Only save changed locales instead of all locales on store listing and app details
10+
- Fix control characters from AI models causing ASC save failures on name and subtitle
1011
- Fix all linting and React compiler errors, make lint failures block CI
1112

1213
## 1.6.1

public/icon.icns

-257 KB
Binary file not shown.

public/icon.png

-204 KB
Loading

src/app/api/ai/route.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -231,9 +231,15 @@ export async function POST(request: Request) {
231231
);
232232
}
233233

234+
// Strip control characters from single-line fields (name, subtitle, keywords).
235+
// LLMs (especially local models) sometimes output newlines or invisible chars.
236+
const singleLineField = field === "keywords" || field === "name" || field === "subtitle";
237+
let cleaned = singleLineField
238+
? result.replace(/[\x00-\x1f\x7f\u200b-\u200f\ufeff]/g, "").trim()
239+
: result;
240+
234241
// For fix-keywords: split multi-word keywords first (Apple indexes words
235242
// individually), then strip forbidden words so individual words are caught.
236-
let cleaned = result;
237243
if (action === "fix-keywords") {
238244
// Split multi-word keywords: "clipboard history" → "clipboard,history"
239245
const splitWords = (s: string) =>

src/lib/asc/localization-mutations.ts

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,37 @@ const URL_FIELDS = new Set([
99
"privacyChoicesUrl",
1010
]);
1111

12+
/** Multi-line fields where newlines (\n) are valid content. */
13+
const MULTILINE_FIELDS = new Set([
14+
"description",
15+
"whatsNew",
16+
"promotionalText",
17+
]);
18+
19+
/**
20+
* Strip control characters that ASC rejects (null, carriage return, tab,
21+
* escape, zero-width chars, etc.). For multi-line fields, preserve \n.
22+
*/
23+
function stripControlChars(value: string, allowNewlines: boolean): string {
24+
// \x00-\x1f covers null, tab, newline, carriage return, escape, etc.
25+
// \x7f is DEL. \u200b-\u200f and \ufeff are zero-width/invisible chars.
26+
if (allowNewlines) {
27+
// Keep \n, strip everything else
28+
return value.replace(/[\x00-\x09\x0b-\x1f\x7f\u200b-\u200f\ufeff]/g, "");
29+
}
30+
return value.replace(/[\x00-\x1f\x7f\u200b-\u200f\ufeff]/g, "");
31+
}
32+
1233
function cleanAttributes(attrs: Record<string, unknown>): Record<string, unknown> {
1334
const cleaned: Record<string, unknown> = {};
1435
for (const [k, v] of Object.entries(attrs)) {
15-
cleaned[k] = URL_FIELDS.has(k) && v === "" ? null : v;
36+
if (URL_FIELDS.has(k) && v === "") {
37+
cleaned[k] = null;
38+
} else if (typeof v === "string") {
39+
cleaned[k] = stripControlChars(v, MULTILINE_FIELDS.has(k));
40+
} else {
41+
cleaned[k] = v;
42+
}
1643
}
1744
return cleaned;
1845
}

0 commit comments

Comments
 (0)