Skip to content

Craft 2055 - Remove react-intl devDep & implement custom i18n extraction#987

Merged
valoriecarli merged 18 commits intomainfrom
CRAFT-2055-react-intl-ya-really-gone
Jan 30, 2026
Merged

Craft 2055 - Remove react-intl devDep & implement custom i18n extraction#987
valoriecarli merged 18 commits intomainfrom
CRAFT-2055-react-intl-ya-really-gone

Conversation

@valoriecarli
Copy link
Collaborator

@valoriecarli valoriecarli commented Jan 29, 2026

  • This PR completes the react-intl@internationalized/string migration started in CRAFT-2029- Migrate from react-intl to compile-time message parsing #841.
  • The final react-intl dep is removed, all defineMessages imports are eliminated & @formatjs/cli is replaced with a custom extraction script.
  • Generated i18n files (144 total) are now gitignored to follow best practices for generated code.
  • All documentation has been updated to fix incorrect library references & clarify the new compile-time i18n architecture.

TL;DR: react-intl is completely gone, extraction is now custom & cleaner, generated files are gitignored & all docs have been updated.


Major Change:

Created custom extraction script (packages/i18n/scripts/extract-messages) to remove the react-intl devDep.
.i18n files are now plain TS objects with no external dependencies. They exist solely for extraction purposes & are never imported by components at runtime.

We now:

  • Dynamically import .i18n files & read the messages export
  • Output Tx JSON format (packages/i18n/data/core.json)
  • Keep it simple, maintainable
  • No external CLI dependencies

Before:

import { defineMessages } from "react-intl";                                                                                                                                       
                                                                                                                                                                                    
 export const messages = defineMessages({                                                                                                                                           
   dismiss: {                                                                                                                                                                       
     id: "Nimbus.Alert.dismiss",                                                                                                                                                    
     description: "aria-label for the dismiss button",                                                                                                                              
     defaultMessage: "Dismiss",                                                                                                                                                     
   },                                                                                                                                                                               
 });   

After:

  export const messages = {                                                                                                                                                          
    dismiss: {                                                                                                                                                                       
      id: "Nimbus.Alert.dismiss",                                                                                                                                                    
      description: "aria-label for the dismiss button",                                                                                                                              
      defaultMessage: "Dismiss",                                                                                                                                                     
    },                                                                                                                                                                               
  };                                                                         

Other goodies in the PR include (but not limited to):

  • Gitignore generated i18n Files (144 files)
  • Tracked down all references to i18n (documents, templates, etc) & made updates accordingly.
  • Updated i18n charts & converted to to Mermaid format.

@vercel
Copy link

vercel bot commented Jan 29, 2026

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

Project Deployment Actions Updated (UTC)
nimbus-documentation Ready Ready Preview, Comment Jan 30, 2026 4:20pm
nimbus-storybook Ready Ready Preview, Comment Jan 30, 2026 4:20pm

Request Review

@changeset-bot
Copy link

changeset-bot bot commented Jan 29, 2026

🦋 Changeset detected

Latest commit: b709bca

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 3 packages
Name Type
@commercetools/nimbus Major
@commercetools/nimbus-tokens Major
@commercetools/nimbus-icons Major

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@valoriecarli valoriecarli changed the title Remove react-intl dev dependency and implement custom i18n extraction Craft 2055 - Remove react-intl devDep & implement custom i18n extraction Jan 29, 2026
@valoriecarli valoriecarli self-assigned this Jan 29, 2026
@valoriecarli valoriecarli requested review from a team, ByronDWall, ddouglasz, jaikamat, misama-ct, stephsprinkle and tylermorrisford and removed request for a team January 29, 2026 17:52
@commercetools commercetools deleted a comment from claude bot Jan 29, 2026
@valoriecarli
Copy link
Collaborator Author

@claude do an initial review

@claude
Copy link

claude bot commented Jan 29, 2026

Claude finished @valoriecarli's task in 2m 26s —— View job


Initial Review of PR #987

I've completed an initial code review of the react-intl removal and custom i18n extraction implementation. Overall, this is an excellent migration that successfully removes the react-intl dependency and implements a clean, maintainable custom extraction approach.


