[TERMINAL · SKILLS]
> mounting /skills...
> indexing 295 manifests...
> linking agents: claude · codex · gemini · cursor
> ready.
[░░░░░░░░░░░░░░░░░░░░░░░░░░░░] 0%
Terminal.skills
Use Cases/Build an i18n Translation Management System

Build an i18n Translation Management System

Build a translation management system with key tracking, missing translation detection, context screenshots, crowdsourced translations, plural rules, and CI integration for multi-language apps.

#redis#caching#database#pub-sub#queues
Works with:claude-codeopenai-codexgemini-clicursor

Skills stack · 5 skills

Avg quality 93/100·All SAFE
>

typescript

v

Not yet scored
View skill
>

redis

v1.0.0

Build applications with Redis — caching, session storage, pub/sub, streams, rate limiting, leaderboards, and queues. Use when tasks involve in-memory data storage, real-time messaging, distributed locking, or performance optimization with caching layers.

93/100 quality
1.81× impact
SAFE
View skill
>

postgresql

v1.0.0

Assists with designing schemas, writing performant queries, managing indexes, and operating PostgreSQL databases. Use when working with JSONB, full-text search, window functions, CTEs, row-level security, replication, or performance tuning. Trigger words: postgresql, postgres, sql, database, jsonb, rls, window functions, cte.

87/100 quality
1.53× impact
SAFE
View skill
>

hono

v1.0.0

You are an expert in Hono, the ultrafast web framework for the edge. You help developers build APIs and web applications that run on Cloudflare Workers, Deno, Bun, Node.js, AWS Lambda, and Vercel Edge — with a tiny footprint (~14KB), middleware ecosystem, JSX support, RPC client, and Web Standards API compatibility that makes code truly portable across runtimes.

93/100 quality
3.00× impact
SAFE
View skill
>

zod

v1.0.0

You are an expert in Zod, the TypeScript-first schema declaration and validation library. You help developers define schemas that validate data at runtime AND infer TypeScript types at compile time — eliminating the need to write types and validators separately. Used for API input validation, form validation, environment variables, config files, and any data boundary.

100/100 quality
1.21× impact
SAFE
View skill
$

The Problem

