AgentStack
SKILL verified Apache-2.0 Self-run

I18n Localization

skill-medy-gribkov-arcana-i18n-localization · by medy-gribkov

Internationalization and localization for web applications. Handles next-intl, react-intl, ICU MessageFormat, RTL layouts, and dynamic locale loading.

No reviews yet
0 installs
6 views
0.0% view→install

Install

$ agentstack add skill-medy-gribkov-arcana-i18n-localization

✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.

Security review

✓ Passed

No issues found. Passed automated security review. · v0.1.0 How review works →

  • Prompt-injection patterns
  • Secret / credential exfiltration
  • Dangerous shell & filesystem operations
  • Untrusted network calls
  • Known-malicious package signatures

What it can access

  • Network access No
  • Filesystem access Used
  • Shell / process execution No
  • Environment & secrets No
  • Dynamic code execution No

From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.

Are you the author of I18n Localization? Claim this listing to set pricing, connect Stripe payouts, and keep 70% of every sale.
Sign up to claim

About

i18n-localization

Internationalization (i18n) and localization (l10n) for web applications. Implements proper message formatting, locale routing, RTL support, and dynamic translation loading.

Translation File Structure

BAD: Flat structure, mixed namespaces

// messages/en.json
{
  "welcomeMessage": "Welcome",
  "homePageTitle": "Home",
  "userProfileName": "Name",
  "userProfileEmail": "Email",
  "checkoutButtonSubmit": "Submit Order"
}

GOOD: Namespaced structure

// messages/en/common.json
{
  "welcome": "Welcome",
  "loading": "Loading...",
  "error": "An error occurred"
}

// messages/en/home.json
{
  "title": "Home",
  "hero": {
    "heading": "Welcome to our platform",
    "subheading": "Build amazing things"
  }
}

// messages/en/user.json
{
  "profile": {
    "name": "Name",
    "email": "Email",
    "updatedAt": "Last updated {date}"
  }
}

ICU MessageFormat - Plurals and Select

BAD: String concatenation

// DON'T: Breaks in many languages
const message = count === 1
  ? `You have ${count} message`
  : `You have ${count} messages`;

// DON'T: No gender agreement support
const greeting = gender === 'male' ? `Welcome, Mr. ${name}` : `Welcome, Ms. ${name}`;

GOOD: ICU MessageFormat

{
  "messages": {
    "count": "{count, plural, =0 {No messages} one {# message} other {# messages}}"
  },
  "greeting": {
    "formal": "{gender, select, male {Welcome, Mr. {name}} female {Welcome, Ms. {name}} other {Welcome, {name}}}"
  },
  "cart": {
    "items": "{itemCount, plural, =0 {Your cart is empty} one {# item in cart} other {# items in cart}}",
    "total": "{total, number, ::currency/USD}"
  }
}
// Usage with react-intl
import { useIntl } from 'react-intl';

function MessageCount({ count }: { count: number }) {
  const intl = useIntl();
  return {intl.formatMessage({ id: 'messages.count' }, { count })};
}

Next.js 16 with next-intl

BAD: Client-side only, no routing

// app/page.tsx - DON'T
'use client';
import { useState } from 'react';

export default function Page() {
  const [locale, setLocale] = useState('en');
  const messages = require(`@/messages/${locale}.json`);

  return {messages.welcome};
}

GOOD: Server-side with middleware routing

// i18n.ts
import { notFound } from 'next/navigation';
import { getRequestConfig } from 'next-intl/server';

export const locales = ['en', 'es', 'fr', 'ar'] as const;
export type Locale = (typeof locales)[number];

export default getRequestConfig(async ({ locale }) => {
  if (!locales.includes(locale as Locale)) notFound();

  return {
    messages: (await import(`./messages/${locale}.json`)).default
  };
});

// middleware.ts
import createMiddleware from 'next-intl/middleware';
import { locales } from './i18n';

export default createMiddleware({
  locales,
  defaultLocale: 'en',
  localePrefix: 'always' // /en/about, /es/about
});

export const config = {
  matcher: ['/', '/(en|es|fr|ar)/:path*']
};

