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

@secorto/i18n

v0.9.0

Published

Framework-agnostic i18n primitive

Readme

@secorto/i18n

Framework-agnostic primitives for highly asymmetric multilingual content systems.

Why this exists: Identity vs. Routing

Most i18n issues are not really translation issues; they are identity and consistency issues.

When a site grows, slugs, locale prefixes, content IDs, and localized strings start mixing together. This creates brittle navigation, ambiguous content relationships, and silent SEO bugs (like broken hreflang targets).

@secorto/i18n separates those concerns. The package models localized content around a stable, unique translationKey instead of a mutable slug or resource ID.

export interface LocalizedEntry<TSection, TEntry, TLocale> {
  section: TSection
  cleanId: string
  translationKey: string
  locale: TLocale
  draft: boolean
  original: TEntry
}

This keeps the identity of the content distinct from the identity of the URL or resource ID. The same content can exist across highly asymmetric locales while still being resolved as a single, unified translation unit.


High-Level Translation Flow

The concept is declarative: each content item is first identified by its meaning (translationKey), then the system indexes and groups them by locale, generates asymmetric paths, and finally compiles safe metadata links for SEO and navigation.

flowchart TD

    %% ────────────────────────────────
    %% 1. DOMAIN CORE (IMMUTABLE)
    %% ────────────────────────────────
    subgraph Domain[1. Domain Core]
        Identity[Stable Identity: translationKey]
        Contracts[Route Contracts: Locales / Sections / Tags / Pages]
        Indexer[Translation Index: Group Siblings + Freeze State]

        Identity --> Contracts
        Contracts --> Indexer
    end

    %% ────────────────────────────────
    %% 2A. STATIC PATHS GENERATION
    %% ────────────────────────────────
    subgraph Gen[2A. Static Paths]
        StaticHelpers[getStaticPaths*]
        AstroEngine[Astro Static Router]

        StaticHelpers --> AstroEngine
    end

    %% ────────────────────────────────
    %% 2B. PER-PAGE RESOLUTION
    %% ────────────────────────────────
    subgraph Resolve[2B. Link Resolution]
        LinkHelpers[create*TranslationLinks]
        PathCheck[Runtime Check: URL vs Contract]

        LinkHelpers --> PathCheck
    end

    %% Domain feeds both branches
    Indexer --> StaticHelpers
    Indexer --> LinkHelpers

    %% ────────────────────────────────
    %% 3. STATE RESOLUTION
    %% ────────────────────────────────
    PathCheck -->|Mismatch| Crash[Error]
    PathCheck -->|Missing| Missing[missingLink]
    PathCheck -->|Draft| Draft[draftLink]
    PathCheck -->|Valid| Available[availableLink]

    Missing --> LinkArray[TranslationLink Array]
    Draft --> LinkArray
    Available --> LinkArray

    %% ────────────────────────────────
    %% 4. FINAL RENDER
    %% ────────────────────────────────
    AstroEngine --> Render[Rendered Page: Canonical + hreflang]
    LinkArray --> Render

In practice, this architectural model ensures

  • Translation Families: Source content is stored per locale but evaluated as a single logical unit.
  • Deterministic Links: Every page can instantly ask "what are the exact sibling paths of this item?" to feed language selectors or hreflang headers without recalculating path rules.
  • Graceful Degradation (missing states): If a language variant doesn't exist yet, the system still computes its potential path but flags it as unavailable.
  • Native Draft Lifecycle (draft states): Translations marked as drafts remain part of the logical group to maintain navigation boundaries but are automatically hidden from canonical SEO indexing (noindex).

1. Domain Configuration (The Invariant Contracts)

The library exposes immutable Value Objects for locales, sections, standalone pages, and tag routes. These enforce strict constraints at construction time, ensuring zero duplicated (locale, slug) pairs and failing fast (throwing explicit errors) during development or build time when a route contract collides across contexts or is otherwise malformed.

Configure your single source of truth (e.g., src/domain/i18n.ts):

import { createLocales, createSectionRoutes, createStandalonePageRoutes, createTagRoutes } from '@secorto/i18n'

// 1. Define supported locales
export const languages = createLocales(['en', 'es'])

// 2. Map localized section slugs to content collections
export const sectionRoutes = createSectionRoutes(
  {
    blog: { en: 'blog', es: 'bitacora' },
  },
  languages,
)

// 3. Map fixed static page slugs
export const standalonePageRoutes = createStandalonePageRoutes(
  {
    about: {
      en: { slug: 'about' },
      es: { slug: 'acerca-de' },
    },
  },
  languages,
)

// 4. Map localized taxonomy slugs (Locale x Section x Tag)
export const tagRoutes = createTagRoutes(
  sectionRoutes,
  { en: 'tags', es: 'tags' },
  {
    dev: { en: 'dev', es: 'desarrollo' },
    opensource: { en: 'opensource', es: 'codigo-abierto' },
  },
  languages,
)

2. Main Content Flow (Collections & Entries)

In this framework, each section maps directly to an Astro Content Collection, while an entry represents the localized resource bundle bound to its core identity index.

Section Index View (/[locale]/[section]/index.astro)

The getStaticPathsSections helper handles root path initialization. Filter your content collections declaratively using availableAtLocale to isolate data segments cleanly.

