Status: Proof of Concept
A compile-time solution for using Phoenix Gettext translations in Svelte components.
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:
mix gettext.extract
needs to discover translation strings in .svelte
files.po
file references should point to the actual Svelte source file:line for maintainabilityThis library uses Elixir macros at compile time to:
.svelte
files for gettext()
and ngettext()
callsmix gettext.extract
can discover.pot
files via custom extractorNo generated files are committed - everything happens at compile time using @external_resource
for automatic recompilation.
mix gettext.extract
workflow.pot
files show assets/svelte/Button.svelte:42
instead of generated code locationsmix.exs
:# mix.exs
def deps do
[
{:live_svelte_gettext, "~> 0.1.0"}
]
end
mix deps.get
mix igniter.install live_svelte_gettext
The installer will:
SvelteStrings
module with the correct configurationimport LiveSvelteGettext.Components
to your web moduleconfig/config.exs
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 automatically
That's it! You're ready to use translations in your Svelte components - no JavaScript setup required!
If the automatic installer doesn't work for your project:
mix.exs
:def deps do
[
{:live_svelte_gettext, "~> 0.1.0"}
]
end
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
config/config.exs
:# config/config.exs
config :live_svelte_gettext,
gettext: MyAppWeb.Gettext
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
# 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.
Once installed, you can start using translations in your Svelte components immediately.
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:
<script id="svelte-translations">
taggettext()
or ngettext()
gettext()
and ngettext()
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" />
<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.
# 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!
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!
This POC uses a compile-time macro approach to bridge Elixir's gettext and Svelte's runtime:
use LiveSvelteGettext
macro runs and scans all .svelte
filesgettext()
and ngettext()
calls with their file:line locations@external_resource
attributes (triggers recompilation when Svelte files change)CustomExtractor.extract_with_location/8
(preserves accurate source references)all_translations/1
function for runtime accessmix gettext.extract
, it discovers the generated extraction callsCustomExtractor
modifies Macro.Env
to inject the actual Svelte file:line into .pot
files<.svelte_translations />
component fetches translations and renders them as JSON in a <script>
taggettext()
and ngettext()
- interpolation and pluralization happen in the browserEverything is generated at compile time in memory. No intermediate files to commit or maintain.
These are the key design choices made in this POC and the reasoning behind them:
Decision: Pass translations via a <script>
tag with JSON rather than as props to each Svelte component.
Reasoning:
This is a preference based on architectural feel rather than hard performance data.
Decision: Use Elixir macros to generate code at compile time rather than runtime discovery or generated files.
Reasoning:
.ex
or .json
files in version controlmix gettext.extract
@external_resource
triggers recompilation when Svelte files changeThis keeps the developer workflow simple: write gettext()
in Svelte, run mix compile
and mix gettext.extract
.
Decision: Ensure complete compatibility with Phoenix's gettext toolchain, including accurate source references.
Reasoning:
.pot
files showing assets/svelte/Button.svelte:42
helps translators understand contextmix gettext.extract
and .po
file workflowsThe CustomExtractor
was necessary to solve the "all references point to the macro invocation line" problem.
Decision: Create a standalone npm package (live-svelte-gettext
) for the runtime translation functions.
Reasoning:
import { gettext } from 'live-svelte-gettext'
immediatelyThe package will be published to npm for easy installation.
When you run mix compile
:
LiveSvelteGettext.Extractor
scans all .svelte
files in your configured pathgettext()
and ngettext()
calls with file:line metadataLiveSvelteGettext.Compiler
generates:@external_resource
attributes (triggers recompilation when files change)CustomExtractor.extract_with_location/8
(preserves source locations)all_translations/1
function for runtime use__lsg_metadata__/0
debug function↓
When you run mix gettext.extract
:
CustomExtractor
modifies Macro.Env
to inject actual Svelte file:linepriv/gettext/default.pot
with accurate references:#: assets/svelte/components/Button.svelte:42
msgid "Save Profile"
↓
When a page loads:
<.svelte_translations />
component calls YourModule.all_translations(locale)
<script id="svelte-translations">
tag↓
gettext()
or ngettext()
call, translations are automatically loaded from the script taggettext()
and ngettext()
No Phoenix hooks required - everything initializes automatically!
Full API documentation is available on HexDocs.
LiveSvelteGettext
- Main module to use
in your Gettext backendLiveSvelteGettext.Components
- Phoenix components for injecting translationsLiveSvelteGettext.Extractor
- Extracts translation strings from Svelte filesLiveSvelteGettext.Compiler
- Generates code at compile time// 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(): void
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
.
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.json
Translations will automatically initialize on first use - no setup required!
Make sure your SvelteStrings
module is compiling successfully. Check for compilation errors:
mix compile
If 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
}
This usually means:
mix gettext.extract
and mix gettext.merge
yet.po
filesCheck your locale:
Gettext.get_locale(MyAppWeb.Gettext)
Use the appropriate escape sequence:
{gettext("She said, \"Hello\"")} <!-- Double quotes inside double quotes -->
{gettext('He\'s here')} <!-- Single quote inside single quotes -->
Force a recompilation:
mix clean
mix deps.clean live_svelte_gettext
mix deps.get
mix compile
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:
mix live_svelte_gettext.fix_references
to update existing POT filesThe fix_references
task is primarily a fallback tool and shouldn't be needed for normal operation.
Contributions are welcome! Here's how you can help:
mix test
mix format
before committing# 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 dialyzer
# 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.html
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:
mix gettext.extract
.pot
filesKnown limitations:
dgettext
) or contexts (pgettext
)This POC was created in response to live_svelte#120. The goal is to:
If you're interested in using this or have ideas for improvement, please open an issue or discussion!
If this POC proves useful:
Alternative approaches to consider:
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
)
end
This 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!
MIT License - see LICENSE file for details.
Copyright (c) 2025 Christopher Nilsson