Translations
Pixelflare supports multiple languages, whereby users can view and interact with the app in their preferred locale. All translations live in the i18n package. We're using Paraglide JS for managing and integrating into the client app.
Updating translations
- Make any additions / changes in the JSON files in the
messagesdirectory - Run the check scripts (see below) to ensure everything is kosher
- For the changes to take effect, Paraglide needs to compile the output into usable code
- This happens automatically during the app's build process
- But you can also run
npm run buildmanually with paraglide cli
Adding new languages
- Create a new JSON file in the
messagesdirectory named the lang code - Then, in
languages.ts, add a new entry to thelanguagesarray with
- A code (matching the filename)
- The English name
- The local name
- A flag emoji
- And the country code
Scripts
There's a couple of scripts (which live in i18n/scripts) that help with common checks and tests.
check:hardcoded- Scans the codebase for hardcoded strings that should be extractedcheck:invalid-keys- Checks frontend isn't using any invalid or non-existent i18n keyscheck:unused-keys- Identifies i18n keys defined here, but not used anywhere in the appcheck:missing-translations- Detects English values, not yet translated in other localescheck:stats- Prints a summary of all checks and stats about the translation progress
You can run any of these scripts, by (first cloning the repo, and npm iing the deps).
Then navigate into the i18n package, and run any script with npm run <script-name>.
cd packages/i18n
npm run checkFrontend Usage
Using translations in code
Step 1: Import the language util
import { t, m } from '$lib/utils/language';Where t is the function for translating strings, and the m object which contains all translation keys
Step 2: Render some translated text in the markup
<p>{t(m.image_encryption)}</p>This is done by passing your desired key m.whatever to the t function which will return the translated string for the current language
How it works in the app
We have a Svelte store, in src/lib/stores/language.svelte.ts, which keeps track of the current language/locale. Then some utils in language.ts which takes care of all the logic (detecting browser lang, reading/writing to storage, etc).
Language Switching Logic
export function switchLanguage(code: Locale): void {
// 1. Check language exists and is supported
if (!isLanguageSupported(code)) {
logger.warn('Unsupported language code', { code });
return;
}
// 2. Save, update and apply new language
setLocale(code, { reload: false });
saveLanguage(code);
updateHtmlLang(code);
languageStore.setCurrentLocale(code);
}setLocalecalls to paraglide runtime, to get new datasaveLanguagestores chosen lang code to localstorage so it persists after reloadupdateHtmlLangadds<html lang="xx">to document, for accessibility & SEOlanguageStore.setCurrentLocaleupdates the Svelte store so all pages get correct lang
Advanced usage in code
Dynamic Parameters
You can pass variables into translations to interpolate dynamic content.
Svelte component markup:
<span>{t(m.welcome_message, { name: user.name, count: 5 })}</span>Translation (en.json) content:
"welcome_message": "Wassup {name}! You got {count} new messages"Output:
<span>Wassup Alicia! You got 67 new messages</span>Pluralization
Not yet supported (todo!). As a workaround, you can have 2 translation keys for singular and plural forms.
Dynamic Keys
Access translation keys programmatically, with type safety:
{t(m[keyName as keyof typeof m] as MessageFunction)}Type assertion required for TypeScript safety:
m[key as keyof typeof m] as (
inputs?: Record<string, unknown>,
options?: Record<string, unknown>
) => string;Conditional Content
Different translations based on conditions (just use normal Svelte/JS logic)
{isAdmin ? t(m.admin_welcome) : t(m.user_welcome)}HTML Content
Very not recommended. But if you really need to, then you can use {@html} (for trusted content only):
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
{@html t(m.formatted_message, {
link: `<a href="/help">${t(m.help_link)}</a>`,
})}Standards
Naming Conventions
- Language codes should be BCP 47 language tag
- e.g.
fr.jsonfor French
- e.g.
- Translation keys should start with the component/page, then a descriptive name
- e.g.
favourites_emptyTitle
- e.g.
- Keys should begin with lowercase, and use camelCase for multi-word keys
- e.g.,
search_noResultsMessage
- e.g.,
Ensure all checks pass (before you commit)
Translation checks are included in the project's overall check process, and therefore must pass before new code can be merged or deployed
To check that everything is all good, and passing, run npm run check and ensure you don't get a zero exit code (echo $? should return 0) This will also show a summary of any issues, and next steps to fix them
General rules
- Never create a translation key which doesn't yet exist in en.json
- Every key added into translation files (en.json) must be used in the code
- Never reference a key in the code which doesn't have a corresponding translation in (at least) en.json
- Be careful to never add duplicate keys, or different translations with the same key, it will break stuff
- Always follow the correct naming conventions (as above)
- Organise en.json and other files logically, grouped by component/page
- Keep translations up to date, and in sync across all languages
- Frequently used strings can be common, and shared
- Use the checking scripts (as listed above) to help you
- Pls don't put anything silly, because I can't read other languages to check 😉
Scope
Currently, only the frontend app is translated/being translated.
The docs, API messaging, and source stuff is still English-only.
Architecture
Translation Finding Priority
When a component requests a translation, the system follows this priority chain:
- Check if translation exists for current language
- If yes, use it ✓
- ☹️ Otherwise, check if translation exists for English (base locale)
- If yes, use it ✓
- ☹️☹️ Otherwise, check if fallback string was provided in code
- If yes, use it ✓
- ☹️☹️☹️ Otherwise, there's a missing translation
- Show a placeholder (the translation key)
- Log a warning to console and report to error handler
flowchart LR
A[Request Translation] --> B{Exists in<br/>Current Locale?}
B -->|Yes| C[✓ Return Translation]
B -->|No| D{Exists in<br/>English?}
D -->|Yes| E[✓ Return English<br/>Fallback]
D -->|No| F{Code Fallback<br/>Provided?}
F -->|Yes| G[✓ Return Code<br/>Fallback]
F -->|No| H[⚠️ Missing Translation]
H --> I[Show Key as Placeholder]
H --> J[Log Warning]
style C fill:#bbffbb33,stroke:#333,stroke-width:2px
style E fill:#ffffdd33,stroke:#333,stroke-width:2px
style G fill:#ffd89b33,stroke:#333,stroke-width:2px
style H fill:#ff9b9b33,stroke:#333,stroke-width:2pxLanguage Detection Priority
- Check user's saved preference (localstorage, and database)
- If language saved, and valid - use their saved language
- Otherwise, detect browser language
- If found a lang, check it's valid - and use the browser language that
- Otherwise, fall back to English (default)
flowchart TD
A[App Initializes] --> B{Check localStorage}
B -->|Found & Valid| C[Use Saved Language]
B -->|Not Found/Invalid| D{Detect Browser Language}
D -->|Supported| E[Use Browser Language]
D -->|Not Supported| F[Use Default: English]
C --> G[setLocale()]
E --> H[Save to localStorage]
H --> G
F --> G
G --> I[Update HTML lang attribute]
I --> J[Update Language Store]
J --> K[Trigger UI Re-render]
L[User Changes Language] --> M[switchLanguage(code)]
M --> H
style B fill:#ffffdd33,stroke:#333,stroke-width:2px
style D fill:#ffffdd33,stroke:#333,stroke-width:2px
style G fill:#ff99ff33,stroke:#333,stroke-width:2px
style K fill:#bbffbb33,stroke:#333,stroke-width:2pxOverall System Architecture
graph TB
subgraph "i18n Package"
A[messages/en.json] --> B[Paraglide Compiler]
C[messages/es.json] --> B
D[messages/fr.json] --> B
E[messages/...] --> B
F[project.inlang/settings.json] --> B
B --> G[src/paraglide/messages/]
G --> H[messages.js exports]
B --> I[src/paraglide/runtime.js]
J[src/languages.ts] --> K[Language metadata]
end
subgraph "Frontend Package"
L[lib/stores/language.svelte.ts] --> M[Language Store]
H --> M
I --> M
K --> M
M --> N[lib/utils/language.ts]
N --> O[Components]
O --> P[t & m functions]
P --> Q[Rendered UI]
end
R[User] --> S[Language Selection]
S --> M
M --> T[localStorage]
style B fill:#ff99ff33,stroke:#333,stroke-width:2px
style M fill:#bbbbff33,stroke:#333,stroke-width:2px
style Q fill:#bbffbb33,stroke:#333,stroke-width:2pxKey Characteristics
- Type Safety: Paraglide generates TypeScript types for all translation keys, providing autocomplete and compile-time validation
- Tree Shaking: Only translations actually used in the build are included in the final bundle
- No Runtime Parsing: Translations are pre-compiled to JavaScript functions, eliminating runtime parsing overhead
- Reactive: Language changes automatically trigger UI updates through Svelte's reactivity system
- Fallback Chain: localStorage → browser language → English (default)
- Parameter Support: Messages can include dynamic values using
{paramName}syntax
Progress so far
I believe that everyone should be able to freely access software, without language ever being a barrier. So, I'm committed to translating Pixelflare into as many languages as possible.
But... that's where I hit a small issue - as an uneducated Brit, I only speak English (and barley that).
So far, I've extracted all user-facing text into translation files for English, and then for the other languages (work in progress), I've used Google Translate API, to generate initial translations, and some AI to verify they make sense in context.
So, if you speak another language, and are up for helping us out, we'd very very much appreciate it!