Skip to content

feat(cli): migrate a raw InternationalizationContext value to the provider - #7969

Draft
vjeux wants to merge 28 commits into
meta:mainfrom
vjeux:perf/i18n-context-codemod
Draft

vjeux wants to merge 28 commits into
meta:mainfrom
vjeux:perf/i18n-context-codemod

Conversation

@vjeux

@vjeux vjeux commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

User need

Apps that configure Astryx's translations by rendering a value into InternationalizationContext stop working when they take #7357, which retires that path ([breaking] there). astryx upgrade is where a version's mechanical migrations live, so this rewrite should run for them instead of every app editing by hand.

Problem and solution fit

What led to it: Astryx's i18n work (#7357) splits the English catalog into per-component slices, so a page without the provider loads 6.2 to 6.8 KB less gzip. Review asked for the migration in its own pull request; this is it.

One staged transform, migrate-internationalization-context:

  • <InternationalizationContext value={i18n}> (or .Provider) becomes <InternationalizationProvider locale={i18n.locale} dir={i18n.direction} messages={i18n.messages}>, with a TODO for overrides and translate.
  • An inline object literal maps key by key.
  • Anything it can't prove stays as written behind a TODO that says why: a call, a conditional, a spread, an empty literal, an element that spreads props.

Current specification

spec:AST-040 FR1 (edit only a resolved, unshadowed binding) and spec:AST-017 FR8's migration evidence. Two owner decisions gate the stack: whether component:InternationalizationProvider's owners accept retiring the raw-context path, and the 0.8.0 release plan for #7357's [breaking] entry.

Public delta and caller burden

astryx upgrade gains the transform once the release moves it from transforms/next/ into its version folder. A caller reviews any TODO it leaves.

Design and alternatives

  • Document the migration only. Nothing to maintain, but every app rewrites by hand.
  • Rewrite every shape, spreads included. More coverage, but a spread can override the value, so the output could silently configure the wrong locale.
  • Chosen: rewrite only what it can prove, and say why it left the rest. A wrong rewrite is worse than a TODO. Scope is resolved by hand (blocks, loops, catch clauses, hoisted vars, namespaces) because the generic scope model gives blocks no scope of their own.

Evidence

The fixtures in transforms/next/__tests__/migrate-internationalization-context.test.mjs pair each input with its output: every rewritten and declined shape, shadowing, type-only imports, alias reuse, comments and a second run that changes nothing. One typechecks the output against the real i18n types.

Scope

  • One capability is added.
  • Unrelated fixes, cleanup, and policy changes were removed or split.
  • Consumer docs, focused tests, and migration evidence ship with the feature.
  • Public text and artifacts contain no internal Meta context.

Testing

  1. Run pnpm vitest run packages/cli/assets/codemods. All pass, each fixture's input rewritten to its expected output, and the typechecked fixture compiles.
  2. The transform was also run with real jscodeshift over the fixtures' shapes and the output read by hand.
  3. Not this change: check:changesets is red on the maintainers' release gate, because this branch carries perf(i18n): let a component carry only its own English strings #7357's [breaking] entry while main plans patch 0.7.5. This PR's own changeset is a patch [feat].

Stacked on #7357, merged after it.

@vercel

vercel Bot commented Oct 23, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
astryx Ready Ready Preview Oct 29, 2026 9:09pm UTC

Request Review

@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Meta Open Source bot. label Oct 23, 2026
@astracat-bot

astracat-bot Bot commented Oct 23, 2026 •

Copy link
Copy Markdown

AI review status for this pull request.

Review status Updated
❓ Needs maintainer input (for maintainers only) Oct 30, 2026, 12:43 AM UTC

@github-actions

github-actions Bot commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

PR Analysis Report

📚 Storybook Preview

View Storybook for this PR

🧪 Sandbox Preview

View Sandbox for this PR

Component Size