---
import { getCollection } from 'astro:content'
import { getStaticPathsSections, availableAtLocale, availableLink } from '@secorto/i18n'
import { languages, sectionRoutes, tagRoutes } from '@domain/i18n'

export async function getStaticPaths() {
  return await getStaticPathsSections(sectionRoutes, languages)
}

const { locale } = Astro.params
const { section } = Astro.props

const posts = await getCollection(section, availableAtLocale(locale))
const links = languages.all.map(lang =>
  availableLink(sectionRoutes.getSectionPath(section, lang), lang)
)
const activeTags = tagRoutes.getTags().filter(tag =>
  posts.some(post => post.data.tags.includes(tag)),
)
---
<html>
  <head>
    {links.map(({ href, locale }) => <link rel="alternate" hreflang={locale} href={href} />)}
  </head>
  <body>
    <nav>
      {activeTags.map(tag => <a href={tagRoutes.getSectionTagPath(section, locale, tag)}>{tag}</a>)}
    </nav>
    <main>
      {posts.map(post => <h2>{post.data.title}</h2>)}
    </main>
  </body>
</html>

Entry Detail View (/[locale]/[section]/[slug].astro)

The getStaticPathsEntries utility indexes structural assets via createTranslationIndex(), groups siblings by core identity, and exposes cross-linked alternate arrays directly into page properties.

---
import { render, getCollection } from 'astro:content'
import { getStaticPathsEntries, createDetailTranslationLinks } from '@secorto/i18n'
import { languages, sectionRoutes } from '@domain/i18n'

export async function getStaticPaths() {
  return await getStaticPathsEntries(sectionRoutes, getCollection, languages)
}

const { locale } = Astro.params
const { entry, siblings, section } = Astro.props

const links = createDetailTranslationLinks(siblings, sectionRoutes, languages)
const { Content } = await render(entry.original)
---
<html>
  <head>
    {entry.draft && <meta name="robots" content="noindex" />}
    {links.map(({ href, locale }) => <link rel="alternate" hreflang={locale} href={href} />)}
  </head>
  <body>
    {entry.draft && <div role="status">Draft Notice: This translation variant is a preliminary work.</div>}
    <article>
      <Content />
    </article>
  </body>
</html>

3. Advanced Ecosystem: Asymmetric Tag Routing

createTagRoutes acts as a dedicated navigation and classification layer. The getStaticPathsSectionTags helper processes tag intersections natively, avoiding empty route compilation by inspecting active datasets on the fly.

---
import { getCollection } from 'astro:content'
import { getStaticPathsSectionTags, createSectionTagTranslationLinks, availableAtLocale, withTag } from '@secorto/i18n'
import { languages, sectionRoutes, tagRoutes } from '@domain/i18n'

export async function getStaticPaths() {
  return getStaticPathsSectionTags(languages, sectionRoutes, tagRoutes, (sec) => getCollection(sec))
}

const { locale } = Astro.params
const { section, tag, siblings } = Astro.props

const tagSlug = tagRoutes.getTagSlug(tag, locale) // Resolves "codigo-abierto" or "opensource" dynamically
const links = createSectionTagTranslationLinks(languages.all, siblings, section, tag, tagRoutes)
const posts = await getCollection(section, availableAtLocale(locale))
const postWithTag = posts.filter(withTag(tag))
---
<html>
  <head>
    {links.map(({ href, locale }) => <link rel="alternate" hreflang={locale} href={href} />)}
  </head>
  <body>
    <h1>Tag: {tagSlug}</h1>
    {postWithTag.map(post => <h3>{post.data.title}</h3>)}
  </body>
</html>

(Note: The package also provides getEntriesBySection and getSectionsWithTagContent primitives to effortlessly create global taxonomy indexes like /[locale]/tags/index.astro, generating real-time item metrics and filtering empty terms out of layout loops).


4. Advanced Ecosystem: Strict Standalone Pages

For standalone static layout paths (e.g., /about or /privacy), the library bypasses filesystem guesswork entirely. It checks the live runtime pathname against your contract layer. If a static asset introduces an unindexed key, the application context aborts instantly to prevent orphan routing anomalies.

---
import type { MarkdownLayoutProps } from 'astro'
import { createStandalonePageLinks } from '@secorto/i18n'
import { languages, standalonePageRoutes } from '@domain/i18n'

type Props = MarkdownLayoutProps<{ title: string; translationKey: string }>
const { frontmatter } = Astro.props
const { title, translationKey } = frontmatter

const path = Astro.url.pathname.slice(1).replace(/\/$/, '')

// Will crash early if the translationKey or route setup violates the domain contract
const links = createStandalonePageLinks(path, translationKey, standalonePageRoutes, languages)

const locale = languages.fromString(Astro.currentLocale)
const page = standalonePageRoutes.getPage(translationKey)
const draft = standalonePageRoutes.routes[page][locale]?.draft ?? false
---
<html>
  <head>
    <title>{title}</title>
    {draft && <meta name="robots" content="noindex" />}
    {links.map(({ href, locale }) => <link rel="alternate" hreflang={locale} href={href} />)}
  </head>
  <body>
    <main>
      <slot />
    </main>
  </body>
</html>

Installation

npm install @secorto/i18n

License

MIT