i18n-localization
Internationalization and localization patterns. Detecting hardcoded strings, managing translations, locale files, RTL support.
Packaged view
This page reorganizes the original catalog entry around fit, installability, and workflow context first. The original raw source lives below.
Install command
npx @skill-hub/cli install vudovn-antigravity-kit-i18n-localization
Repository
Skill path: .agent/skills/i18n-localization
Internationalization and localization patterns. Detecting hardcoded strings, managing translations, locale files, RTL support.
Open repositoryBest for
Primary workflow: Ship Full Stack.
Technical facets: Full Stack.
Target audience: everyone.
License: Unknown.
Original source
Catalog source: SkillHub Club.
Repository owner: vudovn.
This is still a mirrored public skill entry. Review the repository before installing into production workflows.
What it helps with
- Install i18n-localization into Claude Code, Codex CLI, Gemini CLI, or OpenCode workflows
- Review https://github.com/vudovn/antigravity-kit before adding i18n-localization to shared team environments
- Use i18n-localization for development workflows
Works across
Favorites: 0.
Sub-skills: 0.
Aggregator: No.
Original source / Raw SKILL.md
---
name: i18n-localization
description: Internationalization and localization patterns. Detecting hardcoded strings, managing translations, locale files, RTL support.
allowed-tools: Read, Glob, Grep
---
# i18n & Localization
> Internationalization (i18n) and Localization (L10n) best practices.
---
## 1. Core Concepts
| Term | Meaning |
|------|---------|
| **i18n** | Internationalization - making app translatable |
| **L10n** | Localization - actual translations |
| **Locale** | Language + Region (en-US, tr-TR) |
| **RTL** | Right-to-left languages (Arabic, Hebrew) |
---
## 2. When to Use i18n
| Project Type | i18n Needed? |
|--------------|--------------|
| Public web app | ✅ Yes |
| SaaS product | ✅ Yes |
| Internal tool | ⚠️ Maybe |
| Single-region app | ⚠️ Consider future |
| Personal project | ❌ Optional |
---
## 3. Implementation Patterns
### React (react-i18next)
```tsx
import { useTranslation } from 'react-i18next';
function Welcome() {
const { t } = useTranslation();
return <h1>{t('welcome.title')}</h1>;
}
```
### Next.js (next-intl)
```tsx
import { useTranslations } from 'next-intl';
export default function Page() {
const t = useTranslations('Home');
return <h1>{t('title')}</h1>;
}
```
### Python (gettext)
```python
from gettext import gettext as _
print(_("Welcome to our app"))
```
---
## 4. File Structure
```
locales/
├── en/
│ ├── common.json
│ ├── auth.json
│ └── errors.json
├── tr/
│ ├── common.json
│ ├── auth.json
│ └── errors.json
└── ar/ # RTL
└── ...
```
---
## 5. Best Practices
### DO ✅
- Use translation keys, not raw text
- Namespace translations by feature
- Support pluralization
- Handle date/number formats per locale
- Plan for RTL from the start
- Use ICU message format for complex strings
### DON'T ❌
- Hardcode strings in components
- Concatenate translated strings
- Assume text length (German is 30% longer)
- Forget about RTL layout
- Mix languages in same file
---
## 6. Common Issues
| Issue | Solution |
|-------|----------|
| Missing translation | Fallback to default language |
| Hardcoded strings | Use linter/checker script |
| Date format | Use Intl.DateTimeFormat |
| Number format | Use Intl.NumberFormat |
| Pluralization | Use ICU message format |
---
## 7. RTL Support
```css
/* CSS Logical Properties */
.container {
margin-inline-start: 1rem; /* Not margin-left */
padding-inline-end: 1rem; /* Not padding-right */
}
[dir="rtl"] .icon {
transform: scaleX(-1);
}
```
---
## 8. Checklist
Before shipping:
- [ ] All user-facing strings use translation keys
- [ ] Locale files exist for all supported languages
- [ ] Date/number formatting uses Intl API
- [ ] RTL layout tested (if applicable)
- [ ] Fallback language configured
- [ ] No hardcoded strings in components
---
## Script
| Script | Purpose | Command |
|--------|---------|---------|
| `scripts/i18n_checker.py` | Detect hardcoded strings & missing translations | `python scripts/i18n_checker.py <project_path>` |
---
## Referenced Files
> The following files are referenced in this skill and included for context.
### scripts/i18n_checker.py
```python
#!/usr/bin/env python3
"""
i18n Checker - Detects hardcoded strings and missing translations.
Scans for untranslated text in React, Vue, and Python files.
"""
import sys
import re
import json
from pathlib import Path
# Fix Windows console encoding for Unicode output
try:
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
sys.stderr.reconfigure(encoding='utf-8', errors='replace')
except AttributeError:
pass # Python < 3.7
# Patterns that indicate hardcoded strings (should be translated)
HARDCODED_PATTERNS = {
'jsx': [
# Text directly in JSX: <div>Hello World</div>
r'>\s*[A-Z][a-zA-Z\s]{3,30}\s*</',
# JSX attribute strings: title="Welcome"
r'(title|placeholder|label|alt|aria-label)="[A-Z][a-zA-Z\s]{2,}"',
# Button/heading text
r'<(button|h[1-6]|p|span|label)[^>]*>\s*[A-Z][a-zA-Z\s!?.,]{3,}\s*</',
],
'vue': [
# Vue template text
r'>\s*[A-Z][a-zA-Z\s]{3,30}\s*</',
r'(placeholder|label|title)="[A-Z][a-zA-Z\s]{2,}"',
],
'python': [
# print/raise with string literals
r'(print|raise\s+\w+)\s*\(\s*["\'][A-Z][^"\']{5,}["\']',
# Flask flash messages
r'flash\s*\(\s*["\'][A-Z][^"\']{5,}["\']',
]
}
# Patterns that indicate proper i18n usage
I18N_PATTERNS = [
r't\(["\']', # t('key') - react-i18next
r'useTranslation', # React hook
r'\$t\(', # Vue i18n
r'_\(["\']', # Python gettext
r'gettext\(', # Python gettext
r'useTranslations', # next-intl
r'FormattedMessage', # react-intl
r'i18n\.', # Generic i18n
]
def find_locale_files(project_path: Path) -> list:
"""Find translation/locale files."""
patterns = [
"**/locales/**/*.json",
"**/translations/**/*.json",
"**/lang/**/*.json",
"**/i18n/**/*.json",
"**/messages/*.json",
"**/*.po", # gettext
]
files = []
for pattern in patterns:
files.extend(project_path.glob(pattern))
return [f for f in files if 'node_modules' not in str(f)]
def check_locale_completeness(locale_files: list) -> dict:
"""Check if all locales have the same keys."""
issues = []
passed = []
if not locale_files:
return {'passed': [], 'issues': ["[!] No locale files found"]}
# Group by parent folder (language)
locales = {}
for f in locale_files:
if f.suffix == '.json':
try:
lang = f.parent.name
content = json.loads(f.read_text(encoding='utf-8'))
if lang not in locales:
locales[lang] = {}
locales[lang][f.stem] = set(flatten_keys(content))
except:
continue
if len(locales) < 2:
passed.append(f"[OK] Found {len(locale_files)} locale file(s)")
return {'passed': passed, 'issues': issues}
passed.append(f"[OK] Found {len(locales)} language(s): {', '.join(locales.keys())}")
# Compare keys across locales
all_langs = list(locales.keys())
base_lang = all_langs[0]
for namespace in locales.get(base_lang, {}):
base_keys = locales[base_lang].get(namespace, set())
for lang in all_langs[1:]:
other_keys = locales.get(lang, {}).get(namespace, set())
missing = base_keys - other_keys
if missing:
issues.append(f"[X] {lang}/{namespace}: Missing {len(missing)} keys")
extra = other_keys - base_keys
if extra:
issues.append(f"[!] {lang}/{namespace}: {len(extra)} extra keys")
if not issues:
passed.append("[OK] All locales have matching keys")
return {'passed': passed, 'issues': issues}
def flatten_keys(d, prefix=''):
"""Flatten nested dict keys."""
keys = set()
for k, v in d.items():
new_key = f"{prefix}.{k}" if prefix else k
if isinstance(v, dict):
keys.update(flatten_keys(v, new_key))
else:
keys.add(new_key)
return keys
def check_hardcoded_strings(project_path: Path) -> dict:
"""Check for hardcoded strings in code files."""
issues = []
passed = []
# Find code files
extensions = {
'.tsx': 'jsx', '.jsx': 'jsx', '.ts': 'jsx', '.js': 'jsx',
'.vue': 'vue',
'.py': 'python'
}
code_files = []
for ext in extensions:
code_files.extend(project_path.rglob(f"*{ext}"))
code_files = [f for f in code_files if not any(x in str(f) for x in
['node_modules', '.git', 'dist', 'build', '__pycache__', 'venv', 'test', 'spec'])]
if not code_files:
return {'passed': ["[!] No code files found"], 'issues': []}
files_with_i18n = 0
files_with_hardcoded = 0
hardcoded_examples = []
for file_path in code_files[:50]: # Limit
try:
content = file_path.read_text(encoding='utf-8', errors='ignore')
ext = file_path.suffix
file_type = extensions.get(ext, 'jsx')
# Check for i18n usage
has_i18n = any(re.search(p, content) for p in I18N_PATTERNS)
if has_i18n:
files_with_i18n += 1
# Check for hardcoded strings
patterns = HARDCODED_PATTERNS.get(file_type, [])
hardcoded_found = False
for pattern in patterns:
matches = re.findall(pattern, content)
if matches and not has_i18n:
hardcoded_found = True
if len(hardcoded_examples) < 5:
hardcoded_examples.append(f"{file_path.name}: {str(matches[0])[:40]}...")
if hardcoded_found:
files_with_hardcoded += 1
except:
continue
passed.append(f"[OK] Analyzed {len(code_files)} code files")
if files_with_i18n > 0:
passed.append(f"[OK] {files_with_i18n} files use i18n")
if files_with_hardcoded > 0:
issues.append(f"[X] {files_with_hardcoded} files may have hardcoded strings")
for ex in hardcoded_examples:
issues.append(f" → {ex}")
else:
passed.append("[OK] No obvious hardcoded strings detected")
return {'passed': passed, 'issues': issues}
def main():
target = sys.argv[1] if len(sys.argv) > 1 else "."
project_path = Path(target)
print("\n" + "=" * 60)
print(" i18n CHECKER - Internationalization Audit")
print("=" * 60 + "\n")
# Check locale files
locale_files = find_locale_files(project_path)
locale_result = check_locale_completeness(locale_files)
# Check hardcoded strings
code_result = check_hardcoded_strings(project_path)
# Print results
print("[LOCALE FILES]")
print("-" * 40)
for item in locale_result['passed']:
print(f" {item}")
for item in locale_result['issues']:
print(f" {item}")
print("\n[CODE ANALYSIS]")
print("-" * 40)
for item in code_result['passed']:
print(f" {item}")
for item in code_result['issues']:
print(f" {item}")
# Summary
critical_issues = sum(1 for i in locale_result['issues'] + code_result['issues'] if i.startswith("[X]"))
print("\n" + "=" * 60)
if critical_issues == 0:
print("[OK] i18n CHECK: PASSED")
sys.exit(0)
else:
print(f"[X] i18n CHECK: {critical_issues} issues found")
sys.exit(1)
if __name__ == "__main__":
main()
```