Skip to main content
Plugins can provide translations for their user-facing strings, making them accessible to international users. Fresh provides a simple i18n system based on JSON translation files.

Creating Translation Files

Create a .i18n.json file alongside your plugin with the same base name:
plugins/
  my_plugin.ts
  my_plugin.i18n.json

Translation File Structure

The translation file uses a simple two-level structure: locale → key → translation.
{
  "en": {
    "cmd.do_thing": "My Plugin: Do Thing",
    "cmd.do_thing_desc": "Description of the command",
    "status.ready": "My plugin ready",
    "status.found": "Found %{count} items",
    "prompt.search": "Search:"
  },
  "es": {
    "cmd.do_thing": "Mi Plugin: Hacer Cosa",
    "cmd.do_thing_desc": "Descripción del comando",
    "status.ready": "Mi plugin listo",
    "status.found": "Encontrados %{count} elementos",
    "prompt.search": "Buscar:"
  }
}

Key Conventions

Organize your translation keys using prefixes:

cmd.*

Command names and descriptions

status.*

Status bar messages

prompt.*

Prompt labels

error.*

Error messages

Interpolation

Use %{variable} syntax for variable interpolation:
{
  "en": {
    "status.found": "Found %{count} items in %{time}ms",
    "error.file_not_found": "File not found: %{path}"
  }
}

Using Translations

Translating Status Messages

Use editor.t() to translate status messages:
// Simple message
editor.setStatus(editor.t("status.ready"));

// With interpolation
editor.setStatus(editor.t("status.found", {
  count: String(results.length)
}));
The editor.t() method is available through the pluginTranslate API function.

Translating Command Registration

Use % prefix for command names and descriptions to enable automatic translation:
editor.registerCommand(
  "My Plugin: Search",
  "Search through files",
  "my_search",
  "normal",
  "plugin"
);

Translating Prompt Labels

// Before
editor.startPrompt("Search:", "my-search");

// After
editor.startPrompt(editor.t("prompt.search"), "my-search");

Translation API

pluginTranslate

Translate a string for a plugin using the current locale.
pluginTranslate(
  plugin_name: string,
  key: string,
  args: Record<string, unknown>
): string
plugin_name
string
required
Name of the plugin (matches the .ts file name)
key
string
required
Translation key (e.g., “status.ready”)
args
Record<string, unknown>
required
Variables for interpolation
returns
string
Translated string with variables interpolated
Example:
const message = editor.pluginTranslate(
  "my_plugin",
  "status.found",
  { count: "42", time: "125" }
);
// Result (en): "Found 42 items in 125ms"
// Result (es): "Encontrados 42 elementos en 125ms"

getCurrentLocale

Get the currently active locale.
getCurrentLocale(): string
returns
string
Current locale code (e.g., “en”, “es”, “fr”)
Example:
const locale = editor.getCurrentLocale();
editor.debug(`Current locale: ${locale}`);

Translation Loading

Translations are automatically loaded when your plugin loads. If the user’s locale isn’t available in your translation file, English (en) is used as a fallback.

Fallback Chain

  1. User’s configured locale (e.g., es)
  2. English (en)
  3. Original key string
Example:
// User has locale "fr" (French)
// Plugin only has "en" and "es" translations
// Falls back to "en"

const message = editor.t("status.ready");
// Returns English translation: "My plugin ready"

Complete Example

Plugin File (git_grep.ts)

// Register command with translatable strings
editor.registerCommand(
  "%cmd.git_grep",
  "%cmd.git_grep_desc",
  "git_grep",
  "normal",
  "plugin"
);

// Command implementation
globalThis.git_grep = () => {
  editor.startPrompt(editor.t("prompt.search"), "git-grep");
};

// Handle search
globalThis.handleGitGrep = async (data) => {
  if (data.prompt_type === "git-grep" && data.confirmed) {
    editor.setStatus(editor.t("status.searching"));
    
    const result = await editor.spawnProcess("git", [
      "grep",
      "-n",
      data.value
    ]);
    
    if (result.exit_code === 0) {
      const count = result.stdout.split("\n").length - 1;
      editor.setStatus(editor.t("status.found", {
        count: String(count)
      }));
    } else {
      editor.setStatus(editor.t("status.not_found"));
    }
  }
};

editor.on("prompt_submit", "handleGitGrep");

Translation File (git_grep.i18n.json)

{
  "en": {
    "cmd.git_grep": "Git: Grep",
    "cmd.git_grep_desc": "Search for text in git-tracked files",
    "prompt.search": "Git grep:",
    "status.searching": "Searching...",
    "status.found": "Found %{count} matches",
    "status.not_found": "No matches found"
  },
  "es": {
    "cmd.git_grep": "Git: Buscar",
    "cmd.git_grep_desc": "Buscar texto en archivos rastreados por git",
    "prompt.search": "Buscar en git:",
    "status.searching": "Buscando...",
    "status.found": "Encontrados %{count} coincidencias",
    "status.not_found": "No se encontraron coincidencias"
  },
  "fr": {
    "cmd.git_grep": "Git: Rechercher",
    "cmd.git_grep_desc": "Rechercher du texte dans les fichiers suivis par git",
    "prompt.search": "Recherche git:",
    "status.searching": "Recherche en cours...",
    "status.found": "Trouvé %{count} correspondances",
    "status.not_found": "Aucune correspondance trouvée"
  }
}

Best Practices

English (en) is the fallback locale. All plugins should include English translations.
Follow the convention of prefixing keys by category: cmd.*, status.*, prompt.*, etc.
Status bar messages and command names should be short and clear in all languages.
Test your plugin with different locale settings to ensure translations display correctly.
# Test with Spanish locale
LANG=es_ES.UTF-8 fresh
Always convert numbers to strings for interpolation:
// Good
editor.t("status.found", { count: String(results.length) });

// May cause issues
editor.t("status.found", { count: results.length });

Example Plugins with i18n

See these plugins for complete examples:
  • plugins/git_grep.ts + plugins/git_grep.i18n.json
  • plugins/git_find_file.ts + plugins/git_find_file.i18n.json
  • plugins/git_gutter.ts + plugins/git_gutter.i18n.json

Build docs developers (and LLMs) love