LiveSvelteGettext
Status: Proof of Concept
A compile-time solution for using Phoenix Gettext translations in Svelte components.
The Problem
When using live_svelte with Phoenix, there's no straightforward way to use gettext translations in Svelte components. This issue was raised in live_svelte#120.
The challenges:
- Svelte components need access to translations at runtime
mix gettext.extractneeds to discover translation strings in.sveltefiles.pofile references should point to the actual Svelte source file:line for maintainability- Ideally, no generated files to commit or manually maintain
The Solution
This library uses Elixir macros at compile time to:
-
Scan
.sveltefiles forgettext()andngettext()calls -
Generate Elixir code that
mix gettext.extractcan discover -
Inject accurate source references into
.potfiles via custom extractor - Provide runtime translation functions for Svelte via a TypeScript library
No generated files are committed - everything happens at compile time using @external_resource for automatic recompilation.
Key Features
- Compile-Time Extraction: Scans Svelte files during compilation
- Phoenix Gettext Compatible: Works with existing
mix gettext.extractworkflow - Accurate Source References:
.potfiles showassets/svelte/Button.svelte:42instead of generated code locations - Type-Safe Client: TypeScript library for runtime translations
- Simple Setup: Igniter installer handles configuration
- Automatic Initialization: Translations automatically load on first use (no manual setup required)
Installation
Automatic Installation (Recommended)
- Add the dependency to your
mix.exs:
# mix.exs
def deps do
[
{:live_svelte_gettext, "~> 0.1.0"}
]
end- Run the Igniter installer:
mix deps.get
mix igniter.install live_svelte_gettextThe installer will:
- Detect your Gettext backend automatically
- Find your Svelte directory
-
Create a separate
SvelteStringsmodule with the correct configuration -
Add
import LiveSvelteGettext.Componentsto your web module -
Configure
config/config.exs
- (Optional) Install the npm package:
You can either install via npm or use the bundled files from the Hex package:
# Option A: Install from npm (recommended for version management)
npm install live-svelte-gettext
# Option B: Use bundled files (no installation needed)
# The library is available at deps/live_svelte_gettext/assets/dist/
# Your bundler should resolve it automaticallyThat's it! You're ready to use translations in your Svelte components - no JavaScript setup required!
Manual Installation
If the automatic installer doesn't work for your project:
- Add the dependency to your
mix.exs:
def deps do
[
{:live_svelte_gettext, "~> 0.1.0"}
]
end- Create a separate SvelteStrings module (required to avoid circular dependency):
Important: Do NOT add use LiveSvelteGettext to your main Gettext backend module.
This creates a circular dependency that causes compilation errors. Always use a separate module.
# lib/my_app_web/gettext/svelte_strings.ex
defmodule MyAppWeb.Gettext.SvelteStrings do
@moduledoc """
Translation strings extracted from Svelte components.
This module is automatically managed by LiveSvelteGettext.
"""
use Gettext.Backend, otp_app: :my_app
use LiveSvelteGettext,
gettext_backend: MyAppWeb.Gettext,
svelte_path: "assets/svelte"
end- Configure the Gettext module in
config/config.exs:
# config/config.exs
config :live_svelte_gettext,
gettext: MyAppWeb.Gettext- Add the import to your web module (
lib/my_app_web.ex):
def html do
quote do
# ... existing imports ...
import LiveSvelteGettext.Components
end
end
def live_view do
quote do
# ... existing imports ...
import LiveSvelteGettext.Components
end
end- (Optional) Install the npm package:
# Option A: Install from npm
npm install live-svelte-gettext
# Option B: Use bundled files (no installation needed)
# Available at deps/live_svelte_gettext/assets/dist/That's it! Translations automatically initialize on first use.
Quick Start
Once installed, you can start using translations in your Svelte components immediately.
1. Inject translations into your template
Add the <.svelte_translations /> component in your layout or LiveView template. This component renders a <script> tag containing translations as JSON:
<!-- In your layout or LiveView template -->
<.svelte_translations />
<.svelte name="MyComponent" props={%{...}} />
The component renders a <script> tag with translations as JSON. Translations are automatically initialized on first use (lazy initialization).
How it works:
- Component fetches translations for the current locale from your Gettext backend
-
Renders them as JSON in a
<script id="svelte-translations">tag -
Translations are automatically initialized when you first call
gettext()orngettext() -
Your Svelte components can now call
gettext()andngettext()
Advanced usage:
<!-- Override locale -->
<.svelte_translations locale="es" />
<!-- Explicit Gettext module (for multi-tenant apps) -->
<.svelte_translations gettext_module={@tenant.gettext_module} />
<!-- Custom script tag ID -->
<.svelte_translations id="custom-translations" />2. Use translations in your Svelte components
<script>
import { gettext, ngettext } from 'live-svelte-gettext'
let itemCount = 5
</script>
<div>
<h1>{gettext("Welcome to our app")}</h1>
<p>{gettext("Hello, %{name}", { name: "World" })}</p>
<p>{ngettext("1 item", "%{count} items", itemCount)}</p>
</div>That's it! No manual initialization needed - translations are automatically initialized on first use.
3. Extract and translate
# Extract translation strings from both Elixir and Svelte files
mix gettext.extract
# Merge into locale files
mix gettext.merge priv/gettext
# Edit your .po files to add translations
# Then your Svelte components will automatically use the translated strings!Managing Translations
Once you've extracted strings, you need to actually translate them. I've found gettext_ops useful for working with .po files without opening them in an editor:
# See what needs translation
mix gettext_ops.list_untranslated --locale sv --json --limit 10
# Apply translations in bulk
mix gettext_ops.translate --locale sv <<EOF
Welcome to our app = Välkommen till vår app
Hello, %{name} = Hej, %{name}
EOF
This workflow keeps the accurate Svelte source references intact (assets/svelte/Button.svelte:42), which is helpful when you need context for translation.
If you have other tools or workflows that work better, I'd love to hear about them!
How It Works
This POC uses a compile-time macro approach to bridge Elixir's gettext and Svelte's runtime:
Compile Time
- File Scanning: When you compile, the
use LiveSvelteGettextmacro runs and scans all.sveltefiles - String Extraction: Regex patterns extract
gettext()andngettext()calls with their file:line locations - Code Generation: The macro generates Elixir code in your module with:
@external_resourceattributes (triggers recompilation when Svelte files change)-
Calls to
CustomExtractor.extract_with_location/8(preserves accurate source references) -
An
all_translations/1function for runtime access
- Gettext Discovery: When you run
mix gettext.extract, it discovers the generated extraction calls - Accurate References: The
CustomExtractormodifiesMacro.Envto inject the actual Svelte file:line into.potfiles
Runtime
- Server Side: The
<.svelte_translations />component fetches translations and renders them as JSON in a<script>tag - Client Side: Translations are automatically loaded from the script tag on first use (lazy initialization)
- Svelte Components: Call
gettext()andngettext()- interpolation and pluralization happen in the browser
No Generated Files
Everything is generated at compile time in memory. No intermediate files to commit or maintain.
Architectural Decisions
These are the key design choices made in this POC and the reasoning behind them:
1. Script Tag for Translation Injection (Not Props)
Decision: Pass translations via a <script> tag with JSON rather than as props to each Svelte component.
Reasoning:
- Performance: Avoids serializing potentially large translation objects multiple times per page
- Global Access: All Svelte components can access translations without prop drilling
- Separation of Concerns: Translation data is separate from component props
- Caching: The browser can cache the inline script across LiveView updates
This is a preference based on architectural feel rather than hard performance data.
2. Compile-Time Macro Generation
Decision: Use Elixir macros to generate code at compile time rather than runtime discovery or generated files.
Reasoning:
- No Committed Files: Avoids generated
.exor.jsonfiles in version control - Phoenix Integration: Generated code naturally integrates with
mix gettext.extract - Automatic Updates:
@external_resourcetriggers recompilation when Svelte files change - No Runtime Cost: All extraction work happens once at compile time
This keeps the developer workflow simple: write gettext() in Svelte, run mix compile and mix gettext.extract.
3. Full .po File Compatibility
Decision: Ensure complete compatibility with Phoenix's gettext toolchain, including accurate source references.
Reasoning:
- Existing Tools: Developers can use their existing translation workflows
- Reference Accuracy:
.potfiles showingassets/svelte/Button.svelte:42helps translators understand context - CLI Tool Integration: Works seamlessly with gettext_ops for efficient .po file operations and AI-assisted translation.
- No Learning Curve: Developers already know
mix gettext.extractand.pofile workflows
The CustomExtractor was necessary to solve the "all references point to the macro invocation line" problem.
4. NPM Package for TypeScript Client
Decision: Create a standalone npm package (live-svelte-gettext) for the runtime translation functions.
Reasoning:
- Minimal Setup: Developers can
import { gettext } from 'live-svelte-gettext'immediately - Type Safety: Full TypeScript types for better DX
- Reusability: The runtime library could work with other backends in the future
- Familiar Pattern: Follows standard npm package conventions
The package will be published to npm for easy installation.
Architecture
Compile Time (Elixir)
When you run mix compile:
- Scan Svelte files -
LiveSvelteGettext.Extractorscans all.sveltefiles in your configured path - Extract strings - Regex patterns find
gettext()andngettext()calls with file:line metadata - Generate code -
LiveSvelteGettext.Compilergenerates:@external_resourceattributes (triggers recompilation when files change)-
Calls to
CustomExtractor.extract_with_location/8(preserves source locations) -
An
all_translations/1function for runtime use -
A
__lsg_metadata__/0debug function
↓
Translation Extraction
When you run mix gettext.extract:
- Discover strings - Gettext finds the generated extraction calls
- Inject references -
CustomExtractormodifiesMacro.Envto inject actual Svelte file:line - Write POT files - Creates/updates
priv/gettext/default.potwith accurate references:#: assets/svelte/components/Button.svelte:42 msgid "Save Profile"
↓
Runtime (Server)
When a page loads:
- Fetch translations - The
<.svelte_translations />component callsYourModule.all_translations(locale) - Render JSON - Translations are rendered in a
<script id="svelte-translations">tag
↓
Runtime (Client/Browser)
- Lazy initialization - On first
gettext()orngettext()call, translations are automatically loaded from the script tag - Use translations - Svelte components call
gettext()andngettext() - Interpolate - The TypeScript library handles variable substitution and pluralization
No Phoenix hooks required - everything initializes automatically!
API Documentation
Full API documentation is available on HexDocs.
Key Modules
LiveSvelteGettext- Main module tousein your Gettext backendLiveSvelteGettext.Components- Phoenix components for injecting translationsLiveSvelteGettext.Extractor- Extracts translation strings from Svelte filesLiveSvelteGettext.Compiler- Generates code at compile time
TypeScript API
// Get translated string
gettext(key: string, vars?: Record<string, string | number>): string
// Get translated string with pluralization
ngettext(singular: string, plural: string, count: number, vars?: Record<string, string | number>): string
// Initialize translations manually (optional - automatically happens on first use)
initTranslations(translations: Record<string, string>): void
// Check if initialized
isInitialized(): boolean
// Reset (useful for testing)
resetTranslations(): voidTroubleshooting
Translations not updating after changing Svelte files
Make sure your Svelte files are being watched for changes. Run:
mix clean
mix compile
The module should recompile automatically when Svelte files change due to @external_resource.
Import errors
If you get import errors for live-svelte-gettext, you have two options:
# Option 1: Install via npm
npm install live-svelte-gettext
# Option 2: Use bundled files from Hex package
# Ensure the dependency is fetched
mix deps.get
# The library is available at deps/live_svelte_gettext/assets/
# Your bundler should resolve it automatically based on package.jsonTranslations will automatically initialize on first use - no setup required!
Gettext.extract not finding Svelte strings
Make sure your SvelteStrings module is compiling successfully. Check for compilation errors:
mix compileIf there are no errors, verify that strings are being extracted:
# In IEx
iex> MyAppWeb.SvelteStrings.__lsg_metadata__()
%{
extractions: [...], # Should list your strings
svelte_files: [...], # Should list your .svelte files
gettext_backend: MyAppWeb.Gettext
}Translations showing keys instead of translated text
This usually means:
-
You haven't run
mix gettext.extractandmix gettext.mergeyet -
The translations haven't been added to your
.pofiles - The locale isn't set correctly
Check your locale:
Gettext.get_locale(MyAppWeb.Gettext)Escaped quotes not working in Svelte
Use the appropriate escape sequence:
<!-- Double quotes inside double quotes -->
<!-- Single quote inside single quotes -->
Module not recompiling when expected
Force a recompilation:
mix clean
mix deps.clean live_svelte_gettext
mix deps.get
mix compilePOT files showing incorrect Svelte file references
As of v0.1.0, LiveSvelteGettext automatically injects correct Svelte file:line references during mix gettext.extract via CustomExtractor. You should see references like:
#: assets/svelte/components/Button.svelte:42
msgid "Save Profile"
If you see incorrect references (like lib/my_app_web/svelte_strings.ex:39 for all strings), this usually means:
- Migration from older version: Run
mix live_svelte_gettext.fix_referencesto update existing POT files - CustomExtractor not working: This is likely a bug - please report it!
The fix_references task is primarily a fallback tool and shouldn't be needed for normal operation.
Contributing
Contributions are welcome! Here's how you can help:
- Report bugs: Open an issue with a minimal reproduction case
- Suggest features: Open an issue describing the use case and proposed API
- Submit pull requests:
- Fork the repository
- Create a feature branch
- Add tests for new functionality
-
Ensure all tests pass with
mix test -
Run
mix formatbefore committing - Open a PR with a clear description
Development Setup
# Clone the repository
git clone https://github.com/xnilsson/live_svelte_gettext.git
cd live_svelte_gettext
# Install dependencies
mix deps.get
# Run tests
mix test
# Run tests with coverage
mix coveralls.html
# Format code
mix format
# Type checking
mix dialyzerRunning Tests
# Run all tests
mix test
# Run specific test file
mix test test/live_svelte_gettext/extractor_test.exs
# Run with coverage
mix coveralls.html
open cover/excoveralls.htmlProject Status & Future
Current Status
This is a proof of concept extracted from a real project where it solves a practical need. It works well for the use case it was designed for, but has not been widely tested across different Phoenix/Svelte setups.
What's working:
- Compile-time extraction from Svelte files
-
Integration with
mix gettext.extract -
Accurate source references in
.potfiles - Runtime translations with interpolation and pluralization
- Automatic lazy initialization (no manual setup required)
- Igniter-based installation
Known limitations:
- Simple English plural rules only (no CLDR plural forms for other languages)
- Regex-based extraction (won't handle all edge cases like template literals or computed strings)
-
Not tested with domains (
dgettext) or contexts (pgettext)
Sharing with live_svelte Community
This POC was created in response to live_svelte#120. The goal is to:
- Share the approach - Show that compile-time macro extraction can work
- Get feedback - Learn if this solves the problem for others
- Discuss integration - Potentially merge concepts into live_svelte or keep as separate library
If you're interested in using this or have ideas for improvement, please open an issue or discussion!
Possible Future Directions
If this POC proves useful:
- CLDR plural rules for accurate pluralization across languages
- Domain and context support (dgettext, pgettext)
- More robust parsing (proper Svelte AST instead of regex)
- Support for other frontend frameworks (React, Vue, etc.)
Alternative approaches to consider:
- Babel/SWC plugin for extraction (more accurate than regex)
- Build-time JSON generation (simpler but requires committing files)
- Integration directly into live_svelte (would benefit all users)
For Library Authors
If you're building a compile-time i18n extractor for a non-Elixir templating system (like Svelte, Surface, Temple, etc.), you may encounter the same challenge we faced: all extracted translation strings reference the macro invocation line instead of the original source file locations.
The Problem:
# lib/my_app_web/template_strings.ex:39
use MyI18nExtractor # <-- All strings reference this line
# In POT file:
#: lib/my_app_web/template_strings.ex:39
msgid "Save Profile"
#: lib/my_app_web/template_strings.ex:39
msgid "Delete Account"Our Solution:
We solved this by creating a custom extractor that modifies Macro.Env before calling Gettext.Extractor.extract/6. See lib/live_svelte_gettext/custom_extractor.ex for the implementation.
The key insight:
def extract_with_location(env, backend, domain, msgctxt, msgid, extracted_comments, file, line) do
# Create a modified environment with custom file and line
modified_env = %{env | file: file, line: line}
# Gettext reads env.file and env.line
Gettext.Extractor.extract(
modified_env,
backend,
domain,
msgctxt,
msgid,
extracted_comments
)
endThis produces accurate references in POT files:
#: assets/svelte/components/Button.svelte:42
msgid "Save Profile"
#: assets/templates/settings.sface:18
msgid "Delete Account"Feel free to copy this pattern for your own compile-time extraction needs!
License
MIT License - see LICENSE file for details.
Copyright (c) 2025 Christopher Nilsson