WDP Part 8: Internationalization (i18n)
Internationalization support for the Waddling Diagnostic Protocol (WDP). Defines how diagnostic codes, messages, and metadata can be localized for different languages and cultures while maintaining hash stability and cross-language interoperability.
Abstract#
This document specifies internationalization (i18n) support for the Waddling Diagnostic Protocol (WDP). It defines how diagnostic codes, messages, and metadata can be localized for different languages and cultures while maintaining hash stability and cross-language interoperability.
Specification Navigation: See STRUCTURE.md for an overview of all WDP specification parts.
Conformance
The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in BCP 14 [RFC2119] [RFC8174] when, and only when, they appear in all capitals, as shown here.
Key Concepts:
- Canonical codes (English) for hash computation
- Display aliases (any language) for user presentation
- Separate catalog files per locale
- Independent translation workflow
1. Introduction#
1.1 Purpose
This specification defines how WDP diagnostic codes and messages can be localized for international audiences while maintaining:
- Hash stability - Same error produces same compact ID across all languages
- Interoperability - Services in different languages can communicate
- User experience - Users see diagnostics in their native language
1.2 Scope
This document covers:
- Canonical vs localized diagnostic codes
- Catalog file structure for multiple languages
- Code aliases (localized display codes)
- Message translation and parameterization
- Locale-specific formatting (dates, numbers, currency)
- RTL language support
- Translation workflows
1.3 Design Goals
- Separation of Identity and Presentation - Internal identity (hash) vs user display
- Any Language as Primary - Projects can work in their native language
- Hash Stability - Same diagnostic always produces same compact ID
- Independent Catalogs - Each language file is self-contained
- Simple Translation - Translators work on individual files
- Cultural Sensitivity - Respect linguistic diversity
2. Core i18n Principles#
2.1 Two-Layer Identity System
WDP diagnostics have two identities:
Internal Identity (Machine)
- Canonical code: Always English (e.g.,
E.Auth.Token.001) - Compact ID: Hash of canonical code (e.g.,
Ay75d) - Purpose: Hash computation, logs, APIs, databases
- Never changes: Required for interoperability
Display Identity (Human)
- Alias: Localized code for display (e.g.,
E.認証.トークン.001) - Message: Localized text (e.g., "トークンは{{timestamp}}に期限切れになりました")
- Purpose: User interface, error reports, documentation
- Localizable: Can be any language
2.2 Why English for Canonical Codes?
Technical Requirement: The canonical 4-part code MUST be in English to ensure hash stability across implementations.
Rationale:
- Hash Determinism - Same error = same compact ID everywhere
- ASCII Compatibility - Works in URLs, terminals, file systems
- No Normalization Issues - Avoids Unicode problems
- Industry Standard - Aligns with programming conventions
Important: This does NOT require:
- English messages for users
- English documentation
- English as team's working language
The canonical code is an internal identifier. Users see localized aliases.
2.3 Primary Locale Flexibility
While canonical codes are always English, the primary locale (the language your team works in) can be any language:
- Japanese team → Primary locale:
"ja" - French team → Primary locale:
"fr" - Chinese team → Primary locale:
"zh" - English team → Primary locale:
"en"
Other languages are translations from your primary locale.
3. Canonical vs Display Identity#
3.1 What Never Changes (Canonical)
These are immutable across all languages:
- Canonical 4-part code (English)
E.Auth.Token.001- Compact ID (hash of canonical code)
Ay75d- Hash algorithm
xxHash64("E.Auth.Token.001", seed="wdp-v1") → Base62 → Ay75d3.2 What Can Be Localized (Display)
These are localizable per language:
- Display alias (localized code)
- English:
E.Auth.Token.001 - Japanese:
E.認証.トークン.001 - Chinese:
E.认证.令牌.001
- Messages
- English:
"Token expired at {{timestamp}}" - Japanese:
"トークンは{{timestamp}}に期限切れになりました"
- Descriptions, hints, resolutions (all text fields)
3.3 Hash Computation (Always from Canonical)
The compact ID is always computed from the English canonical code:
// Canonical code (English)
const canonical = "E.Auth.Token.001";
// Compute hash
const hash = xxHash64(canonical, seed: "wdp-v1");
const compactId = base62Encode(hash).substring(0, 5);
// → "Ay75d"
// This hash is the same regardless of user's language!All language catalogs must use the same compact ID:
// catalog-en.json
{
"E.Auth.Token.001": {
"compact_id": "Ay75d" // ← Same hash
}
}
// catalog-ja.json
{
"E.Auth.Token.001": {
"compact_id": "Ay75d" // ← Same hash
}
}4. Catalog File Structure#
4.1 One File Per Language
Each language has its own independent catalog file:
catalogs/
catalog-en.json # English
catalog-ja.json # Japanese
catalog-zh.json # Chinese (Simplified)
catalog-zh-TW.json # Chinese (Traditional)
catalog-es.json # Spanish
catalog-fr.json # French
catalog-de.json # German
catalog-ar.json # Arabic4.2 File Naming Convention
Catalog files MUST be named:
catalog-{locale}.jsonWhere {locale} is an IETF BCP 47 language tag.
Examples:
catalog-en.json- Englishcatalog-ja.json- Japanesecatalog-zh.json- Chinese (Simplified)catalog-zh-TW.json- Chinese (Traditional, Taiwan)catalog-es-MX.json- Spanish (Mexico)catalog-pt-BR.json- Portuguese (Brazil)
4.3 Catalog File Format
Each catalog file has identical structure:
{
"wdp_version": "1.0",
"locale": "ja",
"namespace": "myapp",
"diags": {
"E.Auth.Token.001": {
"compact_id": "Ay75d",
"alias": "E.認証.トークン.001",
"message": "トークンは{{timestamp}}に期限切れになりました",
"description": "セッションの有効期限が切れました",
"hints": [
"もう一度ログインしてください",
"セッションは30分後に期限切れになります"
]
}
}
}4.4 Required Fields
Catalog Level:
wdp_version(string) - WDP version (e.g.,"1.0")locale(string) - IETF BCP 47 language tag (e.g.,"ja")namespace(string) - Application namespace (e.g.,"myapp")diags(object) - Diagnostic entries keyed by canonical code
Diagnostic Entry:
compact_id(string) - 5-character Base62 hash (e.g.,"Ay75d")alias(string) - Localized display codemessage(string) - Localized message template with{{field}}placeholders
Optional Fields:
description(string) - Detailed explanationhints(array of strings) - Actionable suggestionsresolution(string) - How to fixdocs_url(string) - Documentation link
4.5 Canonical Code as Key
The top-level keys in diags MUST always be the canonical English code:
{
"diags": {
"E.Auth.Token.001": { // ← Canonical (English) - always this
"alias": "E.認証.トークン.001" // ← Display (localized)
}
}
}Never use localized codes as keys:
// Incorrect:
{
"diags": {
"E.認証.トークン.001": { ... } // Invalid: must use canonical code
}
}5. Code Aliases#
5.1 What Are Code Aliases?
A code alias is a localized version of the diagnostic code for display purposes.
Example:
- Canonical code:
E.Auth.Token.001(internal, English) - Japanese alias:
E.認証.トークン.001(display, Japanese) - Chinese alias:
E.认证.令牌.001(display, Chinese)
All three aliases map to the same internal code and same compact ID.
5.2 Alias Structure
Aliases follow the same 4-part structure but with localized words:
Severity.Component.Primary.SequenceExample localization:
5.3 Severity Letter
The severity letter in display aliases SHOULD remain the same as the canonical code (English letter: E, W, C, B, S, K, I, T, H).
Rationale:
- Consistent across all languages for log searching
- Many languages use same letters (Spanish: Error = E, French: Erreur = E, German: Fehler = F)
- Enables cross-language API integration
- Single character is hard to translate meaningfully in all languages
UI Display: Use emoji from Part 9 for visual presentation, SEPARATE from the code alias:
// Display in UI
const emoji = getSeverityEmoji('E'); // Returns error emoji (❌)
const display = `${emoji} ${alias}`; // Example: "❌ E.認証.トークン.001"5.4 Uniqueness Constraint
Within each locale, aliases MUST be unique. A single alias cannot map to multiple diagnostic codes.
5.5 Bidirectional Lookup
The alias system supports bidirectional lookup:
Alias → Internal Code:
// User reports: "E.認証.トークン.001"
const canonical = lookupCanonical("E.認証.トークン.001", locale="ja");
// → "E.Auth.Token.001"
const compactId = lookupCompactId(canonical);
// → "Ay75d"Internal Code → All Aliases:
const aliases = lookupAliases("E.Auth.Token.001");
// → {
// en: "E.Auth.Token.001",
// ja: "E.認証.トークン.001",
// zh: "E.认证.令牌.001"
// }6. Localized Messages#
6.1 Message Templates
Messages use {{field}} placeholder syntax (double braces):
{
"E.Auth.Token.001": {
"message": "Token expired at {{timestamp}}"
}
}Localized versions:
// catalog-en.json
{
"message": "Token expired at {{timestamp}}"
}
// catalog-ja.json
{
"message": "トークンは{{timestamp}}に期限切れになりました"
}
// catalog-zh.json
{
"message": "令牌已在{{timestamp}}过期"
}
// catalog-es.json
{
"message": "El token expiró en {{timestamp}}"
}6.2 Placeholder Syntax
Correct: {{field}} (double braces)
{
"message": "User {{username}} logged in at {{timestamp}}"
}Incorrect: {field} (single braces)
{
"message": "User {username} logged in at {timestamp}" // Invalid: must use double braces
}6.3 Field Interpolation
Clients replace placeholders with field values:
const template = "Token expired at {{timestamp}}";
const fields = { timestamp: "2024-01-15T10:30:00Z" };
const message = template.replace(/\{\{(\w+)\}\}/g, (_, field) =>
fields[field] || `{{${field}}}`
);
// → "Token expired at 2024-01-15T10:30:00Z"6.4 No Template Expressions
WDP v1 supports only simple string replacement. Template expressions are NOT supported:
Supported:
{
"message": "Quota is {{quota_percent}}% full"
}NOT Supported:
{
"message": "Quota is {{quota_used * 100 / quota_limit}}% full" // Invalid: expressions not supported
}Solution: Server must calculate derived values:
{
"compact_id": "wN4Qm",
"fields": {
"quota_used": "850",
"quota_limit": "1000",
"quota_percent": "85" // ← Server calculates this
}
}6.5 Pluralization
For plural-sensitive messages, use ICU MessageFormat or similar:
// catalog-en.json
{
"E.Validation.Items.001": {
"message": "{count, plural, one {# item failed} other {# items failed}}"
}
}
// catalog-ja.json (Japanese doesn't pluralize)
{
"E.Validation.Items.001": {
"message": "{{count}}個のアイテムが失敗しました"
}
}Note: Pluralization is implementation-specific. Use standard i18n libraries (e.g., i18next, Intl.MessageFormat).
7. Translation Workflow#
7.1 No Master File Required
The specification does NOT require a master file containing all translations. Each language catalog is independent and self-contained.
7.2 Primary Locale
Each project designates one locale as the "primary" or "source" language. This is typically the language developers write diagnostics in.
How to declare primary locale:
Option 1: Configuration file
// wdp.config.json
{
"wdp": {
"primary_locale": "ja",
"supported_locales": ["ja", "en", "zh", "fr"]
}
}Option 2: Documentation
# README.md
## Internationalization
Primary language: Japanese (`catalog-ja.json`)
All translations are made from Japanese.7.3 Translation Process
Step 1: Developer creates diagnostic in primary language
// catalog-ja.json (primary)
{
"E.Auth.Token.001": {
"compact_id": "Ay75d",
"alias": "E.認証.トークン.001",
"message": "トークンは{{timestamp}}に期限切れになりました",
"description": "セッションの有効期限が切れました"
}
}Step 2: Translator receives primary catalog, creates translated catalog
// catalog-en.json (translated from ja)
{
"E.Auth.Token.001": {
"compact_id": "Ay75d",
"alias": "E.Auth.Token.001",
"message": "Token expired at {{timestamp}}",
"description": "Your session has expired"
}
}Step 3: Each translator works on their own file
- English translator:
catalog-en.json - Chinese translator:
catalog-zh.json - French translator:
catalog-fr.json
No merge conflicts! Each person works independently.
7.4 Version Control
# Developer adds new diagnostic (primary language)
git add catalogs/catalog-ja.json
git commit -m "feat: add token expiration error"
# English translator (separate commit, no conflicts)
git add catalogs/catalog-en.json
git commit -m "i18n: translate token expiration to English"
# Chinese translator (separate commit, no conflicts)
git add catalogs/catalog-zh.json
git commit -m "i18n: translate token expiration to Chinese"8. Locale-Specific Formatting#
8.1 Date and Time Formatting
Use locale-appropriate date/time formats:
// Server provides ISO 8601 timestamp
const fields = {
timestamp: "2024-01-15T10:30:00Z"
};
// Client formats for user's locale
const date = new Date(fields.timestamp);
// English (US)
date.toLocaleString('en-US');
// → "1/15/2024, 10:30:00 AM"
// Japanese
date.toLocaleString('ja-JP');
// → "2024/1/15 10:30:00"
// German
date.toLocaleString('de-DE');
// → "15.1.2024, 10:30:00"8.2 Number Formatting
Use locale-appropriate number formats:
const value = 1234567.89;
// English (US) - comma thousands separator, period decimal
value.toLocaleString('en-US');
// → "1,234,567.89"
// French - space thousands separator, comma decimal
value.toLocaleString('fr-FR');
// → "1 234 567,89"
// Japanese - no thousands separator
value.toLocaleString('ja-JP');
// → "1234567.89"8.3 Currency Formatting
Use locale-appropriate currency formats:
const amount = 1234.56;
// US Dollars
amount.toLocaleString('en-US', { style: 'currency', currency: 'USD' });
// → "$1,234.56"
// Japanese Yen
amount.toLocaleString('ja-JP', { style: 'currency', currency: 'JPY' });
// → "¥1,235" (no decimals)
// Euro (Germany)
amount.toLocaleString('de-DE', { style: 'currency', currency: 'EUR' });
// → "1.234,56 €"8.4 Recommendation
Server provides raw values, client formats for locale:
// Server response
{
"compact_id": "xY9Kp",
"fields": {
"amount": "1234.56",
"currency": "USD",
"timestamp": "2024-01-15T10:30:00Z"
}
}
// Client formats
const message = template
.replace('{{amount}}', formatCurrency(fields.amount, fields.currency, locale))
.replace('{{timestamp}}', formatDate(fields.timestamp, locale));9. RTL and BiDi Support#
9.1 Right-to-Left Languages
WDP supports RTL languages (Arabic, Hebrew, Persian, etc.).
Key considerations:
- Text direction is determined by locale
- Error codes remain LTR (left-to-right)
- Messages follow natural text direction
9.2 Display Considerations
English (LTR):
Error: E.Auth.Token.001
Token expired at 2024-01-15T10:30:00ZArabic (RTL):
خطأ: E.Auth.Token.001
انتهت صلاحية الرمز المميز في 2024-01-15T10:30:00ZNote: Error code remains LTR even in RTL text.
9.3 HTML/CSS Direction
<!-- Arabic -->
<div dir="rtl" lang="ar">
<p>خطأ: <code dir="ltr">E.Auth.Token.001</code></p>
<p>انتهت صلاحية الرمز المميز في 2024-01-15T10:30:00Z</p>
</div>
<!-- Hebrew -->
<div dir="rtl" lang="he">
<p>שגיאה: <code dir="ltr">E.Auth.Token.001</code></p>
<p>אסימון פג ב-2024-01-15T10:30:00Z</p>
</div>9.4 Bidirectional Algorithm
Use Unicode Bidirectional Algorithm (UAX #9) for mixed LTR/RTL content:
// catalog-ar.json
{
"E.Auth.Token.001": {
"alias": "E.Auth.Token.001", // Keep code LTR
"message": "انتهت صلاحية الرمز المميز في {{timestamp}}", // Message RTL
"description": "انتهت صلاحية جلستك"
}
}Rendering:
- Overall direction: RTL (Arabic)
- Error code: LTR (embedded in RTL context)
- Unicode BiDi algorithm handles mixing automatically
10. Fallback Strategies#
10.1 Fallback Chain
When a requested locale is unavailable, use a fallback chain:
// User requests: zh-TW (Traditional Chinese, Taiwan)
// Fallback chain:
// 1. zh-TW (exact match)
// 2. zh (parent locale)
// 3. en (default/primary)
// 4. compact ID only (Ay75d)
async function loadCatalog(requestedLocale, primaryLocale = 'en') {
const locales = [
requestedLocale, // zh-TW
requestedLocale.split('-')[0], // zh
primaryLocale, // en
];
for (const locale of locales) {
try {
const catalog = await fetch(`/catalogs/catalog-${locale}.json`);
if (catalog.ok) return catalog.json();
} catch (error) {
continue;
}
}
// No catalog available - display compact IDs only
return null;
}10.2 Partial Translation Handling
If a catalog has partial translations, fall back to primary locale for missing entries:
function getMessage(code, locale, primaryLocale = 'en') {
// Try requested locale
let entry = catalogs[locale]?.diags[code];
if (entry?.message) return entry.message;
// Fall back to primary locale
entry = catalogs[primaryLocale]?.diags[code];
if (entry?.message) return entry.message;
// Fall back to compact ID
return entry?.compact_id || code;
}10.3 Mixed Language Display
When displaying multiple diagnostics with different translation states:
// Some messages available in user's language, others not
const diagnostics = [
{ code: "E.Auth.Token.001", locale: "ja" }, // Translated
{ code: "E.Payment.Failed.005", locale: "en" }, // Fallback to English
{ code: "E.Unknown.Error.999", locale: null }, // Only compact ID
];Display:
❌ エラー: E.認証.トークン.001
トークンは2024-01-15T10:30:00Zに期限切れになりました
❌ Error: E.Payment.Failed.005
Payment processing failed (English fallback)
❌ Error: Ay75d
(No translation available)11. Implementation Guidelines#
11.1 Client-Side Pattern
Loading catalogs:
// Detect user's language
const userLocale = navigator.language || 'en';
// Load catalog
async function loadCatalog(locale) {
try {
const response = await fetch(`https://cdn.example.com/wdp/v1/catalog-${locale}.json`);
return response.json();
} catch (error) {
console.warn(`Failed to load catalog-${locale}.json:`, error);
return null;
}
}
// Initialize
const catalog = await loadCatalog(userLocale);Expanding diagnostics:
function expandDiagnostic(compactId, fields, catalog, locale) {
// Find entry by compact ID
const entry = Object.values(catalog.diags).find(
d => d.compact_id === compactId
);
if (!entry) {
return { code: compactId, message: `Unknown error: ${compactId}` };
}
// Expand message template
let message = entry.message;
for (const [field, value] of Object.entries(fields)) {
message = message.replace(new RegExp(`\\{\\{${field}\\}\\}`, 'g'), value);
}
return {
code: entry.alias,
compactId: compactId,
message: message,
description: entry.description,
hints: entry.hints
};
}11.2 Server-Side Pattern
Generating diagnostics:
// Server does NOT need to know user's language
// Just send canonical code + compact ID + fields
function generateDiagnostic(canonicalCode, fields) {
const compactId = computeCompactId(canonicalCode);
return {
[compactId]: {
f: fields
}
};
}
// Usage
app.post('/api/login', (req, res) => {
if (tokenExpired) {
res.status(401).json({
wd: generateDiagnostic('E.Auth.Token.001', {
timestamp: new Date().toISOString()
})
});
}
});11.3 Language Selection (Out of Scope)
How clients determine which language to request is NOT defined by this specification.
Standard approaches include:
- HTTP
Accept-Languageheader - Browser
navigator.languageAPI - Application user preferences
- URL parameters (
?lang=ja) - Cookie/localStorage
- Operating system locale
12. Complete Examples#
12.1 Catalog Files
catalog-en.json (English)
{
"wdp_version": "1.0",
"locale": "en",
"namespace": "myapp",
"diags": {
"E.Auth.Token.001": {
"compact_id": "Ay75d",
"alias": "E.Auth.Token.001",
"message": "Token expired at {{timestamp}}",
"description": "Your session has expired. Please log in again.",
"hints": [
"Click the 'Login' button to sign in",
"Sessions expire after 30 minutes of inactivity"
],
"docs_url": "https://docs.example.com/errors/auth-token-expired"
},
"W.Quota.Storage.001": {
"compact_id": "wN4Qm",
"alias": "W.Quota.Storage.001",
"message": "Storage quota is {{quota_percent}}% full",
"description": "You are approaching your storage limit.",
"hints": [
"Delete unused files to free up space",
"Upgrade your plan for more storage"
]
}
}
}catalog-ja.json (Japanese)
{
"wdp_version": "1.0",
"locale": "ja",
"namespace": "myapp",
"translated_from": "en",
"diags": {
"E.Auth.Token.001": {
"compact_id": "Ay75d",
"alias": "E.認証.トークン.001",
"message": "トークンは{{timestamp}}に期限切れになりました",
"description": "セッションの有効期限が切れました。もう一度ログインしてください。",
"hints": [
"「ログイン」ボタンをクリックしてサインインしてください",
"セッションは30分間操作がないと期限切れになります"
],
"docs_url": "https://docs.example.com/ja/errors/auth-token-expired"
},
"W.Quota.Storage.001": {
"compact_id": "wN4Qm",
"alias": "W.割当.ストレージ.001",
"message": "ストレージ容量が{{quota_percent}}%使用されています",
"description": "ストレージの上限に近づいています。",
"hints": [
"不要なファイルを削除してスペースを確保してください",
"プランをアップグレードしてストレージを増やすことができます"
]
}
}
}12.2 API Response Examples
Response with Compact ID (Client Expands)
Server response:
{
"success": false,
"wd": {
"Ay75d": {
"f": {
"timestamp": "2024-01-15T10:30:00Z"
}
}
}
}Client expands (Japanese user):
const catalog = await fetch('/catalogs/catalog-ja.json').then(r => r.json());
const diagnostic = expandDiagnostic('Ay75d', { timestamp: '2024-01-15T10:30:00Z' }, catalog, 'ja');
console.log(diagnostic);
// {
// code: "E.認証.トークン.001",
// compactId: "Ay75d",
// message: "トークンは2024-01-15T10:30:00Zに期限切れになりました",
// description: "セッションの有効期限が切れました。もう一度ログインしてください。",
// hints: ["「ログイン」ボタンをクリックしてサインインしてください", ...]
// }References#
Normative References
- Part 1: Severity - Severity level definitions
- Part 2: Component - Component field specification
- Part 3: Primary - Primary field specification
- Part 4: Sequence - Sequence field specification
- Part 5: Compact IDs - Hash computation and Base62 encoding
- RFC 5646 - IETF BCP 47 Language Tags
- Unicode UAX #9 - Unicode Bidirectional Algorithm
Informative References
- Part 9: Presentation - Visual presentation guidelines
- ICU MessageFormat - Pluralization and formatting
- CLDR - Common Locale Data Repository
- Intl API - JavaScript internationalization