CorentinTh/branchlet

Elegant, extensible syntax for handling pluralization, ranges, and any conditional text logic in i18n strings.

★ 6Forks 0TypeScriptGitHub ↗Compare

README

Header banner

Branchlet - Tiny, extensible i18n string processor

Branchlet provides an elegant, extensible syntax for handling conditional text logic in i18n strings.

Features

  • Intuitive Syntax - Natural, readable conditional expressions
  • Extensible - Create custom matchers for your specific use cases
  • Zero Dependencies - Lightweight, fast and tree-shakeable
  • Pure Functions - Predictable, testable, and composable
  • Framework Agnostic - Works anywhere JavaScript runs

Installation

# Using pnpm
pnpm add @branchlet/core

# Using npm
npm install @branchlet/core

# Using yarn
yarn add @branchlet/core

Quick Start

import { createBranchlet } from '@branchlet/core';
import { eqMatcher, intervalMatcher } from '@branchlet/core/matchers';

const { parse } = createBranchlet();

// Simple pluralization
parse(
  'You have {{ count, =0:no items, =1:one item, many items }} in your cart.',
  { count: 0 }
);
// → "You have no items in your cart."

// Range-based conditions
parse(
  'Your score is {{ score, [0-50]:bad, [51-75]:good, [76-100]:excellent }}!',
  { score: 85 }
);
// → "Your score is excellent!"

Core Concepts

Templates

Templates use double curly braces {{ }} for expressions:

branchlet.parse('Hello {{ name }}!', { name: 'World' });
// → "Hello World!"

Conditional Branches

Expressions can contain conditional branches with the syntax: {{ variable, condition1:result, condition2:result, fallback }}

branchlet.parse(
  'Status of the task: {{ status, =active:Active, =pending:Pending, Unknown }}',
  { status: 'active' }
);
// → "Active"

Built-in Matchers

Branchlet includes several ready-to-use matchers:

  • eqMatcher - Equality: =value
  • neqMatcher - Inequality: !=value
  • gtMatcher - Greater than: >value
  • gteMatcher - Greater than or equal: >=value
  • ltMatcher - Less than: <value
  • lteMatcher - Less than or equal: <=value
  • intervalMatcher - Ranges: [min-max]
import { createBranchlet } from '@branchlet/core';
import {
  eqMatcher,
  gteMatcher,
  ltMatcher,
  intervalMatcher,
} from '@branchlet/core/matchers';

const branchlet = createBranchlet({
  matchers: [eqMatcher, gteMatcher, ltMatcher, intervalMatcher],
});

// Equality
branchlet.parse('Status: {{ code, =200:OK, =404:Not Found, Error }}', { code: 200 });
// → "Status: OK"

// Comparisons
branchlet.parse('{{ age, >=18:Adult, <18:Minor }}', { age: 25 });
// → "Adult"

// Ranges
branchlet.parse('{{ temp, [-10-0]:Freezing, [1-15]:Cold, [16-25]:Mild, [26-35]:Warm, Hot }}',
  { temp: 22 }
);
// → "Mild"

Advanced Usage

Variable Interpolation in Results

You can reference the variable value within branch results using single curly braces:

branchlet.parse(
  'You have {{ count, =0:no items, =1:{count} item, {count} items }} in your cart.',
  { count: 5 }
);
// → "You have 5 items in your cart."

Creating Custom Matchers

Extend Branchlet with your own conditional logic:

import type { Matcher } from '@branchlet/core';

// Custom matcher to check if a string has a specific file extension
const hasExtensionMatcher: Matcher = ({ conditionText, value }) => {
  const match = conditionText.match(/^hasExtension\((.*)\)$/);
  if (match) {
    return String(value).endsWith(match[1]);
  }
  return null; // Not recognized, try next matcher
};

const branchlet = createBranchlet({
  matchers: [hasExtensionMatcher],
});

branchlet.parse(
  'Filename: {{ filename, hasExtension(.png):Image file, hasExtension(.doc):Document file, Unknown file }}',
  { filename: 'img_12345.png' }
);

// → "Filename: Image file"

Matcher Composition

Matchers are evaluated in order. Return null to pass control to the next matcher:

const customMatcher: Matcher = ({ conditionText, value }) => {
  if (conditionText.startsWith('custom')) {
    // Your logic here
    return /* true or false */;
  }
  return null; // Not recognized, continue to next matcher
};

Real-World Examples

E-commerce Cart

const template = `
  {{ itemCount, =0:Your cart is empty, =1:You have 1 item, You have {itemCount} items }}
  {{ itemCount, >0:({{ total }} total), }}
`.trim();

branchlet.parse(template, { itemCount: 3, total: '$45.99' });
// → "You have 3 items ($45.99 total)"

Notification System

const notification = branchlet.parse(
  '{{ unread, =0:No new messages, =1:1 new message, {unread} new messages }}',
  { unread: 12 }
);
// → "12 new messages"

User Engagement

const engagement = branchlet.parse(
  'You have {{ points }} points - {{ points, [0-99]:Beginner, [100-499]:Intermediate, [500-999]:Advanced, Expert }} level!',
  { points: 750 }
);
// → "You have 750 points - Advanced level!"

Philosophy

Branchlet is built on functional programming principles:

  • Pure Functions - No side effects, predictable behavior
  • Dependency Injection - Matchers are injected, not hardcoded
  • Composition - Small, focused functions that compose well
  • Immutability - No mutations, just transformations

This makes Branchlet easy to test, extend, and reason about.

API Reference

createBranchlet(options)

Creates a Branchlet instance with custom matchers.

Parameters:

  • options.matchers - Array of matcher functions (optional)

Returns:

  • Object with parse(template, variables) method

parse(template, variables?)

Parses a template string with optional variables.

Parameters:

  • template - Template string with {{ }} expressions
  • variables - Object mapping variable names to values (optional)

Returns:

  • Interpolated string

Matcher Type

type Matcher = (args: {
  conditionText: string;
  value: string | number | boolean;
}) => boolean | null;

Returns:

  • true - Condition matches
  • false - Condition doesn't match (but is recognized)
  • null - Condition not recognized (try next matcher)

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

Credits

This project is crafted with ❤️ by Corentin Thomasset. If you find this project helpful, please consider supporting my work.

Header logo uses Braces and Sprout icons from the Lucide icon collection.

License

This project is under the MIT license © Corentin Thomasset.

Contributors

CorentinTh

Issues