Component Import before Import after Delta CSS before CSS after Delta
core/AlertDialog 56.9KB 56.9KB +4B 12.4KB 12.4KB 0B
core/AppShell 57.9KB 57.9KB -25B 11.6KB 11.6KB 0B
core/Avatar 37.5KB 37.6KB +116B 6.3KB 6.3KB 0B
core/AvatarGroup 39.8KB 39.9KB +94B 6.4KB 6.4KB 0B
core/AvatarGroupOverflow 39.9KB 40.0KB +90B 6.4KB 6.4KB 0B
core/Banner 47.8KB 47.8KB -3B 9.9KB 9.9KB 0B
core/BaseTypeahead 54.3KB 54.3KB +22B 9.9KB 9.9KB 0B
core/BreadcrumbItem 66.4KB 66.3KB -85B 11.0KB 11.0KB 0B
core/Breadcrumbs 22.4KB 22.5KB +113B 3.3KB 3.3KB 0B
core/Button 43.3KB 43.2KB -53B 9.2KB 9.2KB 0B
core/Calendar 54.9KB 55.0KB +87B 10.0KB 10.0KB 0B
core/Carousel 50.2KB 50.2KB -40B 10.2KB 10.2KB 0B
core/ChatComposer 60.6KB 60.6KB +2B 10.8KB 10.8KB 0B
core/ChatComposerDrawer 23.9KB 24.0KB +146B 4.5KB 4.5KB 0B
core/ChatComposerInput 55.7KB 55.8KB +38B 10.1KB 10.1KB 0B
core/ChatComposerTokenElement 55.7KB 55.8KB +41B 10.1KB 10.1KB 0B
core/ChatDictationButton 46.5KB 46.5KB -12B 9.5KB 9.5KB 0B
core/ChatLayout 48.8KB 48.8KB -40B 9.9KB 9.9KB 0B
core/ChatLayoutScrollButton 46.4KB 46.3KB -69B 9.6KB 9.6KB 0B
core/ChatMessage 23.1KB 23.2KB +115B 3.5KB 3.5KB 0B
core/ChatMessageList 28.9KB 28.9KB -36B 7.8KB 7.8KB 0B
core/ChatMessageMetadata 25.8KB 25.9KB +109B 3.9KB 3.9KB 0B
core/ChatSendButton 45.8KB 45.8KB -18B 9.2KB 9.2KB 0B
core/ChatToolCalls 31.9KB 31.8KB -69B 8.9KB 8.9KB 0B
core/CheckboxInput 49.9KB 49.9KB -3B 9.3KB 9.3KB 0B
core/CheckboxList 46.0KB 46.0KB +6B 8.1KB 8.1KB 0B
core/CheckboxListItem 54.0KB 54.0KB -25B 10.5KB 10.5KB 0B
core/Citation 25.9KB 26.0KB +125B 4.1KB 4.1KB 0B
core/CodeBlock 60.5KB 60.5KB -43B 10.7KB 10.7KB 0B
core/CommandPalette 50.9KB 50.9KB -1B 11.8KB 11.8KB 0B
core/CommandPaletteFooter 23.3KB 23.4KB +143B 3.5KB 3.5KB 0B
core/CommandPaletteInput 32.3KB 32.3KB -62B 8.1KB 8.1KB 0B
core/CommandPaletteList 22.4KB 22.5KB +113B 3.3KB 3.3KB 0B
core/ComplexSelector 55.8KB 55.8KB -29B 10.6KB 10.6KB 0B
core/ContextMenu 77.7KB 77.7KB 0B 14.6KB 14.6KB 0B
core/DateInput 99.9KB 100.0KB +92B 14.8KB 14.8KB 0B
core/DateRangeInput 69.4KB 69.5KB +140B 11.2KB 11.2KB 0B
core/DateTimeInput 110.1KB 110.3KB +200B 15.2KB 15.2KB 0B
core/DialogHeader 48.8KB 48.8KB -14B 9.8KB 9.8KB 0B
core/DrawerHeader 48.9KB 48.9KB +21B 11.2KB 11.2KB 0B
core/DropdownMenu 72.7KB 72.7KB -22B 14.7KB 14.7KB 0B
core/DropdownMenuSubMenu 58.1KB 58.1KB +2B 9.5KB 9.5KB 0B
core/Field 44.2KB 44.2KB -12B 8.0KB 8.0KB 0B
core/FieldLabel 39.8KB 39.9KB +74B 6.5KB 6.5KB 0B
core/FileInput 54.1KB 54.1KB 0B 10.3KB 10.3KB 0B
core/IconButton 43.3KB 43.3KB -54B 9.2KB 9.2KB 0B
core/InputGroup 44.8KB 44.8KB -9B 8.0KB 8.0KB 0B
core/LayerProvider 63.3KB 63.3KB +17B 10.1KB 10.1KB 0B
core/Lightbox 51.2KB 51.2KB -36B 9.8KB 9.8KB 0B
core/Link 43.5KB 43.5KB -28B 7.6KB 7.6KB 0B
core/ListItem 53.5KB 53.5KB -15B 10.5KB 10.5KB 0B
core/Markdown 144.5KB 144.4KB -87B 17.4KB 17.4KB 0B
core/MetadataList 23.1KB 23.2KB +101B 3.6KB 3.6KB 0B
core/MobileNav 51.0KB 51.0KB +14B 10.1KB 10.1KB 0B
core/MobileNavToggle 46.2KB 46.2KB -2B 9.4KB 9.4KB 0B
core/MoreMenu 72.9KB 72.9KB +6B 14.7KB 14.7KB 0B
core/MultiSelector 76.2KB 76.2KB -11B 14.6KB 14.6KB 0B
core/NumberInput 55.7KB 55.8KB +71B 10.7KB 10.7KB 0B
core/Outline 28.6KB 28.7KB +124B 4.0KB 4.0KB 0B
core/Pagination 87.7KB 87.8KB +62B 15.3KB 15.3KB 0B
core/Popover 49.8KB 49.8KB -29B 12.1KB 12.1KB 0B
core/PowerSearch 155.1KB 155.1KB +26B 18.6KB 18.6KB 0B
core/RadioList 45.6KB 45.6KB +18B 8.0KB 8.0KB 0B
core/RadioListItem 52.1KB 52.2KB +6B 9.7KB 9.7KB 0B
core/ResizeHandle 25.1KB 25.2KB +123B 3.9KB 3.9KB 0B
core/SegmentedControl 38.8KB 38.9KB +126B 6.3KB 6.3KB 0B
core/Selector 79.1KB 79.1KB -11B 15.0KB 15.0KB 0B
core/SideNav 58.0KB 58.0KB -12B 10.6KB 10.6KB 0B
core/SideNavCollapseButton 46.6KB 46.6KB -68B 9.5KB 9.5KB 0B
core/SideNavHeading 54.7KB 54.8KB +42B 10.1KB 10.1KB 0B
core/SideNavItem 54.5KB 54.5KB +2B 10.1KB 10.1KB 0B
core/Slider 48.5KB 48.5KB +9B 8.5KB 8.5KB 0B
core/Spinner 26.3KB 26.3KB -44B 7.5KB 7.5KB 0B
core/Step 32.4KB 32.5KB +105B 5.3KB 5.3KB 0B
core/Stepper 47.4KB 47.3KB -58B 9.6KB 9.6KB 0B
core/Switch 48.4KB 48.4KB -5B 9.3KB 9.3KB 0B
core/TabList 40.2KB 40.3KB +137B 7.2KB 7.2KB 0B
core/TabMenu 51.6KB 51.6KB -41B 10.0KB 10.0KB 0B
core/Table 84.0KB 84.0KB +54B 15.1KB 15.1KB 0B
core/TableCell 78.8KB 78.8KB -4B 14.8KB 14.8KB 0B
core/TableHeaderCell 78.6KB 78.6KB -5B 14.8KB 14.8KB 0B
core/TableSelectionToolbar 52.4KB 52.4KB +14B 11.2KB 11.2KB 0B
core/TextArea 49.4KB 49.4KB +20B 9.4KB 9.4KB 0B
core/TextInput 53.5KB 53.5KB +10B 10.5KB 10.5KB 0B
core/Thumbnail 50.7KB 50.7KB +27B 11.0KB 11.0KB 0B
core/TimeInput 86.8KB 86.9KB +44B 13.8KB 13.8KB 0B
core/Timestamp 27.5KB 27.6KB +53B 9.5KB 9.5KB 0B
core/Toast 59.8KB 59.9KB +18B 9.7KB 9.7KB 0B
core/ToggleButton 44.1KB 44.1KB -34B 9.3KB 9.3KB 0B
core/Token 29.6KB 29.7KB +152B 4.5KB 4.5KB 0B
core/Tokenizer 64.8KB 64.8KB -16B 11.7KB 11.7KB 0B
core/Toolbar 39.8KB 39.9KB +156B 7.7KB 7.7KB 0B
core/TopNav 51.9KB 51.9KB +2B 10.4KB 10.4KB 0B
core/TopNavHeading 54.2KB 54.2KB +34B 9.9KB 9.9KB 0B
core/TopNavMegaMenu 54.2KB 54.2KB +30B 10.8KB 10.8KB 0B
core/TopNavMenu 53.6KB 53.6KB +20B 10.1KB 10.1KB 0B
core/TreeList 32.5KB 32.6KB +43B 4.7KB 4.7KB 0B
core/Typeahead 61.5KB 61.5KB +1B 11.3KB 11.3KB 0B

