DIT-13480: Capture the raw literal (snapshot_text) in resource-file extractors - #4
Merged
laurakoye merged 19 commits intoAug 19, 2026
Conversation
laurakoye
force-pushed
the
laura/dit-13480-capture-the-raw-literal-snapshot_text-in-resource-file
branch
from
August 17, 2026 22:21
087b646 to
a24036b
Compare
jholiga
reviewed
Aug 18, 2026
jholiga
left a comment
Collaborator
There was a problem hiding this comment.
Testing looked good! Couple smaller things I noted, one is a unit test failure, and the other is something I found with Claude while testing, that appears to be a pre-existing extractor bug but might be more exposed with this change being introd
| describe("stringsExtractor (.strings)", () => { | ||
| test("emits one resource_value per key=value pair", async () => { | ||
| const hits = await extract(`"greeting" = "Hello, world";\n"farewell" = "Goodbye";\n`); | ||
| expect(hits).toEqual([ |
Collaborator
There was a problem hiding this comment.
I think you noted this in the description but these tests seem to be failing
laurakoye
changed the base branch from
laura/dit-13464-capture-the-raw-literal-snapshot_text-in-source-code
to
main
August 18, 2026 14:52
laurakoye
force-pushed
the
laura/dit-13480-capture-the-raw-literal-snapshot_text-in-resource-file
branch
from
August 18, 2026 14:53
11f5a44 to
2313403
Compare
jholiga
reviewed
Aug 19, 2026
jholiga
left a comment
Collaborator
There was a problem hiding this comment.
Couple small things I saw, but I think they're minor! Both relate to location.column in different extractors
…ash-the-line-number-instead-of-the-occurrence [DIT-13503]: candidate ids hash the line number instead of the occurrence
…xt-is-dropped-when-it-carries-an-inline DIT-13504: XLIFF source text is dropped when it carries an inline placeholder
jholiga
approved these changes
Aug 19, 2026
…e-dropped-by-the-i18n-extractor [DIT-13505]: YAML aliases are dropped by the i18n extractor
laurakoye
deleted the
laura/dit-13480-capture-the-raw-literal-snapshot_text-in-resource-file
branch
August 19, 2026 17:11
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Overview
Fills in
snapshot_textfor the resource-file extractors and makes the field required.snapshot_textis the contiguous source region a value came from — not "the region it's safe to write to." Every case has one, so the field is never null.value_rawis decoded and can't be searched for; the snapshot is a verbatim slice, sosource.includes(snapshot_text)always holds.Most formats were already computing the span and discarding it —
json-i18nandxcstringstrackedstart/endbut read onlystart,stringshadendIndex,arbinherits fromjson-i18n, and the CDATA paths captured their payload verbatim. The rest:android-resources,resx,xliff)yaml-i18n|header and per-line indentation.po.properties.poand.propertiesread from the original source via a shared line-offset table rather than re-joining split lines, so CRLF files round-trip..propertiesmaps indices in the assembled logical line back to their physical line, so a bundle that continues the key (long\/key = Value here) spans only the value.The type flip is what makes it stick — the compiler now catches any extractor that forgets:
src/lang/types.tssnapshotText?: stringsnapshotText: stringsrc/extract.tshit.snapshotText ?? nullhit.snapshotTextsrc/types.tsz.string().nullable()z.string()Editability is a separate, write-path concern: a writer must refuse to replace a span carrying markup or syntax the value doesn't. Nothing writes back yet, so that's a gate on DIT-13471, tracked in DIT-13481.
Also fixed:
.stringsdicttruncated its valueselementTextreturned only the firsttextchild, and the HTML grammar splits text at every entity reference and nested tag:Keys truncated the same way, producing a wrong
i18nKey— or failing theUSER_FACING_KEYSlookup and dropping the entry.stringsdictwas the only XML format not usingxml.ts'sinnerText, which reads the whole inner region, strips nested tags, and decodes entities; it now does, andelementTextis gone. Note entities now decode in the value (Tom & Jerry), matching the other three XML formats, andsnapshotTextis the raw span rather than a copy of the decoded value.Also fixed: a
pohit's location pointed at the wrong lineA hit's
locationcame from themsgidside even when its value came from amsgstr[N], solocationandsnapshot_textdescribed different places — line 6 for text living on line 8. It now comes from whichever chunk supplied the value, so the two agree:pickNonEmptyalready returned the winning chunk, which carries its own line, so this is a two-word change plus tests asserting each snapshot is findable on the line its location names.Flagged for review, not changed
Duplicate candidate ids, pre-existing.
makeCandidateIdhashes(file, line, column, value)— the line, not the occurrence index. PO plurals with three or more forms share a location line, so two forms with identical text collide.occurrence_indexdistinguishes them; the id doesn't. Tracked in DIT-13503 — the location fix above doesn't resolve it, since the colliding forms are still on one line.XLIFF
<source>text is dropped when it carries an inline placeholder.<source>is a void element in HTML, and this project parses XML with ast-grep's HTML grammar — so when a<g>/<ph>/<xliff:g>appears inside it, the grammar closes<source>early, makes the placeholder a sibling, and demotes</source>toerroneous_end_tag.innerTextrequires anend_tag, finds none, and returns null, so no candidate is produced.<target>is unaffected — it isn't an HTML element, so it nests normally:So a unit keeps its translation and silently loses its original, and a base-locale XLIFF loses the copy that matters. Pre-existing, documented by a test, tracked separately.
YAML aliases are dropped.
b: *xparses to anAliasnode with novalueproperty, so the walker'sisScalarguard skips it. Unrelated to the XLIFF cause despite the similar symptom — tracked separately.Context
DIT-13480, part of DIT-13405. Based on
main.A code link identifies a string by (file, value, occurrence index) and stores
snapshot_textas the code's own rendering of it. Schema design.ZCodeLink.snapshotTextis non-nullable andrequired: truein mongoose (ditto-app#9346), so a null snapshot couldn't be persisted as a codelink at all — the field being always-present is what makes rung 2 of reconciliation (snapshot_text+occurrence_index) usable when an i18n key is renamed.Screenshots
None — extraction output only.
Test Plan
1. The suite
Eleven extractors joined the
describe.eachtable insnapshot-text.test.ts, so every format now assertssnapshotTextis present on each hit and findable verbatim in the source. A seconddescribepins the spans that aren't replaceable as-is: XML wrapping a nested child, YAML block scalar beside a plain sibling, PO multi-chunk,.propertiescontinuation, and a continued key.A new sweep covers the four XML formats: every span must run from one tag boundary to the next (
>…<, or the CDATA brackets).source.includes()proves a span is findable, never complete —"Tom"satisfies it trivially — which is why the truncation above survived the other checks. Verified by reintroducing the bug: the sweep fails onstringsdictand passes on the other three.strings.test.tsalso needed updating, since it asserts whole hit objects withtoEqual.CI (
.github/workflows/ci.yml) runstypecheck,jest, andbuildon Node 20 and 22 for every PR.2. Coverage on a real app
snap.mjsgoes in the repo root —./dist/index.jsresolves relative to the script, not the working directory.3. What to look for
The second and third lines are the properties that matter: nothing is missing a span, and every span is a real slice of its own file. v0-demo's resource files are JSON catalogs, so the stitched and multi-line paths are covered by unit tests instead.
Note:
yarn linking this into the CLI and runningyarn scandoes not exercise this code until cli#151 merges — the CLI still uses its own copy.