npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@naturetrail/welsh-translator

v0.5.2

Published

Framework-agnostic Welsh word translation with mutation normalisation, plus optional Svelte 5 components

Readme

@naturetrail/welsh-translator

A framework-agnostic, offline-first Welsh word translation library with built-in mutation normalization and optional Svelte 5 components.

Designed for the Nant Gwrtheyrn heritage trail app, this package enables tap-to-translate functionality for Welsh prose, ensuring that users can understand content even when they are offline in remote areas.


🌟 Key Features

  • Initial Mutation Normalisation: Recovers the radical (root) form of mutated Welsh words (Soft, Nasal, and Aspirate mutations).
  • Multi-Word Phrase Lookup: Supports curated multi-word vocabulary entries (e.g. "taith cerdded" → "a walk") with per-word mutation reversal.
  • Offline-First: Designed to work with local vocabulary data (e.g., synced via Zero-Sync or Payload CMS).
  • Framework Agnostic: Core TypeScript logic can be used in React, React Native, Vue, or vanilla JS.
  • Svelte 5 Components: Ready-to-use components for interactive text, built with Svelte 5 runes and bits-ui popovers.
  • Smart Tokenisation: Automatically splits prose into individually interactive word spans.

📦 Installation

# npm
npm install @naturetrail/welsh-translator

# pnpm
pnpm add @naturetrail/welsh-translator

# yarn
yarn add @naturetrail/welsh-translator

[!NOTE] If you are using the Svelte 5 components, you will also need to install bits-ui as a peer dependency.


🚀 Core Usage (Framework Agnostic)

The core logic is exported under @naturetrail/welsh-translator/core. This can be used in any environment (including React Native, Node.js, or standard web apps).

1. Basic Lookup

import { LookupEngine } from '@naturetrail/welsh-translator/core';

// 1. Prepare your vocabulary data (usually synced from CMS)
const vocabulary = [
  { id: '1', welsh: 'bwthyn', english: 'cottage', site: 'nant-gwrtheyrn' },
  { id: '2', welsh: 'carreg', english: 'stone', site: 'nant-gwrtheyrn' },
];

// 2. Initialize the engine
const engine = LookupEngine.fromEntries(vocabulary);

// 3. Perform lookups (handles mutations automatically!)
const result = engine.lookup('fwthyn'); // "fwthyn" is a soft mutation of "bwthyn"

if (result.entry) {
  console.log(`Matched: ${result.entry.welsh} (${result.entry.english})`);
  // Output: Matched: bwthyn (cottage)
  
  if (result.radical) {
    console.log(`Recovered radical: ${result.radical}`);
    // Output: Recovered radical: bwthyn
  }
}

2. Tokenising Prose

The tokeniser splits text into interactive word spans while preserving punctuation and whitespace.

import { tokenise } from '@naturetrail/welsh-translator/core';

const text = 'Ymwelwch â’r fwthyn hanesyddol.';
const tokens = tokenise(text);

tokens.forEach(token => {
  if (token.type === 'word') {
    // Check if the word is in the dictionary
    const hasTranslation = engine.hasTranslation(token.word);
    console.log(`Word: ${token.word}, Translatable: ${hasTranslation}`);
  }
});

3. Multi-Word Phrase Lookup

Vocabulary entries can contain multi-word phrases by using space-separated radical forms in the welsh field. The engine handles mutation reversal independently on each word.

import { LookupEngine, tokenise } from '@naturetrail/welsh-translator/core';

const vocabulary = [
  { id: '1', welsh: 'taith cerdded', english: 'a walk', site: 'nant-gwrtheyrn' },
  { id: '2', welsh: 'rydych chi', english: 'you are', site: 'nant-gwrtheyrn' },
  { id: '3', welsh: 'bwthyn', english: 'cottage', site: 'nant-gwrtheyrn' },
];

const engine = LookupEngine.fromEntries(vocabulary);
const tokens = tokenise('Rydych chi daith gerdded');

// Check for a phrase starting at a token index
const phrase = engine.hasPhrase(tokens, 0);
console.log(phrase.match);     // true — "rydych chi" matched
console.log(phrase.tokenSpan); // 3 — covers tokens [rydych, whitespace, chi]

// Full phrase lookup with debug trace
const result = engine.lookupPhrase(tokens, 0);
console.log(result.entry?.english); // "you are"
console.log(result.wordCount);      // 2

