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

@watchgold/i18n-kit

v0.1.0

Published

Locale metadata helpers, battle-tested hreflang/canonical builders for multilingual sites, and a translation-parity checker.

Readme

@watchgold/i18n-kit

Locale metadata helpers, battle-tested hreflang/canonical builders for multilingual sites, and a translation-parity checker.

Zero dependencies, framework-free. The builder return shapes drop straight into the Next.js Metadata alternates field, but nothing here imports from Next — use the package with any framework that lets you emit canonical and alternate-language links, and run the parity checker from any test runner or CI script.

The lessons baked into this package come from operating an 11-locale production site through Google Search Console incidents, not from reading the hreflang spec.

npm install @watchgold/i18n-kit

Defining your locale set

One typed definition carries every shape of locale metadata a multilingual site needs — routing codes, BCP 47 forms, OpenGraph forms, picker labels, RTL flags — so the language picker, the <html> shell, and the alternate links can never drift apart.

// i18n/locale-set.ts
import { defineLocaleSet } from '@watchgold/i18n-kit';

export const localeSet = defineLocaleSet({
  locales: ['en', 'zh', 'ar'] as const,
  defaultLocale: 'en',
  rtlLocales: ['ar'],
  labels: { en: 'English', zh: '中文', ar: 'العربية' },
  htmlLang: { zh: 'zh-CN' },              // <html lang> / hreflang keys; others default to the code
  ogLocale: { en: 'en_US', zh: 'zh_CN' }, // og:locale form; others default to the code
});

defineLocaleSet throws at module load if defaultLocale is not in locales, so a typo fails your build instead of quietly emitting broken canonical URLs.

Typical layout usage — both attributes server-rendered so crawlers see them and there is no LTR→RTL flip after hydration:

// app/[lang]/layout.tsx
export default async function LocaleLayout({ children, params }) {
  const { lang } = await params;
  if (!localeSet.isLocale(lang)) notFound();
  return (
    <html lang={localeSet.htmlLang(lang)} dir={localeSet.dir(lang)}>
      <body>{children}</body>
    </html>
  );
}

Three canonicalization strategies for multilingual sites

A site with locale-prefixed routing (/en/..., /zh/..., /ar/...) serves every page at N URLs. Whether those URLs are alternates of each other or duplicates of one page depends entirely on the content behind them — and declaring the wrong thing costs indexing. Search engines do not read your intent; they compare the rendered pages.

This package encodes the three cases that cover a real multilingual site. All builders take the locale set as their first argument and build URLs as ${baseUrl}/${locale}${pathSuffix} (no trailing slash on baseUrl; pathSuffix empty or starting with /).

| Content situation | Builder | Canonical | hreflang cluster | | --- | --- | --- | --- | | Fully translated per locale (UI pages, localized landing pages) | buildAlternateLanguages | self | all locales + x-default | | One default-language text rendered under every prefix (DB/CMS content) | buildDefaultLanguageContentAlternates | default-locale URL | none | | Translated per row, backfilling over time | buildTranslatedContentAlternates | self if translated, else default-locale URL | translated locales + default + x-default, or none |

1. Fully translated pages — buildAlternateLanguages

For pages whose content genuinely differs per locale: every locale version is a real alternate, so each one self-canonicalizes and declares the full hreflang cluster. The map is keyed by each locale's BCP 47 htmlLang form (Google matches hreflang values against BCP 47, not your routing codes), and x-default points at the default locale.

// app/[lang]/pricing/page.tsx
import type { Metadata } from 'next';
import { buildAlternateLanguages } from '@watchgold/i18n-kit';
import { localeSet } from '@/i18n/locale-set';

const siteUrl = 'https://example.com';

export async function generateMetadata({ params }): Promise<Metadata> {
  const { lang } = await params;
  return {
    alternates: {
      canonical: `${siteUrl}/${lang}/pricing`,
      languages: buildAlternateLanguages(localeSet, siteUrl, '/pricing'),
    },
  };
}

Emits (for the en/zh/ar set above):

{
  "en": "https://example.com/en/pricing",
  "zh-CN": "https://example.com/zh/pricing",
  "ar": "https://example.com/ar/pricing",
  "x-default": "https://example.com/en/pricing"
}

2. Single-language content under every prefix — buildDefaultLanguageContentAlternates

The trap. Sites with DB or CMS content — articles, editorial notes, generated summaries — usually render that text in its original language under every locale prefix, translating only the surrounding chrome. /zh/news/some-article and /ar/news/some-article are then duplicates of the default-language page, not translations of it.

Declaring the full hreflang cluster with self-canonicals here tells the crawler "these N pages are distinct language versions", and then the crawler reads N identical bodies. This exact configuration got every locale variant of such pages crawled and dropped as duplicates — "Crawled – currently not indexed" in Search Console — wasting crawl budget and diluting the one URL that could rank.

The fix: every locale URL canonicalizes to the default-locale URL, and no hreflang cluster is declared at all. One indexable page, N reachable URLs.

// app/[lang]/news/[slug]/page.tsx — content stored in one language only
import type { Metadata } from 'next';
import { buildDefaultLanguageContentAlternates } from '@watchgold/i18n-kit';
import { localeSet } from '@/i18n/locale-set';

const siteUrl = 'https://example.com';

