Skip to content

Fix merge_strings crash on nested-dictionary .strings value (stacked on #741)#750

Open
mokagio wants to merge 4 commits into
foundationfrom
mokagio/rtk-741-merge-strings-crash-test
Open

Fix merge_strings crash on nested-dictionary .strings value (stacked on #741)#750
mokagio wants to merge 4 commits into
foundationfrom
mokagio/rtk-741-merge-strings-crash-test

Conversation

@mokagio

@mokagio mokagio commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Stacked on #741 (base = foundation). This started as a failing-test artifact to decide how merge_strings' new prefix_keys-based prefixing should handle a nested-dictionary value; it now carries the fix.

Summary

  • L10nHelper.merge_strings no longer crashes on a valid :text .strings file whose value is a nested dictionary ("k" = { a = b; };).
  • It fails soft — copies the un-prefixable file through unprefixed with a UI.important warning — instead of aborting the whole merge.
  • Drive-by: fixes a pre-existing foundation test failure in ios_lint_localizations (unrelated to merge_strings, see below).

Root Cause

merge_strings bookkeeps keys via plutil (which parses a nested-dictionary value fine) but rewrites them by re-tokenizing the raw lines through StringsFileValidationHelper.prefix_keys, whose flat-.strings grammar has no {:

RuntimeError: Invalid character `{` found (current context: after_quoted_key_and_eq)

"k" = { a = b; }; is a valid property list, so plutil -lint passes and file reports ASCII textstrings_file_type returns :text. The file clears the format gate in merge_strings, reaches prefix_keys, and aborts. Before #741 the line-based gsub never raised — it copied such a line through unprefixed. So a public ios_merge_strings_files run that used to complete now crashed.

Fix

Rescue the tokenizer in merge_strings and copy the file's lines through unprefixed on failure — a nested dict has no flat key = value for prefix_keys to rewrite anyway — emitting a UI.important warning. This mirrors the fail-soft decision already made on the scanner path (scan_for_duplicate_keys returns :unscannable rather than crashing the lane). The keys were still bookkept with the prefix, so only this one file's written text is affected.

The regression test now asserts the graceful degradation instead of just not_to raise_error: no raise, a warning is emitted, and the line survives verbatim in the output.

Drive-by: a pre-existing foundation test failure

Independent of merge_strings: commit 9eabbb0 (a review suggestion on #741) reworded ios_lint_localizations' unsupported-format warning to "can only occurr" — a typo — without updating its spec, which still expected the prior "only make sense" text. That left ios_lint_localizations_spec.rb's "warns if input files are not in ASCII-plist format" example failing on foundation, which would otherwise block this PR's suite. Corrected the spelling to "can only occur" and aligned the spec. Worth folding into #741 too if it may merge independently.

Test Plan


Updated by Claude (Opus 4.8) on behalf of @jkmassel.

`L10nHelper.merge_strings` bookkeeps keys via `plutil` (which parses a
nested-dictionary value fine) but re-tokenizes the raw lines through
`prefix_keys`, which raises `Invalid character` on the `{`.
Such a file is a valid property list, still detected as `:text`, so it
passes the format gate and reaches `prefix_keys` — a public
`ios_merge_strings_files` run that previously copied the line through now
aborts.

This stacks a regression test on top of #741 and is intentionally left
red so the reviewer can decide how to make the path fail-soft (e.g.
rescue and copy the line verbatim, matching the scanner path).

---

Generated with the help of Claude Code, https://claude.com/claude-code

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
jkmassel added 2 commits July 3, 2026 15:09
`merge_strings` bookkeeps keys via `plutil` but rewrites them by re-tokenizing
the raw lines through `StringsFileValidationHelper.prefix_keys`, whose
flat-`.strings` grammar has no `{`. A valid file like `"k" = { a = b; };`
clears the `:text` format gate, reaches `prefix_keys`, and raised
`Invalid character` — aborting a merge that previously copied the line through.

Fail soft: rescue the tokenizer and copy the file's lines through unprefixed
with a `UI.important` warning, mirroring the `scan_for_duplicate_keys`
`:unscannable` path. Flip the regression test to assert the graceful
degradation (no raise, warning emitted, line survives).
Commit 9eabbb0 reworded the warning to "can only occurr" (a typo) without
updating the spec, which still expected the prior "only make sense" text —
leaving `ios_lint_localizations_spec.rb`'s "warns if input files are not in
ASCII-plist format" example failing on `foundation`. Correct the spelling to
"can only occur" and align the spec.
@jkmassel jkmassel changed the title Test: prove merge_strings crash on nested-dict .strings (stacked on #741) Fix merge_strings crash on nested-dictionary .strings value (stacked on #741) Jul 3, 2026
@jkmassel jkmassel marked this pull request as ready for review July 3, 2026 21:10
@jkmassel jkmassel requested a review from a team as a code owner July 3, 2026 21:10
The previous fix cleared the `merge_strings` crash on a nested-dictionary
value by rescuing `prefix_keys` and copying the file through *unprefixed*.
That re-opened the exact write/bookkeep inconsistency [#741] closed: the keys
were bookkept with the prefix but written without it, so a same-named key in
another file collided in the merged output, went unreported, and was silently
collapsed by `plutil` — and downstream `ios_extract_keys_from_strings_files`,
which looks keys up by their prefixed name, would drop those translations.

Fix it at the root instead. Teach the shared `StringsFileValidationHelper`
tokenizer the container grammar via a `depth`-counted `:in_container_value`
context, plus `:in_container_quoted_string` and `:maybe_container_comment` so a
`}`/`;` hidden inside a quoted value or comment isn't read as structural.
Because `find_duplicated_keys` and `prefix_keys` share the `TRANSITIONS` table,
both now handle dictionary/array values: the outer key is prefixed and the
value copied verbatim, and inner keys are never recorded as top-level keys.

Also reorder `merge_strings` bookkeeping to run after the rewrite attempt, so
the surviving fail-soft backstop (for constructs still outside the grammar,
e.g. `<data>` values) bookkeeps its keys *unprefixed* to match what was
written — keeping the reported duplicates honest even there.

As a result `scan_for_duplicate_keys` returns `:scanned` instead of
`:unscannable` for nested-dict/array files, so `ios_lint_localizations`'
`check_duplicate_keys` now checks them too.
Copilot AI review requested due to automatic review settings July 3, 2026 23:53

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review is ineligible. To be eligible to request a review, you need a paid Copilot license, or your organization must enable Copilot code review.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants