# Multi Language Localization

> Best practices for multi-language static websites with proper i18n and l10n support

- Skill: `hack23/multi-language-localization` (Agent Skill)
- Install (CLI): `npx skillmds@latest add hack23/multi-language-localization`
- Raw SKILL.md: https://api.skillmd.com/api/skills/hack23/multi-language-localization/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- License: Apache-2.0
- Author: Hack23 (https://skillmd.com/u/hack23)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/hack23/multi-language-localization

---


# Multi-Language Localization Skill


## 🔴 AI FIRST Quality Principle

> **Apply the AI FIRST principle: never accept first-pass quality. Minimum 2 iterations. Read all output, improve every section. No shortcuts.**

## Purpose

Implement proper internationalization (i18n) and localization (l10n) for static websites supporting multiple languages.

## Core Principles

### 1. Language Declaration
```html
<!-- ✅ Always declare language -->
<html lang="en">

<!-- For Swedish -->
<html lang="sv">

<!-- For Arabic (RTL) -->
<html lang="ar" dir="rtl">
```

### 2. File Structure
```
index.html       (en - English, default)
index_sv.html    (sv - Swedish)
index_da.html    (da - Danish)
index_no.html    (no - Norwegian)
index_fi.html    (fi - Finnish)
index_de.html    (de - German)
index_fr.html    (fr - French)
index_es.html    (es - Spanish)
index_nl.html    (nl - Dutch)
index_ar.html    (ar - Arabic, RTL)
index_he.html    (he - Hebrew, RTL)
index_ja.html    (ja - Japanese)
index_ko.html    (ko - Korean)
index_zh.html    (zh - Chinese)
```

### 3. Language Switcher
```html
<nav aria-label="Language selection">
  <ul>
    <li><a href="index.html" hreflang="en">English</a></li>
    <li><a href="index_sv.html" hreflang="sv">Svenska</a></li>
    <li><a href="index_ar.html" hreflang="ar">العربية</a></li>
  </ul>
</nav>
```

### 4. RTL Support
```css
/* RTL-specific styles */
[dir="rtl"] .element {
  text-align: right;
  direction: rtl;
}

/* Use logical properties */
.element {
  margin-inline-start: 1rem;  /* Not margin-left */
  padding-inline-end: 1rem;   /* Not padding-right */
}
```

### 5. Cultural Considerations
- Date formats (US: MM/DD/YYYY, EU: DD/MM/YYYY)
- Number formats (1,000.00 vs 1.000,00)
- Currency symbols
- Text direction (LTR vs RTL)
- Color meanings (cultural significance)

## SEO Best Practices

### Hreflang Tags
```html
<link rel="alternate" hreflang="en" href="https://example.com/index.html">
<link rel="alternate" hreflang="sv" href="https://example.com/index_sv.html">
<link rel="alternate" hreflang="x-default" href="https://example.com/index.html">
```

### Sitemap
```xml
<url>
  <loc>https://example.com/index.html</loc>
  <xhtml:link rel="alternate" hreflang="sv" href="https://example.com/index_sv.html"/>
</url>
```

## Testing

- Native speaker review
- RTL layout testing
- Character encoding verification
- Cultural appropriateness check
- SEO validation (hreflang)

## Remember

- **Native Speakers**: Use professional translation
- **Cultural Context**: Consider cultural differences
- **RTL Support**: Test right-to-left languages
- **Consistent UX**: Same experience across languages
- **SEO**: Proper hreflang and sitemap

## References

- [W3C Internationalization](https://www.w3.org/International/)
- [MDN Localization](https://developer.mozilla.org/en-US/docs/Mozilla/Localization)

---

## Number and Date Formatting (Production Data)

This section provides **validated formatting** from actual translated news articles (Feb 2026).

### Number Formatting by Language

**Thousands Separator and Decimal Point**:

| Language | Thousands | Decimal | Example | Usage |
|----------|-----------|---------|---------|-------|
| English (en) | comma (,) | period (.) | 1,000.50 | International standard |
| Swedish (sv) | space ( ) | comma (,) | 1 000,50 | Official Swedish standard |
| German (de) | space ( ) or period (.) | comma (,) | 1 000,50 or 1.000,50 | Space preferred |
| French (fr) | space ( ) | comma (,) | 1 000,50 | Official French standard |
| Spanish (es) | space ( ) or period (.) | comma (,) | 1 000,50 or 1.000,50 | Space preferred |
| Dutch (nl) | period (.) | comma (,) | 1.000,50 | Period for thousands |
| Danish (da) | period (.) | comma (,) | 1.000,50 | Period for thousands |
| Norwegian (no) | space ( ) | comma (,) | 1 000,50 | Official Norwegian standard |
| Finnish (fi) | space ( ) | comma (,) | 1 000,50 | Official Finnish standard |
| Japanese (ja) | comma (,) | period (.) | 1,000.50 | Western style |
| Korean (ko) | comma (,) | period (.) | 1,000.50 | Western style |
| Chinese (zh) | comma (,) | period (.) | 1,000.50 | Western style |
| Arabic (ar) | comma (,) | period (.) | ١٬٠٠٠٫٥٠ or 1,000.50 | Arabic-Indic optional |
| Hebrew (he) | comma (,) | period (.) | 1,000.50 | Western style |

**Implementation**:
```javascript
// Format numbers for language with documented separators
function formatNumber(num, lang) {
  const formats = {
    'en': { thousands: ',', decimal: '.' },
    'sv': { thousands: ' ', decimal: ',' },
    'de': { thousands: ' ', decimal: ',' },
    'fr': { thousands: ' ', decimal: ',' },
    'es': { thousands: ' ', decimal: ',' },
    'nl': { thousands: '.', decimal: ',' },
    'da': { thousands: '.', decimal: ',' },
    'no': { thousands: ' ', decimal: ',' },
    'fi': { thousands: ' ', decimal: ',' },
    'ja': { thousands: ',', decimal: '.' },
    'ko': { thousands: ',', decimal: '.' },
    'zh': { thousands: ',', decimal: '.' },
    'ar': { thousands: ',', decimal: '.' },
    'he': { thousands: ',', decimal: '.' }
  };
  const fmt = formats[lang] || formats['en'];
  
  // Normalize to two decimals, then trim to 0–2 as needed
  const isNegative = num < 0;
  const absolute = Math.abs(num);
  const fixed = absolute.toFixed(2);
  let [intPart, fracPart] = fixed.split('.');
  
  // Insert thousands separators in the integer part
  intPart = intPart.replace(/\B(?=(\d{3})+(?!\d))/g, fmt.thousands);
  
  // Trim trailing zeros from fractional part to allow 0–2 decimals
  fracPart = fracPart.replace(/0+$/, '');
  
  let result = intPart;
  if (fracPart.length > 0) {
    result += fmt.decimal + fracPart;
  }
  
  if (isNegative) {
    result = '-' + result;
  }
  
  return result;
}
```

### Date Formatting by Language

**From News Articles (Feb 2026)**:

| Language | Format | Example | Day-Month Order |
|----------|--------|---------|-----------------|
| English (en) | Month D, YYYY | February 15, 2026 | Month first |
| Swedish (sv) | D month YYYY | 15 februari 2026 | Day first |
| German (de) | D. Month YYYY | 15. Februar 2026 | Day first (with period) |
| French (fr) | D month YYYY | 15 février 2026 | Day first |
| Spanish (es) | D de month de YYYY | 15 de febrero de 2026 | Day first (with "de") |
| Dutch (nl) | D month YYYY | 15 februari 2026 | Day first |
| Danish (da) | D. month YYYY | 15. februar 2026 | Day first (with period) |
| Norwegian (no) | D. month YYYY | 15. februar 2026 | Day first (with period) |
| Finnish (fi) | D. monthkuuta YYYY | 15. helmikuuta 2026 | Day first (genitive case) |
| Japanese (ja) | YYYY年M月D日 | 2026年2月15日 | Year-Month-Day |
| Korean (ko) | YYYY년 M월 D일 | 2026년 2월 15일 | Year-Month-Day |
| Chinese (zh) | YYYY年M月D日 | 2026年2月15日 | Year-Month-Day |
| Arabic (ar) | D month YYYY | ١٥ فبراير ٢٠٢٦ | Day first (Arabic-Indic optional) |
| Hebrew (he) | D bmonth YYYY | 15 בפברואר 2026 | Day first (with ב prefix) |

**Month Names**:

| English | Swedish | German | French | Spanish | Dutch |
|---------|---------|--------|--------|---------|-------|
| January | januari | Januar | janvier | enero | januari |
| February | februari | Februar | février | febrero | februari |
| March | mars | März | mars | marzo | maart |
| April | april | April | avril | abril | april |
| May | maj | Mai | mai | mayo | mei |
| June | juni | Juni | juin | junio | juni |
| July | juli | Juli | juillet | julio | juli |
| August | augusti | August | août | agosto | augustus |
| September | september | September | septembre | septiembre | september |
| October | oktober | Oktober | octobre | octubre | oktober |
| November | november | November | novembre | noviembre | november |
| December | december | Dezember | décembre | diciembre | december |

**Day of Week**:

| English | Swedish | German | French | Spanish | Dutch | Finnish |
|---------|---------|--------|--------|---------|-------|---------|
| Monday | måndag | Montag | lundi | lunes | maandag | maanantai |
| Tuesday | tisdag | Dienstag | mardi | martes | dinsdag | tiistai |
| Wednesday | onsdag | Mittwoch | mercredi | miércoles | woensdag | keskiviikko |
| Thursday | torsdag | Donnerstag | jeudi | jueves | donderdag | torstai |
| Friday | fredag | Freitag | vendredi | viernes | vrijdag | perjantai |
| Saturday | lördag | Samstag | samedi | sábado | zaterdag | lauantai |
| Sunday | söndag | Sonntag | dimanche | domingo | zondag | sunnuntai |

### Time Formatting

**24-hour vs. 12-hour**:

| Language | Clock | Example | Note |
|----------|-------|---------|------|
| English (en) | 12-hour | 2:30 PM | AM/PM required |
| Swedish (sv) | 24-hour | 14:30 | Period or colon separator |
| German (de) | 24-hour | 14:30 Uhr | "Uhr" suffix |
| French (fr) | 24-hour | 14h30 | "h" separator |
| Spanish (es) | 24-hour | 14:30 | Colon separator |
| Nordic (da, no, fi) | 24-hour | 14:30 or 14.30 | Period common |
| CJK (ja, ko, zh) | 24-hour | 14:30 | Colon separator |
| Arabic (ar) | 12-hour | ٢:٣٠ م | Arabic-Indic optional |
| Hebrew (he) | 24-hour | 14:30 | Colon separator |

### Currency Formatting

**Swedish Krona (SEK)**:

| Language | Format | Example | Note |
|----------|--------|---------|------|
| English | SEK 1,000.50 or 1,000.50 kr | SEK 1,000.50 | Currency code preferred |
| Swedish | 1 000,50 kr | 1 000,50 kr | "kr" suffix standard |
| German | 1 000,50 SEK | 1 000,50 SEK | Currency code suffix |
| French | 1 000,50 SEK | 1 000,50 SEK | Currency code suffix |

### Ordinal Numbers

| Language | Example | Pattern |
|----------|---------|---------|
| English | 1st, 2nd, 3rd, 4th | -st, -nd, -rd, -th |
| Swedish | 1:a, 2:a, 3:e, 4:e | :a or :e |
| German | 1., 2., 3., 4. | Period suffix |
| French | 1er, 2e, 3e, 4e | -er for first, -e for rest |
| Spanish | 1.º, 2.º, 3.º, 4.º | Masculine ordinal |

### Practical Implementation

**Date Formatting Function**:
```javascript
function formatDate(date, lang) {
  const months = {
    en: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
    sv: ['januari', 'februari', 'mars', 'april', 'maj', 'juni', 'juli', 'augusti', 'september', 'oktober', 'november', 'december'],
    de: ['Januar', 'Februar', 'März', 'April', 'Mai', 'Juni', 'Juli', 'August', 'September', 'Oktober', 'November', 'Dezember'],
    fr: ['janvier', 'février', 'mars', 'avril', 'mai', 'juin', 'juillet', 'août', 'septembre', 'octobre', 'novembre', 'décembre'],
    es: ['enero', 'febrero', 'marzo', 'abril', 'mayo', 'junio', 'julio', 'agosto', 'septiembre', 'octubre', 'noviembre', 'diciembre'],
    da: ['januar', 'februar', 'marts', 'april', 'maj', 'juni', 'juli', 'august', 'september', 'oktober', 'november', 'december'],
    no: ['januar', 'februar', 'mars', 'april', 'mai', 'juni', 'juli', 'august', 'september', 'oktober', 'november', 'desember'],
    nl: ['januari', 'februari', 'maart', 'april', 'mei', 'juni', 'juli', 'augustus', 'september', 'oktober', 'november', 'december'],
    fi: ['tammikuuta', 'helmikuuta', 'maaliskuuta', 'huhtikuuta', 'toukokuuta', 'kesäkuuta', 'heinäkuuta', 'elokuuta', 'syyskuuta', 'lokakuuta', 'marraskuuta', 'joulukuuta']
  };
  
  const d = date.getDate();
  const m = date.getMonth();
  const y = date.getFullYear();
  
  if (lang === 'en') return `${months[lang][m]} ${d}, ${y}`;
  if (lang === 'de' || lang === 'da' || lang === 'no') return `${d}. ${months[lang][m]} ${y}`;
  if (lang === 'es') return `${d} de ${months[lang][m]} de ${y}`;
  if (lang === 'fi') return `${d}. ${months[lang][m]} ${y}`;
  if (lang === 'ja') return `${y}年${m+1}月${d}日`;
  if (lang === 'ko') return `${y}년 ${m+1}월 ${d}일`;
  if (lang === 'zh') return `${y}年${m+1}月${d}日`;
  return `${d} ${months[lang]?.[m] || months['en'][m]} ${y}`; // Default format with fallback
}
```

### Validation Checklist

For each language version, verify:
- [ ] Numbers use correct thousands separator
- [ ] Numbers use correct decimal point
- [ ] Dates follow language convention
- [ ] Month names correctly translated and case-appropriate
- [ ] Day names correctly translated
- [ ] Time uses 24-hour format (except English)
- [ ] Currency formatted per convention
- [ ] Ordinals follow language pattern

---

**Data Source**: Validated against 81 news articles (Feb 2026)  
**Standards**: Unicode CLDR, ISO 8601  
**Last Updated**: 2026-02-15

