fvtt-localization

star 0

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.

ImproperSubset By ImproperSubset schedule Updated 1/5/2026

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 - 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

Install via CLI
npx skills add https://github.com/ImproperSubset/hh-agentics --skill fvtt-localization
Repository Details
star Stars 0
call_split Forks 0
navigation Branch main
article Path SKILL.md
More from Creator
ImproperSubset
ImproperSubset Explore all skills →