// app/[locale]/layout.tsx
import { NextIntlClientProvider } from 'next-intl';
import { getMessages } from 'next-intl/server';
import { notFound } from 'next/navigation';
import { locales } from '@/i18n';

export function generateStaticParams() {
  return locales.map((locale) => ({ locale }));
}

export default async function LocaleLayout({
  children,
  params: { locale }
}: {
  children: React.ReactNode;
  params: { locale: string };
}) {
  if (!locales.includes(locale as any)) notFound();

  const messages = await getMessages();

  return (
    
      
        
          {children}
        
      
    
  );
}

// app/[locale]/page.tsx
import { useTranslations } from 'next-intl';

export default function HomePage() {
  const t = useTranslations('home');

  return (
    
      {t('title')}
      {t('hero.heading')}
    
  );
}

Date and Number Formatting

BAD: Hardcoded formats

// DON'T: Breaks internationalization
const date = new Date().toLocaleDateString('en-US');
const price = `$${amount.toFixed(2)}`;
const percent = `${(value * 100).toFixed(1)}%`;

GOOD: Intl API

import { useIntl } from 'react-intl';

function ProductCard({ price, date, discount }: Props) {
  const intl = useIntl();

  // Date formatting
  const formattedDate = intl.formatDate(date, {
    year: 'numeric',
    month: 'long',
    day: 'numeric'
  });

  // Currency formatting
  const formattedPrice = intl.formatNumber(price, {
    style: 'currency',
    currency: 'USD'
  });

  // Percentage formatting
  const formattedDiscount = intl.formatNumber(discount, {
    style: 'percent',
    minimumFractionDigits: 0,
    maximumFractionDigits: 1
  });

  return (
    
      {formattedPrice}
      {formattedDiscount} off
      {formattedDate}
    
  );
}

// Or use native Intl API directly
const dateFormatter = new Intl.DateTimeFormat('es-ES', {
  dateStyle: 'full',
  timeStyle: 'short'
});

const numberFormatter = new Intl.NumberFormat('de-DE', {
  style: 'decimal',
  minimumFractionDigits: 2
});

console.log(dateFormatter.format(new Date())); // "domingo, 2 de marzo de 2026, 14:30"
console.log(numberFormatter.format(1234.5)); // "1.234,50"

RTL Layout Support

BAD: Hardcoded left/right

/* DON'T: Breaks in RTL languages */
.sidebar {
  float: left;
  margin-right: 20px;
  text-align: left;
}

.arrow {
  padding-left: 10px;
}

GOOD: Logical properties

/* Use logical properties */
.sidebar {
  float: inline-start;
  margin-inline-end: 20px;
  text-align: start;
}

.arrow {
  padding-inline-start: 10px;
}

/* Or use Tailwind RTL utilities */
.element {
  @apply ms-4 me-2; /* margin-start, margin-end */
  @apply ps-4 pe-2; /* padding-start, padding-end */
}

/* RTL-specific overrides */
[dir="rtl"] .custom-element {
  transform: scaleX(-1); /* Flip icons */
}
// tailwind.config.ts
import type { Config } from 'tailwindcss';

const config: Config = {
  content: ['./app/**/*.{ts,tsx}'],
  theme: {
    extend: {}
  },
  plugins: [
    require('tailwindcss-logical')
  ]
};

export default config;

Dynamic Locale Loading

BAD: Load all locales upfront

// DON'T: Bundles all translations
import en from './messages/en.json';
import es from './messages/es.json';
import fr from './messages/fr.json';
import ar from './messages/ar.json';

const messages = { en, es, fr, ar };

GOOD: Dynamic imports

// lib/i18n.ts
export async function loadMessages(locale: string) {
  try {
    const messages = await import(`@/messages/${locale}.json`);
    return messages.default;
  } catch (error) {
    console.error(`Failed to load messages for locale: ${locale}`);
    return import('@/messages/en.json').then(m => m.default);
  }
}

// For large translation files, split by namespace
export async function loadNamespace(locale: string, namespace: string) {
  const messages = await import(`@/messages/${locale}/${namespace}.json`);
  return messages.default;
}

// app/[locale]/dashboard/page.tsx
import { loadNamespace } from '@/lib/i18n';

export default async function DashboardPage({ params }: Props) {
  const messages = await loadNamespace(params.locale, 'dashboard');

  return 
    {/* Dashboard content */}
  ;
}

Language Detection and Switching

BAD: Manual cookie management

// DON'T: Unreliable, no fallback
'use client';
import { useRouter } from 'next/navigation';

function LangSwitcher() {
  const router = useRouter();

  const changeLocale = (locale: string) => {
    document.cookie = `locale=${locale}`;
    router.refresh();
  };

  return  changeLocale('es')}>Español;
}

GOOD: next-intl locale switching

// components/LocaleSwitcher.tsx
'use client';
import { useLocale, useTranslations } from 'next-intl';
import { useRouter, usePathname } from 'next/navigation';
import { locales } from '@/i18n';

export function LocaleSwitcher() {
  const t = useTranslations('common');
  const locale = useLocale();
  const router = useRouter();
  const pathname = usePathname();

  const changeLocale = (newLocale: string) => {
    // Replace locale in pathname
    const segments = pathname.split('/');
    segments[1] = newLocale;
    router.push(segments.join('/'));
  };

  return (
     changeLocale(e.target.value)}
      aria-label={t('selectLanguage')}
    >
      {locales.map((loc) => (
        
          {t(`locales.${loc}`)}
        
      ))}
    
  );
}

// With content negotiation fallback
// middleware.ts
import createMiddleware from 'next-intl/middleware';
import { locales } from './i18n';

export default createMiddleware({
  locales,
  defaultLocale: 'en',
  localeDetection: true, // Uses Accept-Language header
  localePrefix: 'as-needed' // Only add prefix for non-default locales
});

Type-Safe Translations

BAD: String keys, runtime errors

// DON'T: Typos cause runtime errors
const title = t('home.titel'); // Typo: "titel" instead of "title"
const message = t('user.proifle.name'); // Typo: "proifle"

GOOD: Generated types

// scripts/generate-types.ts
import fs from 'fs';
import path from 'path';

function generateTypes() {
  const enMessages = JSON.parse(
    fs.readFileSync('./messages/en.json', 'utf-8')
  );

  const types = generateTypeFromObject(enMessages);

  fs.writeFileSync(
    './types/messages.d.ts',
    `export type Messages = ${types};`
  );
}

// types/messages.d.ts (generated)
export type Messages = {
  common: {
    welcome: string;
    loading: string;
  };
  home: {
    title: string;
    hero: {
      heading: string;
      subheading: string;
    };
  };
  user: {
    profile: {
      name: string;
      email: string;
    };
  };
};

// Usage with type safety
import { useTranslations } from 'next-intl';

function HomePage() {
  const t = useTranslations('home');

  // TypeScript autocomplete and validation
  return {t('title')}; // ✓
  // return {t('titel')}; // ✗ TypeScript error
}

Common Patterns

Pluralization with Complex Rules

{
  "items": {
    "selected": "{count, plural, =0 {No items selected} one {# item selected} other {# items selected}}",
    "remaining": "{count, plural, =0 {All done!} one {# item left} other {# items left}}"
  }
}

Rich Text Formatting

// messages/en.json
{
  "terms": "By signing up, you agree to our Terms of Service"
}

// Component
import { useTranslations } from 'next-intl';
import Link from 'next/link';

function SignupForm() {
  const t = useTranslations('auth');

  return (
    
      {t.rich('terms', {
        link: (chunks) => {chunks}
      })}
    
  );
}

Server Component Translations

// app/[locale]/about/page.tsx
import { getTranslations } from 'next-intl/server';

export async function generateMetadata({ params }: Props) {
  const t = await getTranslations({ locale: params.locale, namespace: 'about' });

  return {
    title: t('metaTitle'),
    description: t('metaDescription')
  };
}

export default async function AboutPage({ params }: Props) {
  const t = await getTranslations({ locale: params.locale, namespace: 'about' });

  return {t('title')};
}

Apply these patterns to build robust, scalable internationalization in web applications.

Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

Install and usage instructions live in the source repository linked above.

Reviews

No reviews yet — be the first.

Versions

  • v0.1.0 Imported from the upstream source.