Sizes are gzip. Import counts the JavaScript the import loads eagerly; CSS counts the package stylesheet rules referenced by the modules it can load from its own package, lazily loaded ones included.

Bundle Size Summary

Gzip figures measure each package's entry file, not the code an import pulls in.

Package Entry (ESM) Entry gzip before Entry gzip after Delta
@astryxdesign/core 4.9KB 1.2KB 1.2KB 0B
Package stylesheet Gzip before Gzip after Delta
@astryxdesign/core 34.6KB 34.6KB 0B

Accessibility Audit

Status: No accessibility violations detected.

Visual Regression

55 of 502 shot(s) changed.

A change here is a question, not a failure: check whether the after is the
picture you intended. Record that review on the PR. Baseline maintenance is an
explicit dispatch of CI; this report never rewrites the baseline or adds a release gate.

component story theme mode pixels
Selector Indicator Space Bottom Sheet Narrow End RTL neutral light 753,933
Selector Indicator Space Bottom Sheet Wide Start neutral light 740,072
RichTextEditor Markdown parity: side by side probe dark 558,840
RichTextEditor Markdown parity: side by side probe light 540,756
Chat Full AI Chat probe light 230,823
Chat Full AI Chat probe dark 229,805
AppShell With Mobile Nav probe light 217,010
AppShell With Mobile Nav probe dark 216,960
Collapsible Dividers probe dark 66,445
Collapsible Dividers probe light 66,445
AppShell Playground neutral dark 65,198
AppShell Playground neutral light 63,788
Chat Default neutral dark 36,304
Chat Default neutral light 35,767
Chat Empty State probe light 23,531
Chat Empty State probe dark 22,622
ChatAutoScroll Scroll Behavior Comparison neutral dark 15,378
ChatAutoScroll Scroll Behavior Comparison neutral light 14,107
Chat With Attachments probe light 13,406
Chat With Attachments probe dark 11,232

