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
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:
Before (hardcoded)
After (i18n-enabled)
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
Name of the plugin (matches the .ts file name)
Translation key (e.g., “status.ready”)
args
Record<string, unknown>
required
Variables for interpolation
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
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
User’s configured locale (e.g., es)
English (en)
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
Always provide English translations
English (en) is the fallback locale. All plugins should include English translations.
Use consistent key naming
Follow the convention of prefixing keys by category: cmd.*, status.*, prompt.*, etc.
Keep translations concise
Status bar messages and command names should be short and clear in all languages.
Test with different locales
Test your plugin with different locale settings to ensure translations display correctly. # Test with Spanish locale
LANG = es_ES.UTF-8 fresh
Use type-safe interpolation
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