[!NOTE] The Svelte components (TranslatableText and TranslatableHTML) detect and render phrases automatically — no extra configuration needed. Phrase words are grouped into a single interactive button.


🧩 Svelte 5 Integration

The package includes Svelte 5 components for rapid UI development. These components use bits-ui for accessible popovers.

Usage in Svelte

<script lang="ts">
  import { LookupEngine } from '@naturetrail/welsh-translator/core';
  import { TranslatableText } from '@naturetrail/welsh-translator/svelte';

  let { welshText, vocabulary } = $props();

  // Initialize engine (can be done in a separate module or context)
  const engine = LookupEngine.fromEntries(vocabulary);
</script>

<div class="prose">
  <TranslatableText text={welshText} {engine} />
</div>

<style>
  /* See the Styling & Customisation section below for all available CSS variables */
</style>

Usage with HTML Content

If your content contains HTML formatting (e.g. from a CMS rich text field), use TranslatableHTML instead. It preserves the HTML structure while making Welsh words interactive.

<script lang="ts">
  import { LookupEngine } from '@naturetrail/welsh-translator/core';
  import { TranslatableHTML } from '@naturetrail/welsh-translator/svelte';

  let { vocabulary } = $props();

  const engine = LookupEngine.fromEntries(vocabulary);
  const html = '<p>Mae <strong>carreg</strong> yn y <em>bwthyn</em>.</p>';
</script>

<TranslatableHTML {html} {engine} />

[!NOTE] TranslatableHTML uses Svelte's {@html} internally. You are responsible for sanitising the HTML input if it comes from an untrusted source.

Toggling Translation On/Off

Both components accept an enabled prop. When false, the component renders the raw text or HTML with zero overhead — no tokenisation, no DOM walking, no popover setup.

The enabled prop defaults to engine.enabled, so you can control all components at once via the engine, or override per-component.

Via the engine (framework-agnostic):

// Disable globally — any component reading engine.enabled will skip processing
engine.enabled = false;

// Re-enable
engine.enabled = true;

Via the component prop (Svelte reactive toggle):

<script lang="ts">
  import { LookupEngine } from '@naturetrail/welsh-translator/core';
  import { TranslatableText, TranslatableHTML } from '@naturetrail/welsh-translator/svelte';

  const engine = LookupEngine.fromEntries(vocabulary);
  let translationEnabled = $state(true);
</script>

<button onclick={() => (translationEnabled = !translationEnabled)}>
  Toggle Translation
</button>

<TranslatableText text={welshText} {engine} enabled={translationEnabled} />
<TranslatableHTML html={welshHtml} {engine} enabled={translationEnabled} />

When enabled is false, both components render as plain <span> elements — identical output to rendering the text or HTML directly.


🎨 Styling & Customisation

All visual properties are exposed as CSS custom properties (variables) with sensible defaults. The default gold-on-dark theme works out of the box — override any --wt-* variable to customise the appearance.

Quick Example

Set variables on a parent element or :root to override the defaults:

<div style="--wt-popover-bg: #1a1a2e; --wt-welsh-color: #ffd700;">
  <TranslatableText text={welshText} {engine} />
</div>

CSS Variable Reference

Word / Button Styling

These variables control the interactive word buttons rendered inline in the text.

| Variable | Default | Description | |----------|---------|-------------| | --wt-word-border-color | rgba(201, 168, 76, 0.45) | Underline colour for translatable words | | --wt-word-hover-bg | rgba(201, 168, 76, 0.12) | Background on hover | | --wt-word-hover-border-color | rgba(201, 168, 76, 1) | Underline colour on hover | | --wt-word-focus-outline | 2px solid rgba(201, 168, 76, 0.4) | Outline on hover / keyboard focus | | --wt-word-active-bg | rgba(201, 168, 76, 0.18) | Background when popover is open | | --wt-word-active-border-color | rgba(201, 168, 76, 1) | Underline colour when popover is open | | --wt-word-border-radius | 2px | Border radius of word buttons | | --wt-word-padding | 0 1px | Padding around word buttons | | --wt-word-transition | border-color 0.15s, background-color 0.15s | Transition for hover/active states |

Popover Panel

These variables control the translation popover container.