and 35 more.


Generated by PR Enrichment workflow | Storybook | Sandbox | View full report

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This is on hold for a maintainer decision: stacked on #7357, this branch makes a value rendered straight into InternationalizationContext (or its .Provider) stop configuring useLocale, useDirection and useCollator, and component:InternationalizationProvider lists the provider's context behavior as stable public API. That record's owners need to update it to accept the retirement (including that a custom translate has no provider input and that this codemod is the migration) or reject it, and the [breaking] entry also needs main to declare the minor before it can merge. Separately, one fix is needed: packages/cli/assets/codemods/transforms/next/index.mjs still exports an empty list, so astryx upgrade never runs migrate-internationalization-context; please restore the manifest entry that was dropped when the codemod moved out of #7357.

[Automated review]

'@astryxdesign/cli': patch
---

[feat] `astryx upgrade` runs `migrate-internationalization-context`, which rewrites `<InternationalizationContext value={…}>` (or its `.Provider`) to `<InternationalizationProvider locale dir messages overrides>`: a variable maps `locale`, `direction` (as `dir`) and `messages` — the fields every context value has — and its TODO names `overrides` and `translate`, which the variable's type may lack and the provider has no input for; an inline object literal maps key by key, `overrides` included, with a TODO for `translate` (a literal's `translate` key is kept beside it). Any other value — a call, a spread into a literal, a conditional — is left unchanged behind a TODO saying why. It rewrites JSX whose name resolves, unshadowed, to an Astryx import (named, aliased, or namespace), reuses the file's own provider import name, and leaves a reader of the context alone.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This says astryx upgrade runs the transform, but packages/cli/assets/codemods/transforms/next/index.mjs still exports [] and the codemod registry runs only manifest entries, so at this head the transform never runs. Restoring the manifest entry, dropped when the codemod moved out of #7357, makes this sentence true.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Registered in bb41827 — the split had dropped the manifest entry, which is exactly the kind of gap that ships silently. transforms/next/index.mjs now exports {name: 'migrate-internationalization-context', transform, meta}, and the fixture file opens with a test that the manifest lists the transform by name with the same transform and meta identities.

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This is on hold for a maintainer decision: stacked on #7357, this branch makes a value rendered straight into InternationalizationContext (or its .Provider) stop configuring useLocale, useDirection and useCollator, while component:InternationalizationProvider lists the provider's context behavior as stable public API. That record's owners need to update it to accept the retirement (including that a custom translate has no provider input and that this transform is the migration) or reject it, and main must declare the minor before the [breaking] entry can merge. The restored manifest entry and its test hold, so astryx upgrade will run the transform once it is promoted. Once the decision is made, one change remains: against main this branch is 2,015 authored lines because it carries #7357, so please land #7357 first and rebase, leaving this pull request's own 1,202-line commit.