Summary

Scope: 100 files changed (+2,296/-3,529)

  • ✅ Removed react-intl devDependency completely
  • ✅ Created custom extraction script (106 lines, well-structured)
  • ✅ Converted 24 .i18n.ts files to plain TypeScript objects
  • ✅ Deleted 144 generated i18n files (now gitignored)
  • ✅ Updated extensive documentation across the codebase

✅ Strengths

1. Clean Custom Extraction Script (packages/i18n/scripts/extract-messages.ts)

  • Simple, maintainable implementation using dynamic imports
  • Clear error handling with exit codes
  • Validates required fields (id, description, defaultMessage)
  • Alphabetically sorts output for consistent git diffs
  • Good logging with emojis for readability
  • No external CLI dependencies (just glob and fs)

2. Consistent .i18n.ts Conversion Pattern

All 24 files converted correctly:

// Before: defineMessages wrapper
import { defineMessages } from "react-intl";
export const messages = defineMessages({ ... });

// After: Plain TypeScript object
export const messages = { ... };

3. Comprehensive Documentation Updates

  • ✅ Updated all references to remove react-intl mentions
  • ✅ Corrected library references throughout (README, CLAUDE.md, guidelines)
  • ✅ Updated Mermaid diagrams for i18n workflow
  • ✅ Fixed skill documentation (writing-i18n, writing-main-component)
  • ✅ Updated OpenSpec files with accurate descriptions

4. Proper Git Hygiene

  • ✅ Added 144 generated files to .gitignore:
    • packages/nimbus/src/components/**/intl/
    • packages/nimbus/src/components/**/*.messages.ts
  • ✅ Follows best practices for generated code
  • ✅ Includes clear regeneration instructions in docs

5. Script Integration

Updated extract-intl command correctly:

// Before:
"extract-intl": "pnpm dlx @formatjs/cli extract ... && pnpm --filter @commercetools/nimbus-i18n build"

// After:
"extract-intl": "tsx packages/i18n/scripts/extract-messages.ts && pnpm --filter @commercetools/nimbus-i18n build"

🔍 Minor Observations & Questions

1. Changeset Severity

The changeset is marked as minor, but this might be considered a major change depending on interpretation:

  • Arguments for minor:
    • react-intl was only a devDependency (not affecting consumers)
    • Runtime API remains unchanged (components still use @internationalized/string)
    • No breaking changes to consumer code
  • Arguments for major:
    • Significant internal architecture change
    • Generated files no longer tracked in git (could affect contributors)
    • Different extraction workflow for maintainers

Recommendation: The current minor designation seems reasonable since consumer-facing APIs are unchanged. However, you may want to document the contributor workflow changes prominently in release notes.

2. Error Handling in Extraction Script

The script uses catch blocks effectively, but consider these edge cases:

Line 48: Dynamic import could fail with module resolution errors:

const module = await import(file);

Current error handling catches this, but the error message might not be specific enough. Consider:

} catch (error) {
  errorCount++;
  console.error(`  ✗ ${path.relative(process.cwd(), file)}`);
  console.error(`    Error: ${error instanceof Error ? error.message : String(error)}`);
  // Consider: More specific messages for import failures vs validation failures
}

Line 59-61: Field validation could provide more specific errors:

if (!msg.id || !msg.defaultMessage || !msg.description) {
  throw new Error(`Invalid message "${key}": missing required fields`);
}

Consider specifying which field(s) are missing.

3. Glob Pattern Assumption

Line 33: The pattern assumes running from repository root:

const pattern = "{packages,src/components}/**/*.i18n.ts";

This works when run via pnpm extract-intl from root, but might be fragile if run directly from packages/i18n/. The cwd: process.cwd() helps, but worth documenting that the script expects to run from repository root.

4. TypeScript Type Safety

Line 57: Type assertion without runtime validation:

const msg = descriptor as MessageDescriptor;

The subsequent validation (lines 59-61) catches invalid messages, but the type assertion happens first. This is fine, but could be more defensive with a type guard.


