Glot
View SourceA minimalistic translation system for Elixir applications
Glot provides a simple translation system for Elixir applications using JSONL glossaries. It supports interpolation, live reloading in development, and semantic lexeme-based translations.
Features
- High Performance: Direct ETS table access for fast lookups
- Multi-locale Support: Easy locale switching with fallback support
- Live Reloading: Automatic translation updates during development
- Interpolation: Dynamic content with
{{variable}}
placeholders - Semantic Keys: Meaning-based lexemes instead of UI position keys
- Module Isolation: Each module gets its own isolated translation table
- Minimal Dependencies: Only requires
jason
andfile_system
Installation
Add glot
to your list of dependencies in mix.exs
:
def deps do
[
{:glot, "~> 0.1.0"}
]
end
Quick Start
1. Create Translation Files
Create JSONL files for each locale and domain:
priv/translations/game.en.jsonl
:
{"key": "game.won", "value": "You won!"}
{"key": "game.lost", "value": "Game over."}
{"key": "game.score", "value": "Your score: {{score}}"}
{"key": "game.greeting", "value": "Hello, {{name}}!"}
priv/translations/game.ru.jsonl
:
{"key": "game.won", "value": "Вы победили!"}
{"key": "game.lost", "value": "Игра окончена."}
{"key": "game.score", "value": "Ваш счёт: {{score}}"}
{"key": "game.greeting", "value": "Привет, {{name}}!"}
2. Use in Your Module
defmodule MyApp.Game do
use Glot,
base: "priv/translations",
sources: ["game"],
default_locale: "en",
watch: true # Enable live reloading in development
def welcome_message(name) do
t("game.greeting", name: name)
end
def score_message(score) do
t("game.score", score: score)
end
def win_message do
t("game.won")
end
end
3. Use Translations
# Basic translation
MyApp.Game.win_message()
# => "You won!"
# With interpolation
MyApp.Game.welcome_message("Alice")
# => "Hello, Alice!"
# Different locale
MyApp.Game.t("game.won", "ru")
# => "Вы победили!"
# With interpolation in different locale
MyApp.Game.t("game.score", "ru", score: 100)
# => "Ваш счёт: 100"
API Reference
Configuration Options
Option | Type | Default | Description |
---|---|---|---|
base | String | Required | Base directory for translation files |
sources | [String] | Required | List of translation source names |
default_locale | String | "en" | Default locale for translations |
watch | Boolean | false | Enable live reloading in development |
Functions
t(key, locale \\ nil, interpolations \\ [])
Translates a lexeme key to the specified locale with optional interpolation.
# Basic translation
t("game.won")
# => "You won!"
# With locale
t("game.won", "ru")
# => "Вы победили!"
# With interpolation
t("game.score", "en", score: 42)
# => "Your score: 42"
# With locale and interpolation
t("game.greeting", "ru", name: "Алиса")
# => "Привет, Алиса!"
reload()
Reloads translations from files. Useful after manual file changes.
MyApp.Game.reload()
# => :ok
has_changes?()
Checks if there are uncommitted changes in the translation table.
MyApp.Game.has_changes?()
# => false
grep_keys(locale, substring)
Searches for translation keys containing a substring in a specific locale.
MyApp.Game.grep_keys("en", "game")
# => [{"en.game.won", "You won!"}, {"en.game.lost", "Game over."}]
loaded_files()
Lists loaded files for this module.
MyApp.Game.loaded_files()
# => [""priv/translations/game.en.jsonl", "priv/translations/game.ru.jsonl"]
Architecture
Core Components
Glot
: Main module providing theuse Glot
macroGlot.Translator
: GenServer managing translation tables and lookupsGlot.Lexicon
: Compiles JSONL files into translation mapsGlot.Watcher
: File system watcher for live reloadingGlot.Jsonl
: JSONL file parser
Performance Features
- ETS Tables: Fast in-memory lookups using Erlang's ETS
- Direct Access: Bypasses GenServer calls for maximum performance
- Lazy Loading: GenServer starts automatically on first use
- Module Isolation: Each module gets its own ETS table
Best Practices
Semantic Lexemes
Use meaningful, semantic keys instead of UI position keys:
# Good - semantic meaning
{"key": "game.won", "value": "You won!"}
{"key": "user.greeting", "value": "Hello, {{name}}!"}
# Bad - UI position
{"key": "label.bottom_right", "value": "Submit"}
{"key": "button.header", "value": "Save"}
File Organization
Group translations by domain in separate files:
priv/translations/
├── game.en.jsonl # Game-related translations
├── game.ru.jsonl
├── user.en.jsonl # User-related translations
├── user.ru.jsonl
├── validation.en.jsonl # Validation messages
└── validation.ru.jsonl
Interpolation
Use {{variable}}
placeholders for dynamic content:
{"key": "user.welcome", "value": "Welcome back, {{name}}!"}
{"key": "game.score", "value": "Score: {{score}} points"}
{"key": "validation.required", "value": "{{field}} is required"}
Development
Live Reloading
Enable live reloading in development by setting watch: true
:
use Glot,
base: "priv/translations",
sources: ["game"],
default_locale: "en",
watch: true # Automatically reloads when files change
Testing
defmodule MyApp.GameTest do
use ExUnit.Case
defmodule TestGame do
use Glot,
base: "test/fixtures",
sources: ["game"],
default_locale: "en"
end
test "translates basic messages" do
assert TestGame.t("game.won") == "You won!"
end
test "handles interpolation" do
assert TestGame.t("game.score", score: 100) == "Score: 100"
end
end
License
This project is licensed under the MIT License - see the LICENSE file for details.
Contributing
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature
) - Commit your changes (
git commit -m 'Add amazing feature'
) - Push to the branch (
git push origin feature/amazing-feature
) - Open a Pull Request
Performance
Glot is designed for efficient translation lookups:
- Memory efficient: Uses ETS tables for fast access
- Direct table access: Bypasses GenServer calls for hot paths
- Lazy initialization: GenServer starts only when needed
Related Projects
- Gettext - Elixir's official internationalization library
- ExI18n - Alternative i18n solution
- Cldr - Unicode CLDR data for Elixir
- glossary - Minimalistic semantic translation system for Elixir apps
Made with ❤️ for the Elixir community