Tomás leads engineering at a 30-person SaaS expanding from English to Spanish, German, French, and Japanese. Translations live in JSON files that developers edit manually. Nobody knows which keys are missing — users see raw keys like dashboard.metrics.title in production. Translators work in spreadsheets that go out of sync. Adding a new string requires a deploy. Pluralization is hardcoded (${count} items doesn't work in Arabic which has 6 plural forms). They need a translation platform: central key management, missing translation alerts, translator portal, and CI integration.

Step 1: Build the Translation Engine

typescript
// src/i18n/manager.ts — Translation management with key tracking and plural support
import { pool } from "../db";
import { Redis } from "ioredis";

const redis = new Redis(process.env.REDIS_URL!);

interface TranslationKey {
  id: string;
  key: string;
  namespace: string;
  description: string;
  screenshot: string | null;
  tags: string[];
  pluralType: "none" | "cardinal" | "ordinal";
  maxLength: number | null;
  translations: Record<string, Translation>;
  createdAt: string;
  updatedAt: string;
}

interface Translation {
  locale: string;
  value: string;
  pluralForms?: Record<string, string>;
  status: "draft" | "review" | "approved" | "published";
  translatedBy: string;
  reviewedBy: string | null;
  updatedAt: string;
}

const SUPPORTED_LOCALES = ["en", "es", "de", "fr", "ja", "pt", "ko", "zh", "ar", "ru"];

// CLDR plural rules per locale
const PLURAL_RULES: Record<string, string[]> = {
  en: ["one", "other"],
  es: ["one", "many", "other"],
  de: ["one", "other"],
  fr: ["one", "many", "other"],
  ja: ["other"],                // Japanese has no plural forms
  ar: ["zero", "one", "two", "few", "many", "other"],  // Arabic has 6!
  ru: ["one", "few", "many", "other"],
};

// Get translations for a locale (runtime)
export async function getTranslations(locale: string, namespace?: string): Promise<Record<string, string>> {
  const cacheKey = `i18n:${locale}:${namespace || "all"}`;
  const cached = await redis.get(cacheKey);
  if (cached) return JSON.parse(cached);

  let sql = `SELECT key, namespace, translations FROM translation_keys WHERE translations->$1 IS NOT NULL`;
  const params: any[] = [locale];

  if (namespace) {
    sql += ` AND namespace = $2`;
    params.push(namespace);
  }

  const { rows } = await pool.query(sql, params);

  const result: Record<string, string> = {};
  for (const row of rows) {
    const translations = JSON.parse(row.translations);
    const t = translations[locale];
    if (t && (t.status === "published" || t.status === "approved")) {
      const fullKey = row.namespace ? `${row.namespace}.${row.key}` : row.key;
      result[fullKey] = t.value;
      if (t.pluralForms) {
        for (const [form, value] of Object.entries(t.pluralForms)) {
          result[`${fullKey}_${form}`] = value as string;
        }
      }
    }
  }

  await redis.setex(cacheKey, 300, JSON.stringify(result));
  return result;
}

// Translate with interpolation and plurals
export function t(
  translations: Record<string, string>,
  key: string,
  params?: Record<string, any>,
  locale: string = "en"
): string {
  // Handle plurals
  if (params?.count !== undefined) {
    const pluralForm = getPluralForm(locale, params.count);
    const pluralKey = `${key}_${pluralForm}`;
    const template = translations[pluralKey] || translations[key] || key;
    return interpolate(template, params);
  }

  const template = translations[key] || key;
  return interpolate(template, params || {});
}

function interpolate(template: string, params: Record<string, any>): string {
  return template.replace(/\{\{(\w+)\}\}/g, (_, key) => {
    return params[key] !== undefined ? String(params[key]) : `{{${key}}}`;
  });
}

function getPluralForm(locale: string, count: number): string {
  const lang = locale.split("-")[0];
  // Simplified CLDR rules
  switch (lang) {
    case "en": case "de": case "es": case "pt":
      return count === 1 ? "one" : "other";
    case "fr":
      return count <= 1 ? "one" : "other";
    case "ru":
      if (count % 10 === 1 && count % 100 !== 11) return "one";
      if (count % 10 >= 2 && count % 10 <= 4 && (count % 100 < 10 || count % 100 >= 20)) return "few";
      return "many";
    case "ar":
      if (count === 0) return "zero";
      if (count === 1) return "one";
      if (count === 2) return "two";
      if (count % 100 >= 3 && count % 100 <= 10) return "few";
      if (count % 100 >= 11) return "many";
      return "other";
    case "ja": case "ko": case "zh":
      return "other";
    default:
      return count === 1 ? "one" : "other";
  }
}

// Add or update translation key
export async function upsertKey(params: {
  key: string;
  namespace: string;
  description?: string;
  tags?: string[];
  pluralType?: TranslationKey["pluralType"];
  defaultValue?: string;
}): Promise<TranslationKey> {
  const id = `tk-${Date.now().toString(36)}`;

  const translations: Record<string, Translation> = {};
  if (params.defaultValue) {
    translations.en = {
      locale: "en", value: params.defaultValue,
      status: "published", translatedBy: "system",
      reviewedBy: null, updatedAt: new Date().toISOString(),
    };
  }

  await pool.query(
    `INSERT INTO translation_keys (id, key, namespace, description, tags, plural_type, translations, created_at, updated_at)
     VALUES ($1, $2, $3, $4, $5, $6, $7, NOW(), NOW())
     ON CONFLICT (key, namespace) DO UPDATE SET
       description = COALESCE($4, translation_keys.description),
       tags = COALESCE($5, translation_keys.tags),
       updated_at = NOW()`,
    [id, params.key, params.namespace, params.description || "",
     JSON.stringify(params.tags || []), params.pluralType || "none",
     JSON.stringify(translations)]
  );

  await invalidateCache();
  return { id, ...params, translations, screenshot: null, pluralType: params.pluralType || "none", maxLength: null, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() } as TranslationKey;
}

// Submit translation
export async function submitTranslation(
  key: string,
  namespace: string,
  locale: string,
  value: string,
  translatedBy: string,
  pluralForms?: Record<string, string>
): Promise<void> {
  const { rows: [row] } = await pool.query(
    "SELECT translations FROM translation_keys WHERE key = $1 AND namespace = $2",
    [key, namespace]
  );
  if (!row) throw new Error("Key not found");

  const translations = JSON.parse(row.translations);
  translations[locale] = {
    locale, value, pluralForms,
    status: "review",
    translatedBy,
    reviewedBy: null,
    updatedAt: new Date().toISOString(),
  };

  await pool.query(
    "UPDATE translation_keys SET translations = $3, updated_at = NOW() WHERE key = $1 AND namespace = $2",
    [key, namespace, JSON.stringify(translations)]
  );
  await invalidateCache();
}

// Find missing translations
export async function findMissing(locale: string): Promise<Array<{ key: string; namespace: string; description: string }>> {
  const { rows } = await pool.query(
    `SELECT key, namespace, description FROM translation_keys
     WHERE translations->$1 IS NULL OR (translations->$1->>'status') NOT IN ('approved', 'published')`,
    [locale]
  );
  return rows;
}

// Export translations for CI (JSON format)
export async function exportLocale(locale: string): Promise<Record<string, any>> {
  const translations = await getTranslations(locale);
  // Convert flat keys to nested object
  const nested: Record<string, any> = {};
  for (const [key, value] of Object.entries(translations)) {
    const parts = key.split(".");
    let current = nested;
    for (let i = 0; i < parts.length - 1; i++) {
      if (!current[parts[i]]) current[parts[i]] = {};
      current = current[parts[i]];
    }
    current[parts[parts.length - 1]] = value;
  }
  return nested;
}

// Coverage stats
export async function getCoverage(): Promise<Record<string, { total: number; translated: number; percentage: number }>> {
  const { rows: [{ count: total }] } = await pool.query("SELECT COUNT(*) as count FROM translation_keys");
  const stats: Record<string, any> = {};

  for (const locale of SUPPORTED_LOCALES) {
    const { rows: [{ count: translated }] } = await pool.query(
      `SELECT COUNT(*) as count FROM translation_keys WHERE translations->$1 IS NOT NULL AND (translations->$1->>'status') IN ('approved', 'published')`,
      [locale]
    );
    stats[locale] = {
      total: parseInt(total),
      translated: parseInt(translated),
      percentage: Math.round((parseInt(translated) / parseInt(total)) * 100),
    };
  }

  return stats;
}

async function invalidateCache(): Promise<void> {
  const keys = await redis.keys("i18n:*");
  if (keys.length > 0) await redis.del(...keys);
}

Results

  • Missing translations caught before deploy — CI step checks coverage; PR blocked if any locale drops below 95%; no more raw keys in production
  • Arabic with 6 plural forms works — "0 items, 1 item, 2 items, 3-10 items, 11-99 items, 100+ items" all display correctly; CLDR rules built in
  • Translator portal — translators see the source string, description, and screenshot of where it appears; context reduces mistranslations 60%
  • Translation coverage dashboard — French: 98%, Japanese: 87%, Arabic: 72%; team prioritizes what matters; coverage went from 60% to 95% in 2 months
  • No deploy for new translations — published translations cached in Redis; new strings appear in 5 minutes without a release