| Variable | Default | Description | |----------|---------|-------------| | --wt-popover-bg | #2c2f3a | Popover background colour | | --wt-popover-border | 1px solid rgba(201, 168, 76, 0.3) | Popover border | | --wt-popover-border-radius | 8px | Popover corner radius | | --wt-popover-padding | 0.75rem 1rem | Popover inner padding | | --wt-popover-min-width | 160px | Minimum popover width | | --wt-popover-max-width | 280px | Maximum popover width | | --wt-popover-shadow | 0 8px 24px rgba(0, 0, 0, 0.4) | Popover box shadow | | --wt-popover-z-index | 1000 | Popover stacking order | | --wt-popover-arrow-fill | #2c2f3a | Arrow fill colour | | --wt-popover-arrow-stroke | rgba(201, 168, 76, 0.3) | Arrow border colour |

Popover Content

These variables control the text inside the popover.

| Variable | Default | Description | |----------|---------|-------------| | --wt-welsh-color | #e8d4a0 | Welsh word heading colour | | --wt-welsh-font-size | 1.1rem | Welsh word heading font size | | --wt-welsh-font-weight | 700 | Welsh word heading font weight | | --wt-english-color | #e8e0d0 | English translation colour | | --wt-english-font-size | 1rem | English translation font size | | --wt-radical-color | #8a9bb0 | Radical form text colour | | --wt-radical-font-size | 0.75rem | Radical form font size | | --wt-note-color | #8a9bb0 | Note text colour | | --wt-note-font-size | 0.8rem | Note font size | | --wt-note-border-color | rgba(138, 155, 176, 0.2) | Note separator line colour |

Container

| Variable | Default | Description | |----------|---------|-------------| | --wt-line-height | 1.8 | Line height for TranslatableText / TranslatableHTML containers |

Class-Based Overrides

All components accept a class prop for additional customisation:

<TranslatableText text={welshText} {engine} class="my-custom-class" />
<TranslatableHTML html={htmlContent} {engine} class="my-custom-class" />

For deeper overrides, you can target the component CSS classes directly using :global():

<style>
  :global(.translation-popover) {
    font-family: 'Georgia', serif;
  }
</style>

Theme Example: Light Mode

Here is a complete light theme override you can apply to :root or a parent element:

:root {
  --wt-popover-bg: #ffffff;
  --wt-popover-border: 1px solid #e0e0e0;
  --wt-popover-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
  --wt-popover-arrow-fill: #ffffff;
  --wt-popover-arrow-stroke: #e0e0e0;
  --wt-welsh-color: #2c5f2d;
  --wt-english-color: #333333;
  --wt-radical-color: #666666;
  --wt-note-color: #666666;
  --wt-note-border-color: rgba(0, 0, 0, 0.1);
  --wt-word-border-color: rgba(44, 95, 45, 0.4);
  --wt-word-hover-bg: rgba(44, 95, 45, 0.08);
  --wt-word-hover-border-color: rgba(44, 95, 45, 1);
  --wt-word-active-bg: rgba(44, 95, 45, 0.15);
  --wt-word-active-border-color: rgba(44, 95, 45, 1);
  --wt-word-focus-outline: 2px solid rgba(44, 95, 45, 0.3);
}

Importing Styles Separately

The default styles are bundled with the Svelte components automatically. If you need to import the stylesheet separately (e.g. for SSR or a custom setup):

import '@naturetrail/welsh-translator/styles';

📱 React Native / Framework Agnostic Example

For React Native, you can use the core logic to build your own interactive word component.

import React from 'react';
import { View, Text, TouchableOpacity } from 'react-native';
import { LookupEngine, tokenise } from '@naturetrail/welsh-translator/core';

const WelshText = ({ text, engine }) => {
  const tokens = React.useMemo(() => tokenise(text), [text]);

  // Build items with phrase-awareness
  const items = React.useMemo(() => {
    const result = [];
    let i = 0;
    while (i < tokens.length) {
      const token = tokens[i];
      if (token.type === 'word') {
        // Check for phrase match first
        const phrase = engine.hasPhrase(tokens, i);
        if (phrase.match) {
          const phraseTokens = tokens.slice(i, i + phrase.tokenSpan);
          const phraseText = phraseTokens.map(t => t.type === 'word' ? t.word : t.raw).join('');
          result.push({ key: i, type: 'phrase', text: phraseText, startIndex: i });
          i += phrase.tokenSpan;
          continue;
        }
        // Single word
        const entry = engine.lookup(token.word).entry;
        result.push({ key: i, type: 'word', text: token.word, entry });
      } else {
        result.push({ key: i, type: 'raw', text: token.raw });
      }
      i++;
    }
    return result;
  }, [tokens, engine]);

  return (
    <View style={{ flexDirection: 'row', flexWrap: 'wrap' }}>
      {items.map(item => {
        if (item.type === 'phrase') {
          return (
            <TouchableOpacity key={item.key} onPress={() => {
              const result = engine.lookupPhrase(tokens, item.startIndex);
              if (result.entry) alert(result.entry.english);
            }}>
              <Text style={{ textDecorationLine: 'underline' }}>{item.text}</Text>
            </TouchableOpacity>
          );
        }
        if (item.type === 'word') {
          return (
            <TouchableOpacity key={item.key} onPress={() => item.entry && alert(item.entry.english)}>
              <Text style={{ textDecorationLine: item.entry ? 'underline' : 'none' }}>
                {item.text}
              </Text>
            </TouchableOpacity>
          );
        }
        return <Text key={item.key}>{item.text}</Text>;
      })}
    </View>
  );
};

