fvtt-localization
This skill should be used when implementing internationalization (i18n), creating language files, using game.i18n.localize/format, adding template localization helpers, or following best practices for translatable strings.
When & Why to Use This Skill
The fvtt-localization skill is a specialized toolkit designed for Foundry VTT developers to implement robust internationalization (i18n) in modules and systems. It streamlines the creation of JSON language files, ensures adherence to strict naming conventions, and provides expert guidance on utilizing the Foundry VTT i18n API. By automating the transition from hardcoded text to dynamic localized strings, it enables developers to build accessible, multi-language gaming environments while following industry best practices for software localization.
Use Cases
- Generating structured JSON language files with proper namespacing to prevent translation key conflicts between different modules.
- Refactoring JavaScript code to replace hardcoded strings with game.i18n.localize and game.i18n.format for dynamic content and variable interpolation.
- Implementing localization helpers within Handlebars (.hbs) templates to ensure UI elements like buttons, labels, and tooltips are translatable.
- Configuring module.json or system.json manifests to correctly register language paths and ISO 639-1/2 codes for global compatibility.
- Handling complex linguistic requirements such as pluralization logic and locale-aware list formatting using the native Foundry API.
- Ensuring SEO-friendly and maintainable codebases by following best practices for word order, context-specific strings, and fallback mechanisms.
| name | fvtt-localization |
|---|---|
| description | This skill should be used when implementing internationalization (i18n), creating language files, using game.i18n.localize/format, adding template localization helpers, or following best practices for translatable strings. |
Foundry VTT Localization
Domain: Foundry VTT Module/System Development Status: Production-Ready Last Updated: 2026-01-05
Overview
Foundry VTT uses JSON language files for internationalization. All user-facing text should be localized to support translation.
When to Use This Skill
- Creating language files for modules/systems
- Using localize/format in JavaScript code
- Adding localization to templates
- Following naming conventions for translatable strings
- Handling pluralization and interpolation
Language File Structure
Basic Format
{
"MYMODULE.Title": "My Module",
"MYMODULE.Settings.Enable": "Enable Feature",
"MYMODULE.Settings.EnableHint": "Turn this feature on or off",
"MYMODULE.Dialog.Confirm": "Are you sure?",
"MYMODULE.Button.Save": "Save",
"MYMODULE.Button.Cancel": "Cancel"
}
Namespace Convention
Use your package ID as prefix to avoid conflicts:
{
"MYSYSTEM.Actor.HP": "Hit Points",
"MYSYSTEM.Actor.AC": "Armor Class",
"MYSYSTEM.Item.Weight": "Weight"
}
Document Type Labels
{
"TYPES": {
"Actor": {
"character": "Character",
"npc": "Non-Player Character",
"vehicle": "Vehicle"
},
"Item": {
"weapon": "Weapon",
"armor": "Armor",
"spell": "Spell"
}
}
}
Manifest Registration
module.json / system.json
{
"id": "my-module",
"languages": [
{
"lang": "en",
"name": "English",
"path": "lang/en.json"
},
{
"lang": "es",
"name": "Español",
"path": "lang/es.json"
},
{
"lang": "fr",
"name": "Français",
"path": "lang/fr.json"
}
]
}
Language Codes
Use ISO 639-1 (2-letter) or ISO 639-2 (3-letter) codes:
en- Englishes- Spanishfr- Frenchde- Germanja- Japanesezh- Chinese
JavaScript API
game.i18n.localize()
Simple string lookup:
const title = game.i18n.localize("MYMODULE.Title");
// Returns: "My Module"
// Missing key returns the key itself
const missing = game.i18n.localize("MYMODULE.Missing");
// Returns: "MYMODULE.Missing"
game.i18n.format()
String interpolation with variables:
{
"MYMODULE.Welcome": "Welcome, {name}!",
"MYMODULE.ItemCount": "You have {count} items",
"MYMODULE.Comparison": "{item1} vs {item2}"
}
game.i18n.format("MYMODULE.Welcome", { name: "Alice" });
// Returns: "Welcome, Alice!"
game.i18n.format("MYMODULE.ItemCount", { count: 5 });
// Returns: "You have 5 items"
game.i18n.format("MYMODULE.Comparison", {
item1: "Sword",
item2: "Axe"
});
// Returns: "Sword vs Axe"
game.i18n.has()
Check if translation exists:
// Check with English fallback
if (game.i18n.has("MYMODULE.Feature")) {
// Key exists (in current language OR English)
}
// Check without fallback
if (game.i18n.has("MYMODULE.Feature", false)) {
// Key exists in current language only
}
game.i18n.getListFormatter()
Format lists according to locale:
const formatter = game.i18n.getListFormatter({
style: "long", // "long", "short", "narrow"
type: "conjunction" // "conjunction", "disjunction"
});
formatter.format(["apples", "oranges", "bananas"]);
// English: "apples, oranges, and bananas"
// Spanish: "manzanas, naranjas y plátanos"
Template Localization
Basic Usage
<h1>{{localize "MYMODULE.Title"}}</h1>
<button>{{localize "MYMODULE.Button.Save"}}</button>
<!-- In attributes, use single quotes -->
<input placeholder="{{localize 'MYMODULE.Placeholder'}}">
<a title="{{localize 'MYMODULE.Tooltip'}}">Hover me</a>
With Variables
{{localize "MYMODULE.Welcome" name=user.name}}
{{localize "MYMODULE.ItemCount" count=items.length}}
Dynamic Keys
{{localize (concat "MYMODULE.Status." statusKey)}}
Pluralization
Foundry has no built-in pluralization. Handle manually:
Separate Keys Approach
{
"MYMODULE.Item.One": "1 item",
"MYMODULE.Item.Many": "{count} items"
}
function localizeCount(count) {
const key = count === 1
? "MYMODULE.Item.One"
: "MYMODULE.Item.Many";
return game.i18n.format(key, { count });
}
Language-Aware Pluralization
// For complex languages, use multiple keys
const key = count === 0 ? "Zero"
: count === 1 ? "One"
: count < 5 ? "Few"
: "Many";
return game.i18n.format(`MYMODULE.Items.${key}`, { count });
Best Practices
Key Naming
{
// Good - specific and hierarchical
"MYSYS.CharacterSheet.Abilities.Strength": "Strength",
"MYSYS.CharacterSheet.Abilities.Dexterity": "Dexterity",
"MYSYS.Dialog.ConfirmDelete.Title": "Confirm Deletion",
"MYSYS.Dialog.ConfirmDelete.Message": "Delete {name}?",
// Bad - vague and conflict-prone
"MYSYS.Label": "Label",
"MYSYS.Title": "Title",
"MYSYS.Button": "Button"
}
Word Order for Translation
{
// Good - translator can reorder
"MYMODULE.Message": "The {adjective} {noun} is here",
// Bad - forces English word order
"MYMODULE.Prefix": "The",
"MYMODULE.Suffix": "is here"
}
Context-Specific Strings
{
// Good - separate by context
"MYSYS.Button.Save.Settings": "Save Settings",
"MYSYS.Ability.Save.Fortitude": "Fortitude Save",
// Bad - ambiguous
"MYSYS.Save": "Save"
}
What to Localize
Do localize:
- UI labels and headings
- Button text
- Dialog titles and messages
- Error/notification messages
- Placeholders and hints
- Document type names
Don't localize:
- User-entered content
- Code identifiers
- Technical paths/URLs
- Data that users create
Common Patterns
Settings Registration
game.settings.register("my-module", "enableFeature", {
name: game.i18n.localize("MYMODULE.Settings.Enable"),
hint: game.i18n.localize("MYMODULE.Settings.EnableHint"),
scope: "world",
type: Boolean,
default: true
});
Dialog with Localization
new Dialog({
title: game.i18n.localize("MYMODULE.Dialog.Title"),
content: game.i18n.format("MYMODULE.Dialog.Content", {
name: item.name
}),
buttons: {
confirm: {
label: game.i18n.localize("MYMODULE.Button.Confirm"),
callback: () => { /* ... */ }
},
cancel: {
label: game.i18n.localize("MYMODULE.Button.Cancel")
}
}
}).render(true);
Notification
ui.notifications.info(
game.i18n.format("MYMODULE.Notification.Created", {
type: item.type,
name: item.name
})
);
Common Pitfalls
1. Concatenating Strings
// WRONG - breaks translation
const msg = game.i18n.localize("MYMOD.The") + " " +
name + " " +
game.i18n.localize("MYMOD.IsReady");
// CORRECT - use format with placeholders
const msg = game.i18n.format("MYMOD.ItemReady", { name });
2. Hardcoded Text
// WRONG
ui.notifications.info("Character saved!");
// CORRECT
ui.notifications.info(game.i18n.localize("MYMOD.CharacterSaved"));
3. Missing Fallback Check
// Be defensive with dynamic keys
const key = `MYMOD.Status.${status}`;
const label = game.i18n.has(key)
? game.i18n.localize(key)
: status; // Fallback to raw value
4. Template Quote Issues
<!-- WRONG - breaks attribute -->
<input title="{{localize "MYMOD.Tip"}}">
<!-- CORRECT - use single quotes inside -->
<input title="{{localize 'MYMOD.Tip'}}">
5. Forgetting Manifest Entry
// Don't forget to register in manifest!
{
"languages": [
{ "lang": "en", "name": "English", "path": "lang/en.json" }
]
}
Directory Structure
my-module/
├── module.json
├── lang/
│ ├── en.json # Required - fallback language
│ ├── es.json
│ ├── fr.json
│ └── de.json
├── templates/
│ └── sheet.hbs
└── scripts/
└── main.js
Implementation Checklist
- Create
lang/en.jsonwith all strings - Register languages in manifest
- Use namespaced keys (MODNAME.Category.Key)
- Use
localize()for simple strings - Use
format()for strings with variables - Use single quotes in template attributes
- Avoid string concatenation
- Provide context-specific keys
- Handle pluralization with separate keys
- Test with different languages
References
Last Updated: 2026-01-05 Status: Production-Ready Maintainer: ImproperSubset