export async function generateMetadata({ params }): Promise<Metadata> {
  const { slug } = await params;
  return {
    alternates: buildDefaultLanguageContentAlternates(localeSet, siteUrl, `/news/${slug}`),
  };
}

Every locale renders { canonical: "https://example.com/en/news/<slug>" } — including the default locale itself.

3. Per-row translations that backfill over time — buildTranslatedContentAlternates

The middle ground. Row-level content often gains translations incrementally: each row carries the list of locales that have a real stored translation (an available_langs column, a translations join table, ...). A single site-wide rule is wrong for both ends, so the alternates must be decided per row, per locale:

  • A locale with a stored translation is a true alternate: it self-canonicalizes and declares a cluster covering only the translated locales plus the default (the original) and x-default.
  • A locale still serving the default-language fallback is a duplicate: it canonicalizes to the default-locale URL and declares no cluster — the same reasoning as strategy 2.
  • Codes outside the locale set are ignored; an unsupported current locale is treated as the duplicate case.

As translations backfill, rows migrate from the duplicate case to the cluster case automatically — no metadata code changes.

// app/[lang]/news/[slug]/page.tsx — rows carry availableLangs
import type { Metadata } from 'next';
import { buildTranslatedContentAlternates } from '@watchgold/i18n-kit';
import { localeSet } from '@/i18n/locale-set';

const siteUrl = 'https://example.com';

export async function generateMetadata({ params }): Promise<Metadata> {
  const { lang, slug } = await params;
  const article = await fetchArticle(slug); // { availableLangs: ['zh'], ... }
  return {
    alternates: buildTranslatedContentAlternates(
      localeSet,
      siteUrl,
      `/news/${slug}`,
      article.availableLangs,
      lang,
    ),
  };
}

With availableLangs: ['zh'], the zh page emits a self canonical plus a {en, zh-CN, x-default} cluster; the ar page (still showing the default-language fallback) emits only the default canonical; and once ar gains a stored translation the same code emits the three-locale cluster.

Translation parity checking

The other half of running many locales: keeping every dictionary structurally identical to the source-of-truth locale. parity.ts is framework-free — flatten, diff, and validate nested dictionaries from any test runner or a plain CI script.

  • getLeafPaths(obj) — sorted leaf key paths, array-index aware: { a: [{ b: 'x' }] }['a.0.b'].
  • resolveLeafPath(obj, path) — value at a dotted path (undefined when unresolvable).
  • compareLocaleKeys(base, other){ missing, extra } leaf-path diff.
  • findInvalidLeafValues(obj, { allowEmpty? }) — empty-string and non-string leaves with their paths and types.
  • checkLocaleParity({ base, locales, baseName? }) — the one-call version: every dictionary diffed against the base, plus leaf-value validation (the base included, reported under baseName); ok is true only when everything matches.

A locale-completeness test in vitest:

import { describe, it, expect } from 'vitest';
import { checkLocaleParity, compareLocaleKeys } from '@watchgold/i18n-kit';

import en from '../locales/en';
import zh from '../locales/zh';
import ar from '../locales/ar';

const bundles = { zh, ar };

describe('locale completeness', () => {
  // One assertion covering structure and values; failures print the full report.
  it('every locale mirrors the en key structure with valid values', () => {
    const result = checkLocaleParity({ base: en, locales: bundles, baseName: 'en' });
    expect(result.problems).toEqual([]);
    expect(result.ok).toBe(true);
  });

  // Or per-locale cases for granular failure output:
  for (const [name, bundle] of Object.entries(bundles)) {
    it(`${name} has exactly the keys of en`, () => {
      const { missing, extra } = compareLocaleKeys(en, bundle);
      expect(missing, `${name} is missing keys: ${missing.join(', ')}`).toEqual([]);
      expect(extra, `${name} has extra keys: ${extra.join(', ')}`).toEqual([]);
    });
  }
});

The same check as a CI script:

import { checkLocaleParity } from '@watchgold/i18n-kit';

const result = checkLocaleParity({ base: en, locales: { zh, ar }, baseName: 'en' });
if (!result.ok) {
  console.error(JSON.stringify(result.problems, null, 2));
  process.exit(1);
}

Practical policy that pairs well with the checker: when a key is added to the base locale, add it to every other locale in the same commit — starting a translation as the base-language string is fine (the checker flags missing keys, not identical strings), but structural drift is never fine.

LocaleSet API

| Member | Type | Notes | | --- | --- | --- | | locales | readonly L[] | Configuration order — also the emission order of hreflang maps. | | defaultLocale | L | Validated member of locales. | | isLocale(value) | value is L | Type guard for URL segments, headers, cookies. | | isRtl(locale) | boolean | false for unknown locales. | | dir(locale) | 'rtl' \| 'ltr' | For <html dir>; 'ltr' for unknown locales. | | htmlLang(locale) | string | BCP 47 form for <html lang> and hreflang keys; falls back to the code. | | ogLocale(locale) | string | og:locale form; falls back to the code. | | label(locale) | string | Native-name picker label; falls back to the code. |

RTL tip: prefer logical CSS properties (margin-inline-start, padding-inline-end, text-align: start) so layouts mirror correctly under dir="rtl" without per-locale styles.


Extracted from the production codebase of WatchGold, a precious-metals market-data platform.

MIT