🏴󠁧󠁢󠁷󠁬󠁳󠁿 Why Mutation Normalisation?

Welsh uses initial consonant mutations, where the first letter of a word changes based on grammatical context.

For example, the word carreg (stone) can appear as:

  • garreg (Soft mutation)
  • ngharreg (Nasal mutation)
  • charreg (Aspirate mutation)

Without a normaliser, a simple dictionary lookup for garreg would fail. This library generates prioritized candidate radicals for any given word, allowing it to correctly identify carreg as the root form.


📖 Examples

You can find complete, runnable examples in the examples/ directory:

  • Svelte 5 Example: Uses the <TranslatableText /> component with Svelte 5 runes and bits-ui.
  • Svelte 5 HTML Example: Uses the <TranslatableHTML /> component with rich HTML content (bold, italic, paragraphs).
  • Svelte 5 Toggle Example: Demonstrates toggling translation on/off at runtime with both TranslatableText and TranslatableHTML.
  • Custom Theme Example: Demonstrates overriding --wt-* CSS variables to create light, dark, and forest colour themes.
  • Vanilla TS Example: Demonstrates manual usage of the core library and LookupEngine.

Both examples use real vocabulary and prose data extracted from the initial Nant Gwrtheyrn project.

Running the Demos

You can launch an interactive demo gallery to test these examples locally:

# 1. Start the Vite dev server
npm run demo

# 2. Open the URL provided in your terminal (usually http://localhost:5173)

The gallery will allow you to switch between the Svelte 5 and Vanilla TypeScript implementations.


🛠️ Payload CMS Integration

This package is designed to consume data from a Payload CMS collection structured like this:

| Field | Type | Notes | |-------|------|-------| | welsh | Text | The radical (root) form only | | english | Text | English translation | | site | Select | Scopes the word to a specific heritage site | | note | Textarea | Optional usage notes or gender info |

The vocabulary is synced to the device as a simple JSON array and passed to the LookupEngine.


📚 Background & Research

For a deep dive into the linguistic research, initial design decisions, and Welsh mutation rules that informed this package, see the Background & Research guide.


🚢 Deployment & Versioning

This package is published to NPM automatically via GitHub Actions when a version tag is pushed.

Releasing a new version

  1. Bump the version using npm's built-in command. This updates package.json and creates a git tag in one step:

    npm version patch   # 0.1.0 → 0.1.1  (bug fixes)
    npm version minor   # 0.1.0 → 0.2.0  (new features, backwards compatible)
    npm version major   # 0.1.0 → 1.0.0  (breaking changes)
  2. Push the commit and tag:

    git push && git push --tags
  3. GitHub Actions takes over — the publish workflow runs type checking, tests, and build before publishing to NPM. You can monitor the run in the Actions tab of the repository.

Versioning guidelines

This package follows Semantic Versioning:

| Change type | Version bump | Examples | |---|---|---| | Bug fix, internal refactor | patch | Fix incorrect mutation candidate, fix tokeniser edge case | | New feature, new export | minor | New component, new engine method, new mutation type | | Breaking API change | major | Rename/remove exports, change LookupResult shape |

[!NOTE] The package is currently at v0.x.x, meaning the API may still evolve. Pin to an exact version in production apps until v1.0.0 is released.

CI

Every push and pull request to main runs the CI workflow: type checking, tests, and a full build. A passing CI badge indicates the package builds and all 80 tests pass.


🤝 Contributing & License

This project is part of the Nant Gwrtheyrn initiative.

GPLv3 License 2026 Tinkr.