🎯 Potential Issues (Low Priority)

1. Generated Files Already Committed

The PR shows 60+ deleted intl/ and *.messages.ts files, which means they were previously tracked. The .gitignore now prevents future commits, but:

  • Existing checkouts might have uncommitted generated files after pulling
  • Consider adding a note in the PR description or migration docs about running git clean -fd packages/nimbus/src/components/*/intl/ after merging

2. Shebang Line

Line 1: #!/usr/bin/env tsx
This assumes tsx is available in the environment. The script is always run via tsx packages/i18n/scripts/extract-messages.ts in package.json, so the shebang might be unnecessary. Not an issue, just an observation.


📋 Documentation Accuracy Check

I reviewed documentation updates across:

  • packages/i18n/README.md - Accurate, comprehensive
  • packages/nimbus/README.md - Correctly updated
  • docs/file-type-guidelines/i18n.md - All references fixed
  • docs/component-templates/i18n.template.md - Updated correctly
  • .claude/skills/writing-i18n/SKILL.md - Fixed all examples
  • ✅ OpenSpec files - Accurate descriptions of new architecture

All documentation correctly reflects the new architecture.


✅ Testing Verification

While I can't run tests directly, I verified:

  • ✅ No remaining react-intl imports in source code
  • ✅ No remaining defineMessages calls in source code
  • ✅ All .i18n.ts files follow the new plain object pattern
  • ✅ Extraction script has proper error handling
  • ✅ Build scripts remain intact

🎉 Overall Assessment

This is excellent work! The migration is:

  • Complete: All react-intl references removed
  • Clean: Simple, maintainable custom extraction script
  • Well-documented: Comprehensive updates across all documentation
  • Consistent: All 24 components follow the same pattern
  • Best practices: Generated files properly gitignored

The custom extraction script is significantly simpler than using @formatjs/cli (106 lines vs. external dependency) while maintaining all necessary functionality.


💡 Recommendations

Before Merging:

  1. ✅ Already done: All critical items addressed
  2. Consider: Add a brief migration note about running pnpm extract-intl after pulling (for contributors with stale generated files)

Optional Improvements (Future PRs):

  1. More specific error messages in extraction script (which fields are missing)
  2. Consider adding @types/node if not already present for better TS support in scripts
  3. Add unit tests for extraction script (mock .i18n.ts files, verify output format)

Verdict: ✅ Approved for Merge

This PR successfully completes the react-intl@internationalized/string migration. The custom extraction approach is clean, maintainable, and well-documented. No blocking issues identified.

Great work on this migration! 🚀


Copy link
Contributor

@stephsprinkle stephsprinkle left a comment

Choose a reason for hiding this comment

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

Tested out the script, does the thing as far as I can tell

Copy link
Collaborator

@misama-ct misama-ct left a comment

Choose a reason for hiding this comment

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

Image

I approve.

valoriecarli and others added 10 commits January 30, 2026 10:16
Update spec documentation to clearly state that NimbusI18nProvider is for
date/number formatting (React Aria I18n), not UI text translation.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Fix syntax error in variable message example. The message key must be
properly quoted and variables passed as second parameter.

Before: msg.format("pageInfo, { current: 1, total: 10 }");
After:  msg.format("pageInfo", { current: 1, total: 10 });

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
@valoriecarli valoriecarli force-pushed the CRAFT-2055-react-intl-ya-really-gone branch from 564617c to b709bca Compare January 30, 2026 16:16
Copy link
Contributor

@ByronDWall ByronDWall left a comment

Choose a reason for hiding this comment

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

This is really great work - a true win for nimbus' developer experience.

Also, wow openspec is real real verbose, huh?

@stephsprinkle
Copy link
Contributor

This is really great work - a true win for nimbus' developer experience.

Also, wow openspec is real real verbose, huh?

Boy howdy.

@valoriecarli valoriecarli merged commit 53b01dc into main Jan 30, 2026
9 checks passed
@valoriecarli valoriecarli deleted the CRAFT-2055-react-intl-ya-really-gone branch January 30, 2026 16:25
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.

4 participants