[Automated review]

@vjeux
vjeux force-pushed the perf/i18n-context-codemod branch from bb41827 to fd3e204 Compare October 23, 2026 17:28

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This is on hold for a maintainer decision: stacked on #7357, this branch makes a value rendered straight into InternationalizationContext (or its .Provider) stop configuring useLocale, useDirection and useCollator, while component:InternationalizationProvider lists the provider's context behavior as stable public API. That record's owners need to update it to accept the retirement (including that a custom translate has no provider input and that this transform is the migration) or reject it, and main must declare the minor before the [breaking] entry can merge. The transform itself holds up: its fixtures, the tsc case and a story it rewrote, which renders identically to the base, cover the migration. Once the decision is made, one change remains: against main this branch is 2,050 authored lines because it carries #7357, so please land #7357 first and rebase, leaving this pull request's own 1,202-line commit.

[Automated review]

vjeux added 8 commits October 25, 2026 03:46
resolve.ts imported the whole shipped English catalog (408 keys, 78 component
namespaces, 22.6 KB raw / 6 KB gzip) as the fallback of every translation, so
any app rendering one component that calls t() bundled the strings of the
other hundred.

This is the runtime half, with no change for an app yet; the components move
to it in a follow-up. The generator also emits
generated-locales/en/<namespace>.generated.ts, one per key namespace, and the
exports map gives ./locales/en/* a null target so the slices stay the
package's own while every whole catalog stays importable. resolve.ts imports
no catalog: the English fallback is the caller's, passed per call.
useComponentTranslator(slice) is the hook a core component renders with; it,
useLocale, useDirection and useCollator read a new internal
TranslationRuntimeContext that carries the resolver and no catalog. The
public InternationalizationContext keeps its shape and its `translate` bound
to the whole catalog, the provider writes both, and useTranslator() reads the
public one as before, so a consumer's direct `translate(key)` and
`useTranslator()` behave exactly as today; the provider's messages and
overrides still win over the fallback.

check:i18n-catalog gains the slice-coverage rule: every @AstryX.<ns>.* key a
slice-rendering file reads must belong to a namespace its slice carries, and
a file handed `t` is covered by its component directory's slices. Tests pin
the resolver, the hook, the public context boundary (a direct `translate` of
a core key with no provider → the English string), the compiled import graph
of the runtime, the export surface through Node's package resolution, the
generator split and the coverage rule. CONTRIBUTING and the hardcoded-string
lint messages describe the component pattern; the CLI's i18n guide keeps to
what a caller can use.
A value rendered straight into the public InternationalizationContext, without
InternationalizationProvider, drove useLocale, useDirection, useCollator and
every component's strings in the last release; with the hooks reading the
provider's runtime context it reaches none of them. The changeset says so at
the breaking tier, with the migration (mount the provider), and the context
module's header says which readers each context reaches. The shipped i18n
guide qualifies which apps the slicing saves bytes for, and carries its own
docs changeset.
…vider

`astryx upgrade` runs migrate-internationalization-context: a
<InternationalizationContext value={v}> (or .Provider) becomes
<InternationalizationProvider locale={v.locale} dir={v.direction}
messages={v.messages} overrides={v.overrides}>; an inline object maps key by
key and a `translate` key, which has no provider input, stays behind a TODO.
Only JSX bound to an Astryx import is rewritten; a file that only reads the
context is left alone. The breaking changeset now carries the full migration:
direction → dir, an own `translate` → messages/overrides, and the codemod.
…; qualify the bundle claim

At this revision a raw context value still drives the components' strings
through useTranslator(); only useLocale, useDirection and useCollator — and
the components reading the locale through them — stop following it, and the
sweep's changeset takes the strings when the components move. CONTRIBUTING
names the apps the slicing saves bytes for.
…e safely

Per the consumer file-modification contract: an element is rewritten only
when its name resolves, unshadowed, to the Astryx import (a shadowed local
and a type-only import prove nothing); a variable value maps field by field
and an inline object literal key by key, each with a TODO for the value's
`translate`, which the provider has no input for; any other value — a call,
a spread into a literal, a conditional — is left unchanged behind a TODO
that says why, since a call would run once per prop and a spread of the
ambient value would mount a provider without the app's catalogs. An existing
aliased provider import is reused instead of referencing an unbound name. A
second run changes nothing. Fixtures cover each case, including idempotence,
shadowing and type-only imports; both changesets say what the transform
leaves behind, and the breaking entry's component list is corrected.
The generic scope model gives a block no scope of its own, so a same-named
local declared in a module-level block was rewritten into the provider and a
real use beside a block-level redeclaration inside a function was skipped.
The binding check now walks from the element to the module and treats a
declaration of the name in any block, switch, loop head, function parameter
list, named function expression or catch clause on the way as a shadow.
Fixture: four shadowed uses stay, the one unshadowed use beside them is
rewritten, and the import is kept.
…and sees hoisted vars

A module that already binds InternationalizationProvider — its own wrapper
of that name, or an import from its own module — now gets the Astryx
provider under a free alias instead of a second binding of the same name.
A `var` anywhere in the enclosing function (a nested block, a loop head) and
a named class expression's own name shadow the import like a parameter
does. Fixtures for each shape.
…m the runtime half

The provider name written must itself resolve to the provider's value import
at the edit site: a `type` specifier of the provider no longer counts as its
import, a local that shadows the provider import leaves the element behind
the TODO, and an element whose value arrives through spread props gets the
same TODO. Fixtures for each.

The lint rule's message edits stay; its formatting reflow is dropped. The
slice-coverage rule of check:i18n-catalog moves to the component sweep,
where its first callers land.
vjeux added 14 commits October 25, 2026 03:46
The codemod changeset, the breaking entry and the transform's description
match the transform: a variable maps locale, dir and messages and leaves
overrides and translate to the TODO; an inline literal maps key by key.
…alone

A code search found three uses of InternationalizationContext outside
Astryx (two internal, one public), all of one shape: read the ambient value
with useContext, re-mount InternationalizationProvider with a derived one.
The transform over those three files returned no change; this fixture is
that shape.
… provider

When a local shadows the provider import at the edit site, the TODO said the
value was an expression; it now names the shadowing local as the cause.
… on its attribute

An empty value={{}} has nothing to map; its TODO sat on the opening element,
where the printer rendered it as text between the tags. It now stays on the
value attribute, as a comment.
…blocks and namespace bodies

A class private method's parameter, a declaration in a class `static {}`
block and one in a TypeScript `namespace` body shadow the import like any
other scope on the way to the module; a `var` search no longer crosses into a
private method or a static block. Fixture for each. The context and codemod
headers describe a component as reading the runtime context once it adopts
its slice, not before.
…shadow; drops an emptied import

`export const Ctx` inside a namespace body declares the name like a bare
`const`; the statement-list check now looks through `export`. An Astryx
import declaration the transform leaves with no specifier is removed instead
of staying behind as a bare side-effect import. Fixtures for both.
…ions; keeps an emptied import's comments

In a namespace body, `import Ctx = Other.Context`, `declare function Ctx`
and `enum Ctx` declare the name like a const does. When an emptied Astryx
import is removed, the comments that sat on it (a license header) move to
the next statement, and the printer's stray semicolon after a directive is
taken out, so `'use client';` stays as written. Fixtures for each.
`namespace Ctx {}` inside a namespace body declares the name, so
`<Ctx.Provider value={v}>` under it stays as written; and a module that
declares `namespace InternationalizationProvider` binds that name, so the
provider import takes a free alias and the rewritten element renders the
alias rather than the namespace. Fixtures for both.
…own name

Inside `namespace Outer.Ctx {}`, `Ctx` names that namespace, so a
`<Ctx.Provider value={v}>` there stays as written, and inside
`namespace Outer.i18n {}` the namespace-import form is left alone rather
than becoming an unbound member. A TSModuleDeclaration on the element's path
binds its last segment. Fixture beside the namespace cases.
…talog

The contributing guide's bundle sentence names the third way an app pulls
in every string: the public context module imports the English catalog for
its default translate.
JSX attributes apply in order, so in `<InternationalizationContext value={v}
{...props}>` a `props.value` wins and the effective value is not provable
from the source; the element now stays as written behind a TODO that says
so. A spread BEFORE `value` is overridden by it and is kept in place: the
mapped props take `value`'s slot in the attribute list, so the spread still
comes first and is still overridden. Fixtures for both orders, and for a
spread on each side.
The upgrade transform, its fixtures and its changeset leave this PR so the
runtime half stays within the review size; the breaking entry points at
the stacked PR that carries them. CONTRIBUTING's bundle sentence names
PowerSearch, whose caller-supplied operator keys keep the whole catalog.
… will

The core changelog takes this entry verbatim, so it names the `astryx upgrade`
transform and what it rewrites rather than pointing at the pull request that
ships it. CONTRIBUTING's PowerSearch clause moves to the component sweep,
where the hook it names exists.
…stays the package's own

`astryxStylex()` aliased `@astryxdesign/core/locales/(.+).generated.js`
straight to the source tree, which let a Vite app resolve the per-namespace
English slices the exports map keeps out of reach with a `null` target.
The capture now takes a single segment, the whole-catalog case; a slice
specifier under `locales/en/` resolves to nothing the package ships. Test
for the slice specifier beside the existing catalog cases.

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This is on hold for an engineering-owner decision: stacked on #7357, this branch makes a value rendered straight into InternationalizationContext (or its .Provider) stop configuring useLocale, useDirection and useCollator, while component:InternationalizationProvider lists the provider's context behavior as stable public API. An engineering owner (the author may, as one) records the decision by commenting /approve-spec <full head SHA> on this pull request, accepting the retirement (including that a custom translate has no provider input and that this transform is the migration) with that record's Compatibility and migration claim amended on main, or rejects it; main must also declare the minor before the [breaking] entry can merge. The transform itself holds up: its fixtures, the tsc case and a story it rewrote, which renders identically to the base, cover the migration. Once the decision is made, one change remains: against main this branch is 2,050 authored lines because it carries #7357, so please land #7357 first and rebase, leaving this pull request's own 1,202-line commit.

[Automated review]

vjeux added 2 commits October 25, 2026 06:27
…erence

The docsite's server routes trace this package's files, and the test ran
its resolver from apps/sandbox, a path the tracer follows: the search
route then shipped the whole sandbox app (190.3 MB, over the docsite's
150 MB function budget), so every preview deploy of this branch failed.
The package's self-reference applies the same exports map a consumer's
import does, so the test now resolves from the package's own directory;
the route is back to 59.0 MB (main: 58.9 MB), and the test still fails
when the ./locales/en/* null export is removed.
…vider

`astryx upgrade` runs `migrate-internationalization-context`, the codemod
for the retirement that the stacked runtime change declares: a
`<InternationalizationContext value={v}>` (or `.Provider`) whose value is a
variable maps `locale`, `direction` (as `dir`) and `messages` to
`<InternationalizationProvider>` with a TODO for `overrides` and
`translate`; an inline object literal maps key by key with a TODO for
`translate`; any other value — a call, a conditional, a literal with a
spread, an empty literal — and any element that spreads props stay as
written behind a TODO that says why (a spread after `value` may override
it; a spread anywhere may carry `dir`, `messages` or `overrides`, which the
context ignored but the provider would read). Only an element whose name
resolves, unshadowed, to the Astryx import is touched: blocks, loop heads,
catch clauses, parameters (private methods too), hoisted `var`s, named
class and function expressions, `static {}` blocks, namespace bodies
(including `export`, `import X =`, `declare function`, `enum`, nested and
dotted namespaces) all shadow it; a type-only import proves nothing. The
provider is imported under a free alias when the module already binds its
name, and an emptied import goes with its comments carried forward and
the directive left intact. A second run changes nothing.

Fixtures cover each shape; one typechecks the rewritten output with tsc
against the real i18n source types; one is the read-and-re-mount shape every
real consumer found by a code search has (the transform leaves it alone).

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This is on hold for an engineering-owner decision: stacked on #7357, this branch makes a value rendered straight into InternationalizationContext (or its .Provider) stop configuring useLocale, useDirection and useCollator, while component:InternationalizationProvider lists the provider's context behavior as stable public API. An engineering owner (the author may, as one) records the decision by commenting /approve-spec <full head SHA> on this pull request, accepting the retirement (including that a custom translate has no provider input and that this transform is the migration) with that record's Compatibility and migration claim amended on main, or rejecting it; main must also declare the minor before the [breaking] entry can merge. If the retirement is accepted, two changes remain:

  • In .js and .jsx files the transform turns an accessor or method in an inline value literal into an unparseable or function-valued prop; admitting only plain properties in its literal check, with a babel-parser fixture, fixes it.
  • Against main this branch is 2,054 authored lines because it carries #7357; please land #7357 first and rebase, leaving this pull request's own 1,202-line commit.

[Automated review]

Comment on lines +512 to +520
value.type === 'ObjectExpression' &&
value.properties.every(
(/** @type {any} */ p) =>
(p.type === 'Property' || p.type === 'ObjectProperty') &&
!p.computed &&
(p.key.type === 'Identifier' ||
p.key.type === 'StringLiteral' ||
p.key.type === 'Literal'),
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

