Skip to content

feat: Add minimal primitives for userland internationalization (i18n) packages #779

@hoteye

Description

@hoteye

Problem Statement

Internationalization is a common need for CLI tools built with Ink, but current architecture makes it challenging for userland i18n packages to work correctly:

Current Pain Point:

// This fails in Ink due to reconciler constraints
import { Trans } from 'react-i18next';
<Trans i18nKey="welcome" components={{ bold: <Text bold /> }} />
// Error: Text string must be rendered inside <Text> component

Root Cause:
Ink's reconciler requires all text nodes to have isInsideText: true context, but existing i18n libraries like react-i18next generate text nodes that violate this constraint.

1. Implementation Principles

Minimal Primitives Approach

Instead of built-in i18n support, expose minimal primitives that enable robust userland solutions. This approach:

  • Keeps Ink core focused and lightweight
  • Provides necessary building blocks for community
  • Maintains architectural integrity
  • Enables multiple i18n frameworks to coexist

Two-Phase Strategy

Why split implementation?

  • Risk mitigation: Start with low-risk foundation APIs
  • Community feedback: Learn from Phase 1 usage before advanced features
  • Incremental value: Immediate benefits while building toward complete solution
  • Review efficiency: Smaller PRs are easier to review and merge

Expected Outcome: Enable community packages like ink-i18next, ink-react-intl to work seamlessly:

import { InkTrans } from 'ink-i18next';
<InkTrans i18nKey="welcome" components={{ bold: <Text bold /> }} />
// Works perfectly with Ink's constraints

Technical Context:

  • Reconciler constraint: reconciler.js:126-130 validates text context
  • Text merging: squash-text-nodes.js combines adjacent text nodes
  • Transform pipeline: internal_transform handles styling

Implementation Plan

Step 1: Foundation APIs PR (low risk, immediate value)
Step 2: Advanced utilities PR (based on community feedback)
Step 3: Community adoption and ecosystem growth

References


Would appreciate feedback on this approach before starting implementation!

2. Phase 1: Foundation APIs

1. Text Fragment Composition API

interface TextFragment {
  text: string;
  transform?: (text: string, index: number) => string;
}

export function composeTextFragments(
  fragments: TextFragment[]
): string

Purpose: Safely combine multiple text pieces while respecting Ink's text merging mechanism.

Example:

const result = composeTextFragments([
  { text: "Hello " },
  { text: "World", transform: (text) => chalk.bold(text) },
  { text: "!" }
]);

2. Text Context Validation API

interface TextContextInfo {
  isInsideText: boolean;
  validateTextPlacement: (node: ReactNode) => boolean;
}

export function createTextContext(): TextContextInfo

Purpose: Allow userland packages to validate text placement and avoid reconciler errors.

3. Phase 2: Advanced Utilities

1. Styled Text Parser API

interface StyledTextComponent {
  tag: string;
  component: ComponentType<{ children: ReactNode }>;
}

export function parseStyledText(
  template: string,
  components: Record<string, StyledTextComponent>,
  variables?: Record<string, any>
): ReactElement

Purpose: Parse template strings with XML-like tags and convert to Ink-compatible component trees.

Example:

const result = parseStyledText(
  "Hello <bold>{name}</bold>, you have <count>{count}</count> messages",
  { 
    bold: { tag: 'bold', component: BoldText }, 
    count: { tag: 'count', component: CountText } 
  },
  { name: "Alice", count: 5 }
);

2. Transform Pipeline API

export function createTransformPipeline(
  transforms: Array<(text: string, index: number) => string>
): (text: string, index: number) => string

export function combineTransforms(
  baseTransform?: (text: string, index: number) => string,
  additionalTransforms: Array<(text: string, index: number) => string>
): (text: string, index: number) => string

Purpose: Extend Ink's internal_transform mechanism for complex text processing pipelines.

Impact & Assessment

Benefits: Unblocks existing i18n pain points, enables community packages, maintains backward compatibility.

Risks: Phase 1 (low) - pure utilities; Phase 2 (medium) - complex parsing logic.

Success Criteria:

  • Phase 1: APIs merged, community adoption, zero regressions
  • Phase 2: Multiple i18n frameworks supported, documentation available

This proposal aims to solve internationalization challenges while maintaining Ink's architectural principles and community-driven ecosystem approach.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions