Skip to content

[DIT-13505]: YAML aliases are dropped by the i18n extractor - #9

Merged
laurakoye merged 2 commits into
laura/dit-13480-capture-the-raw-literal-snapshot_text-in-resource-filefrom
laura/dit-13505-yaml-aliases-are-dropped-by-the-i18n-extractor
Aug 19, 2026
Merged

[DIT-13505]: YAML aliases are dropped by the i18n extractor#9
laurakoye merged 2 commits into
laura/dit-13480-capture-the-raw-literal-snapshot_text-in-resource-filefrom
laura/dit-13505-yaml-aliases-are-dropped-by-the-i18n-extractor

Conversation

@laurakoye

@laurakoye laurakoye commented Aug 18, 2026

Copy link
Copy Markdown
Collaborator

Closes DIT-13505.

A YAML key whose value is an alias produced no candidate, so copy shared through anchors was invisible to the scan everywhere it was reused.

en:
  greeting: &hello Hello there   # extracted
  farewell: *hello               # dropped

Why: an Alias node has no value property, only a reference, and the walker emits on nodes that have one.

Decision

Aliases are emitted as value links. farewell yields "Hello there" with snapshot_text: "*hello" — the region actually written at that key. This is the first snapshot that shares no text with its value; source.includes(snapshot_text) still holds.

Overwriting *hello would break the alias, so it joins the other spans that aren't safe to replace as-is in DIT-13481. Nothing on the write path changes here.

Also: merge keys

<<: *defaults is the same Alias node, so it was dropped too. Merged maps now fold into the merging path:

defaults: &d
  title: Welcome
page:
  <<: *d          # en.page.title → "Welcome", snapshot "*d"
  body: Some body

<<: [*a, *b] works as well.

Resolution follows the parser

Review caught three cases where the first pass resolved aliases differently from YAML itself. All three are fixed:

YAML means first pass emitted
an anchor name declared twice the declaration above the alias the last one in the file
<<: *d plus an explicit key the explicit key both, twice
<<: [*a, *b] sharing a key the first anchor both, twice

The first one is the reason this changed shape: it reported a wrong value, not a missing one.

The anchor half is now the parser's job — Alias.resolve(doc) finds "the last instance of the anchor before this node", which is exactly the rule a name → node map gets wrong. That deleted the anchor pre-pass entirely.

Merge precedence stays ours, because the document tree keeps << as an ordinary pair — only toJS() resolves it, and that throws away the positions snapshot_text needs. The rule is one line: within a map the first candidate for a key wins, and explicit keys are considered before merged ones.

Tests

toJS({ merge: true }) does resolve all of this correctly, so it makes a good oracle. Five inputs — including all three cases above — assert that what we emit matches what the parser resolves, which covers the whole class rather than the instances that happened to get found.

Two more pin where we deliberately diverge: toJS() throws on an anchor cycle and on a dangling alias, and we keep going. Those tests assert both halves.

The rest are unchanged: alias value and span, an alias reporting its own line rather than the anchor's, merge key, merge-key list, aliased sequence item, missing anchor skipped, self-referential anchor terminating, and one per bug above.

Plain scalars, block scalars and plural suffixes are untouched.

Verify locally

From the repo root on this branch:

mkdir -p /tmp/yamldemo && cat > /tmp/yamldemo/en.yml <<'EOF'
en:
  greeting: &hello Hello there
  farewell: *hello
  defaults: &page
    title: Welcome
  home:
    <<: *page
    body: Some body
  planets:
    - *hello
    - Mercury
EOF

cat > alias-check.ts <<'EOF'
import { yamlI18nExtractor } from "./src/lang/extractors/yaml-i18n";
import { readFileSync } from "node:fs";

const source = readFileSync(process.argv[2], "utf8");

(async () => {
  const hits = await yamlI18nExtractor.extract({ source, kind: "yaml_i18n" });
  for (const h of hits) {
    const found = source.includes(h.snapshotText) ? "ok " : "MISSING";
    console.log(`${found}  ${h.i18nKey?.padEnd(20)} ${JSON.stringify(h.value).padEnd(16)} snapshot=${JSON.stringify(h.snapshotText)}`);
  }
  console.log(`\n${hits.length} candidates, ${hits.filter((h) => !source.includes(h.snapshotText)).length} with a span not found in the file`);
})();
EOF

npx tsx ./alias-check.ts /tmp/yamldemo/en.yml

The script has to sit in the repo root — the relative import resolves from there.

Expected — 7 candidates:

ok   en.greeting          "Hello there"    snapshot="Hello there"
ok   en.farewell          "Hello there"    snapshot="*hello"
ok   en.defaults.title    "Welcome"        snapshot="Welcome"
ok   en.home.title        "Welcome"        snapshot="*page"
ok   en.home.body         "Some body"      snapshot="Some body"
ok   en.planets.0         "Hello there"    snapshot="*hello"
ok   en.planets.1         "Mercury"        snapshot="Mercury"

7 candidates, 0 with a span not found in the file

MISSING in the first column is the acceptance criterion — every span must be findable verbatim in the file, aliases included.

Before the fix — 4 candidates. Swap in the base version of the one file and rerun:

git checkout origin/laura/dit-13480-capture-the-raw-literal-snapshot_text-in-resource-file -- src/lang/extractors/yaml-i18n.ts
npx tsx ./alias-check.ts /tmp/yamldemo/en.yml
git checkout HEAD -- src/lang/extractors/yaml-i18n.ts

en.farewell, en.home.title and en.planets.0 vanish.

Cleanup: rm alias-check.ts && rm -rf /tmp/yamldemo

@jholiga jholiga left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I found a few super edge cases with Claude that I noted. I wonder if it's worth checking out if there's any opportunity to lean more on the yaml parser that we import and call on line 17 of yaml-i18n.ts?

Comment thread src/lang/extractors/yaml-i18n.ts Outdated
Comment thread src/lang/extractors/yaml-i18n.ts Outdated
Comment thread src/lang/extractors/yaml-i18n.ts Outdated
@laurakoye
laurakoye requested a review from jholiga August 19, 2026 16:29
@laurakoye
laurakoye merged commit 7f4ead9 into laura/dit-13480-capture-the-raw-literal-snapshot_text-in-resource-file Aug 19, 2026
3 checks passed
@laurakoye
laurakoye deleted the laura/dit-13505-yaml-aliases-are-dropped-by-the-i18n-extractor branch August 19, 2026 17:02
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.

2 participants