In .js, .jsx, .mjs and .cjs files the runner parses with babel (run-codemod.mjs:381), where an accessor or a method is a Property with kind: 'get' or method: true, so this check admits it. get locale() { return store.locale; } becomes locale={() { return store.locale; }}, which the runner's re-parse rejects, failing the whole file without a TODO, and messages() { return m; } becomes messages={function() { return m; }}. The same literals in a .tsx file get the TODO. Admitting only plain properties fixes it:

(p.type === 'ObjectProperty' ||
  (p.type === 'Property' && p.kind === 'init' && !p.method)) &&

A fixture run through jscodeshift.withParser('babel') would cover that path; every case today uses tsx.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 5b4e841: the literal check admits only plain key: value entries, so a getter or a method in an inline value keeps the expression TODO, as a call or a spread does. A fixture parsed with the babel parser covers both and fails without the fix.

# Conflicts:
#	packages/cli/assets/codemods/transforms/next/index.mjs
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 27, 2026
# Conflicts:
#	packages/core/src/Markdown/Markdown.tsx

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This is on hold for an engineering-owner decision: stacked on #7357, this branch makes a value rendered straight into InternationalizationContext (or its .Provider) stop configuring useLocale, useDirection and useCollator, while component:InternationalizationProvider lists the provider's context behavior as stable public API. An engineering owner (the author may, as one) records the decision by commenting /approve-spec <full head SHA> on this pull request, accepting the retirement (including that a custom translate has no provider input and that this transform is the migration) with that record's Compatibility and migration claim amended on main, or rejecting it; main must also declare the minor before the [breaking] entry can merge. If the retirement is accepted, two changes remain:

  • In .js and .jsx files the transform turns a getter or method in an inline value literal into an unparseable or function-valued prop; admitting only plain properties in its literal check, with a babel-parser fixture, fixes it.
  • Against main this branch is 2,050 authored lines because it carries #7357; please land #7357 first and rebase, leaving this pull request's own 1,198 lines.

