fvtt-localization

ImproperSubset's avatarfrom ImproperSubset

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.

0stars🔀0forks📁View on GitHub🕐Updated Jan 6, 2026

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.
namefvtt-localization
descriptionThis 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 - English
  • es - Spanish
  • fr - French
  • de - German
  • ja - Japanese
  • zh - 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.json with 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