[Automated review]

…ehind the TODO

In .js files the runner parses with babel, which reports an accessor or a
method in an object literal as a Property (kind 'get', method: true). The
literal check admitted them, so a getter for locale became a
function-valued prop. Only plain key: value entries are mapped now; any
other literal keeps the expression TODO, as a call or a spread does.

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This is on hold for an engineering-owner decision: stacked on #7357, this branch makes a value rendered straight into InternationalizationContext (or its .Provider) stop configuring useLocale, useDirection and useCollator, while component:InternationalizationProvider lists the provider's context behavior as stable public API. An engineering owner (the author may, as one) records the decision by commenting /approve-spec <full head SHA> on this pull request, accepting the retirement (including that a custom translate has no provider input and that this transform is the migration) with that record's Compatibility and migration claim amended on main, or rejecting it; main must also declare the minor before the [breaking] entry can merge. The transform itself holds up: getter and method literals in .js files now stay behind the TODO, and its fixtures, its tsc case and a story it rewrote, which renders like the base, cover the migration. Once the decision is made, one change remains: against main this branch is 2,077 authored lines because it carries #7357, so please land #7357 first and merge or rebase main, leaving this pull request's own 1,225 lines.

[Automated review]

@vjeux
vjeux marked this pull request as draft October 29, 2026 22:29

This branch was successfully deployed

1 active deployment
Preview — 5b4e841d Deployed Oct 29, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Meta Open Source bot. needs:design-review Affects visuals — Design should review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant