Melhores Práticas para Paraglide JS + Svelte 5
Trate o locale como estado de requisição/build resolvido por um compilador, e não como estado reativo da aplicação — deixe a navegação de documento completo do Paraglide cuidar das trocas de locale e deixe os runes do Svelte 5 cuidarem de todo o resto. Paraglide JS é a integração i18n oficial do SvelteKit: ele compila messages/{locale}.json em funções ESM tree-shakable e totalmente tipadas, resultando em bundles i18n até ~70% menores do que bibliotecas em tempo de execução (≈47 KB vs ≈205 KB no benchmark publicado com 5 locales / 200 mensagens), sem waterfalls assíncronos e sem a necessidade de um provedor i18n nos componentes Svelte 5.
1. Tabela de Decisão de Referência Rápida
| Decisão | Padrão recomendado | Por quê |
|---|---|---|
| Caminho de instalação | npx sv add paraglide="languageTags:en,de" em um novo app SvelteKit | Configura o plugin Vite, hooks, atributos app.html e .gitignore em uma única etapa |
| Pacote | @inlang/paraglide-js v2.x | O adaptador v1 @inlang/paraglide-js-adapter-sveltekit está obsoleto — use o pacote principal diretamente |
| Estratégia (SSR + URLs localizadas) | ["url", "cookie", "baseLocale"] | URL é a fonte da verdade para SEO; cookie persiste a escolha do usuário visível ao servidor; baseLocale sempre resolve por último |
| Estratégia (adicionar substituição armazenada) | ["localStorage", "cookie", "preferredLanguage", "url", "baseLocale"] | localStorage sozinho é invisível para a primeira requisição SSR; cookie faz a ponte servidor/cliente |
| Comportamento da troca de locale | Padrão setLocale(locale) (navegação de documento completo) | A reatividade do framework não re-renderiza <html lang>, dir, <title> ou URLs localizadas; a navegação sim |
| AsyncLocalStorage | Mantenha ativado (padrão) | Vercel Edge e Cloudflare Workers (compatibilidade com Node) suportam; desativar corre o risco de vazar locale entre requisições concorrentes |
| Declarações TS | emitTsDeclarations: true (TypeScript ≥ 5.6) | Mantém os servidores de linguagem Svelte + TS sincronizados quando as chaves mudam durante o desenvolvimento |
| Chaves de mensagem | Chaves aleatórias planas, estáveis e legíveis (calm_green_otter) | Recomendação oficial; chaves aninhadas funcionam, mas chaves planas são preferidas |
| Pré-renderização | export const prerender = true + entries() por locale | SSG precisa que cada locale resolva para um arquivo de URL distinto |
| Reatividade para locale | Nenhuma nos componentes por padrão | getLocale() tem escopo de requisição; um novo documento é renderizado por locale, então os runes não precisam rastreá-lo |
2. Configuração (Paraglide 2.x + SvelteKit + Svelte 5)
2.1 Caminho mais rápido
npx sv create my-app
npx sv add paraglide="languageTags:en,de,demo:yes"
O complemento da CLI do Svelte configura o projeto inlang, o plugin Vite, os hooks handle/reroute do SvelteKit, %lang%/%dir% em app.html e uma página de demonstração opcional.
2.2 Configuração manual (boilerplate completo)
Instale e crie o projeto, então conecte quatro arquivos:
vite.config.ts — plugin com estratégia explícita:
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';
import { paraglideVitePlugin } from '@inlang/paraglide-js';
export default defineConfig({
plugins: [
sveltekit(),
paraglideVitePlugin({
project: './project.inlang',
outdir: './src/lib/paraglide',
emitTsDeclarations: true,
strategy: ['url', 'cookie', 'baseLocale'],
}),
],
});
emitTsDeclarations: true regenera os .d.ts conforme você edita as mensagens para que os servidores Svelte e TypeScript permaneçam sincronizados; requer TypeScript 5.6+ (defina false para o caminho mais rápido de inferência JSDoc).
src/app.html — substitua o lang codificado:
<!doctype html>
<html lang="%lang%" dir="%dir%">
src/hooks.server.ts — middleware do servidor com locale de escopo de requisição:
import type { Handle } from '@sveltejs/kit';
import { paraglideMiddleware } from '$lib/paraglide/server';
import { getTextDirection } from '$lib/paraglide/runtime';
const paraglideHandle: Handle = ({ event, resolve }) =>
paraglideMiddleware(event.request, ({ request: localizedRequest, locale }) => {
event.request = localizedRequest;
return resolve(event, {
transformPageChunk: ({ html }) =>
html.replace('%lang%', locale).replace('%dir%', getTextDirection(locale)),
});
});
export const handle: Handle = paraglideHandle;
O middleware detecta o locale (estratégias em ordem), redireciona requisições de documento quando a URL discorda do locale detectado, deslocaliza /de/about → /about antes do SvelteKit vê-lo e envolve a requisição em AsyncLocalStorage para que getLocale() retorne o locale correto por requisição concorrente.
src/hooks.ts — limite de roteamento (deve ser hooks.ts, não hooks.server.ts):
import type { Reroute } from '@sveltejs/kit';
import { deLocalizeUrl } from '$lib/paraglide/runtime';
export const reroute: Reroute = (request) => deLocalizeUrl(request.url).pathname;
// project.inlang/paraglide.config.ts
import { defineConfig } from '@inlang/paraglide-js';
export default defineConfig({
outdir: './src/lib/paraglide',
strategy: ['url', 'cookie', 'baseLocale'],
urlPatterns: [
{ pattern: '/', localized: [['en', '/']] },
{
pattern: '/:path(.*)?',
localized: [
['en', '/en/:path(.*)?'],
['de', '/de/:path(.*)?'],
],
},
],
});
Precedência: flags CLI / opções do plugin > arquivo de configuração > padrões embutidos. O modo de observação recarrega a configuração a quente.
3. Padrões de Uso Principais (Mensagens, Locale, Roteamento)
3.1 Mensagens
import { m } from '$lib/paraglide/messages';
import type { LocalizedString } from '$lib/paraglide/runtime';
m.hello_world(); // "Olá Mundo!"
m.greeting({ name: 'Ada' }); // "Olá Ada!" — parâmetros são tipados
m.greeting({ name: 'Ada' }, { locale: 'de' }); // força locale para uma chamada
const title: LocalizedString = m.welcome_title(); // tipo de string seguro
As mensagens ficam em messages/{locale}.json; os locales são declarados em project.inlang/settings.json (baseLocale + locales). Prefira chaves planas e estáveis; chaves aninhadas são suportadas via m['user.profile.title'](). O acesso dinâmico a chaves prejudica o tree-shaking — mapeie IDs dinâmicos para referências de mensagem explícitas.
3.2 Obtendo e definindo o locale
import { getLocale, locales, setLocale, getTextDirection } from '$lib/paraglide/runtime';
getLocale(); // ex. "de" — escopo de requisição no servidor
getTextDirection(); // "ltr" | "rtl" para o locale atual
setLocale('de'); // navegação de documento completo para a URL localizada
setLocale() atualiza suas estratégias configuradas e então realiza uma navegação de documento completo — para a URL localizada quando a estratégia url está ativa, caso contrário um recarregamento. Isso é intencional: <html lang>, dir, <title>, metadados e a URL localizada são estado de nível de documento que a reatividade do framework não pode atualizar.
A escotilha de escape setLocale(locale, { reload: false }) existe apenas para superfícies totalmente renderizadas no cliente (widgets embutidos, páginas de extensão) que devem preservar estado de memória não restaurável. Nunca a use para seletores de locale comuns, em documentos SSR/SSG ou em rotas cuja estratégia inclua url — ela deixa a URL antiga e o shell no lugar, então getLocale() continua resolvendo o locale antigo.
3.3 URLs localizadas e links
import { localizeHref, deLocalizeUrl, localizeUrl } from '$lib/paraglide/runtime';
localizeHref('/blog'); // "/de/blog" para o locale atual
localizeHref('/blog', { locale: 'de' }); // explícito
localizeUrl('https://example.com/about', { locale: 'de' }).href; // forma de URL completa
Os padrões de URL usam a sintaxe URLPattern padrão; a ordem importa — coloque padrões específicos antes de curingas e teste-os em urlpattern.com.
3.4 Seletor de locale do Svelte 5 (idiomático)
<script lang="ts">
import { page } from '$app/state'; // Svelte 5: objeto, não um store
import { resolve } from '$app/paths';
import { locales, localizeHref } from '$lib/paraglide/runtime';
</script>
<nav aria-label="Idiomas">
{#each locales as locale (locale)}
<a
href={resolve(localizeHref(page.url.pathname, { locale }))}
data-sveltekit-reload
aria-current={page.params.locale === locale ? 'page' : undefined}
>
{locale}
</a>
{/each}
</nav>
Dois detalhes do Svelte 5 importam aqui: $app/state expõe page como um objeto reativo (o store $page e $app/stores são o padrão legado), e data-sveltekit-reload em links entre locales mantém a URL localizada, o documento renderizado no servidor e o locale do tempo de execução sincronizados — também necessário para crawling pré-renderizado.
4. Rich Text com Snippets do Svelte 5 (ParaglideMessage)
A marcação permite que os tradutores controlem a estrutura enquanto seu aplicativo controla a renderização. Os nomes das tags são definidos pelo aplicativo — nada mapeia para HTML automaticamente.
// messages/en.json
{
"cta": "{#link to=|/docs|}Leia a documentação{/link}",
"welcome": "{#b}Olá {name}{/b}{#icon/}"
}
<script lang="ts">
import { ParaglideMessage } from '@inlang/paraglide-js-svelte';
import { m } from '$lib/paraglide/messages';
const name = $props().name; // ou qualquer $state
</script>
<ParaglideMessage message={m.welcome} inputs={{ name }}>
{#snippet b({ children })}
<strong>{@render children?.()}</strong>
{/snippet}
{#snippet icon({ attributes })}
<Icon variant={attributes?.variant} />
{/snippet}
</ParaglideMessage>
<ParaglideMessage> usa message.parts() quando a marcação existe e recorre a message() para texto simples; a sintaxe {#snippet tag({ children, options, attributes })} é exatamente o padrão de snippet do Svelte 5 — sem slots, sem legado <slot>. Observe a sintaxe literal to=|/docs| (delimitada por pipe) e que m.welcome() sozinho retorna texto simples sem marcação.
5. Integração com Runes do Svelte 5: O Que É Reativo (e O Que Não É)
O modelo mental principal: o Paraglide é dono do locale como estado de documento/requisição; os runes do Svelte 5 são donos do estado da UI. Como as mudanças de locale enviam um novo documento, raramente você precisa de runes para rastrear getLocale(). Padrões que surgem na prática:
Valores derivados dependentes de locale dentro de um único documento (por exemplo, uma superfície somente cliente usando reload: false, ou um painel de pré-visualização renderizando vários locales explicitamente):
<script lang="ts">
import { getLocale, locales } from '$lib/paraglide/runtime';
import { m } from '$lib/paraglide/messages';
let { locale = getLocale() } = $props(); // props tipadas
// $derived reavalia quando a prop `locale` muda — não quando
// o locale interno do Paraglide muda (isso não acontece, sem navegação):
const greeting = $derived(m.greeting({ name: 'Ada' }, { locale }));
const isRtl = $derived(getTextDirection(locale) === 'rtl');
</script>
Passar o locale forçado para baixo em vez de ler o estado global é o padrão amigável a runes: as opções { locale } em cada chamada de mensagem tornam os componentes funções puras das props, que é exatamente como o Svelte 5 quer que o estado flua. Reserve $effect para efeitos colaterais verdadeiros (por exemplo, persistir um locale de pré-visualização) — nunca atribua a um $derived e não escreva $state de dentro de $effect para "empurrar" o locale; passe props.
Estado global do cliente (por exemplo, "último locale pré-visualizado" para uma ferramenta de tradução no aplicativo):
// src/lib/locale-preview.svelte.ts — arquivos .svelte.ts suportam runes no escopo do módulo
export const preview = $state({ locale: 'en' as string | undefined });
$state no nível do módulo em arquivos .svelte.ts é compartilhado entre todos os importadores — bom para singletons do aplicativo inteiro, mas lembre-se de que não é como o locale de produção funciona (isso é a cadeia de estratégia + navegação de documento).
6. SSR, Hidratação e SSG
6.1 Regras do SSR
- Uma ordem da cadeia
handle: coloqueparaglideHandleprimeiro (ou antes de manipuladores de autenticação que precisam deevent.localschaveado por locale) para que os hooks downstream vejam a requisição deslocalizada. - Atrás de um proxy terminador de TLS, passe
effectiveRequestUrlpara que a correspondência de URL e os redirecionamentos usem a origem voltada para o navegador. - Mantenha
AsyncLocalStorageativado; é ele que tornagetLocale()seguro sob requisições concorrentes e streaming SSR. O fallbackdisableAsyncLocalStorageé apenas para runtimes semnode:async_hookse isolamento de requisição garantido — usá-lo em outros lugares pode vazar locale entre usuários.
6.2 Correção da hidratação
Servidor e cliente devem resolver o locale a partir da mesma ordem de estratégia. A incompatibilidade clássica: o servidor resolve da URL, mas a estratégia do cliente lê localStorage primeiro, que não existe no tempo do SSR. Corrija incluindo uma estratégia cookie visível ao servidor quando uma escolha persistida deve moldar a primeira resposta.
6.3 SSG (adapter-static / prerender)
A pré-renderização nunca executa o middleware — defina o locale por página durante a construção:
// src/routes/[locale]/+layout.ts
import { setLocale, assertIsLocale } from '$lib/paraglide/runtime';
import type { LayoutLoad } from './$types';
export const prerender = true;
export function load({ params }: { params: { locale: string } }) {
setLocale(assertIsLocale(params.locale));
}
// src/routes/[locale]/+page.ts
import { locales } from '$lib/paraglide/runtime';
export const prerender = true;
export function entries() {
return locales.map((locale) => ({ locale }));
}
O SSG requer que cada locale tenha um prefixo de URL (configure urlPatterns para que /en/… e /de/… existam), caso contrário o SvelteKit não pode emitir arquivos distintos por locale. Para o fallback SPA do adaptador estático, defina kit.paths.relative = false para evitar erros 404 de ativos com prefixo de locale e use generateStaticLocalizedUrls() para sitemaps.
7. Performance e Tree-Shaking
- Chame as funções de mensagem diretamente.
m.hello()compila para uma pequena função ESM; seu bundler descarta automaticamente mensagens não utilizadas e suas traduções. O tamanho do bundle permanece estável à medida que o catálogo cresce (benchmark: 47 KB quer o projeto tenha 200 ou 1.000 mensagens). - Use chaves planas em vez de aninhadas. Objetos aninhados (
m.user.profile.title()) são suportados, mas impedem que o compilador elimine subárvores não utilizadas; chaves planas permitem tree-shaking total. - Evite acesso dinâmico a chaves.
m[key]()impede o tree-shaking completamente — toda a mensagem é retida. Em vez disso, mapeie IDs dinâmicos para referências estáticas de mensagem. - O padrão de estratégia padrão usa apenas
url. Se você tem apenas um locale, nenhuma estratégia é necessária — Paraglide usabaseLocale.
8. Padrões de Teste
8.1 Testes unitários de mensagens
import { describe, it, expect } from 'vitest';
import { m } from '$lib/paraglide/messages';
import { setLocale } from '$lib/paraglide/runtime';
describe('saudações', () => {
it('saúda em inglês', () => {
setLocale('en', { reload: false });
expect(m.hello()).toBe('Hello!');
});
it('saúda em alemão', () => {
setLocale('de', { reload: false });
expect(m.hello()).toBe('Hallo!');
});
});
8.2 Testes de integração do SvelteKit (Playwright)
import { expect, test } from '@playwright/test';
test('troca de locale via link', async ({ page }) => {
await page.goto('/');
await page.click('a[href="/de"]');
await expect(page.locator('html')).toHaveAttribute('lang', 'de');
});
9. Solução de Problemas
| Sintoma | Causa provável | Correção |
|---|---|---|
Erro getLocale() retorna locale errado no servidor | AsyncLocalStorage desativado ou substituído | Verifique se disableAsyncLocalStorage não está definido; coloque paraglideHandle primeiro na cadeia de hooks |
| Hidratação com conteúdo piscando (locale incorreto no carregamento) | Estratégia servidor/cliente incompatível | Inclua cookie na estratégia; certifique-se de que ele seja definido no SSR |
| Mensagens não encontradas em desenvolvimento | Servidor de linguagem TS desatualizado | Ative emitTsDeclarations: true e reinicie o servidor de linguagem |
| Sitemap SSG contém apenas um locale | entries() ausente ou urlPatterns cobre apenas um locale | Adicione entries() à página [locale] e garanta que cada locale tenha um padrão de URL |
setLocale({ reload: false }) não atualiza a UI | Navegação nunca acontece; estado antigo permanece | Use setLocale(locale) sem opções, ou force a re-renderização manualmente em superfícies somente cliente |
10. Estratégias Avançadas
10.1 Negociação de locale do servidor (cabeçalho Accept-Language)
// Em hooks.server.ts
import { detectLocaleFromAcceptLanguageHeader } from '$lib/paraglide/runtime';
// Dentro do paraglideHandle
const preferred = detectLocaleFromAcceptLanguageHeader(
event.request.headers.get('Accept-Language')
);
if (preferred) {
// Redireciona ou define cookie com base na preferência
}
10.2 Conteúdo localizado dinâmico com dados de API
Para conteúdo que já está localizado por uma API externa (por exemplo, CMS), passe o locale para a chamada da API:
import { getLocale } from '$lib/paraglide/runtime';
import type { PageLoad } from './$types';
export const load: PageLoad = async ({ fetch, params }) => {
const locale = getLocale();
const res = await fetch(`/api/content?locale=${locale}`);
return { content: await res.json() };
};
10.3 Roteamento com base em dados localizados
Se você tem slugs localizados de um CMS, use um parâmetro [slug] não localizado em vez de [locale]/[slug] e resolva o conteúdo por locale internamente.
11. Migração do v1
Se você está migrando do @inlang/paraglide-js-adapter-sveltekit v1:
- Remova a dependência do adaptador.
- Adicione
@inlang/paraglide-jsv2.x. - Substitua
paraglideVitePluginconforme mostrado na seção de configuração. - Substitua os hooks do adaptador pelos hooks
paraglideMiddleware+reroute. - Atualize as importações:
$lib/paraglide/…(o diretório de saída padrão mudou parasrc/lib/paraglide). - Altere as chamadas da API se usava
localede $app/stores — agora usegetLocale().
12. Notas Específicas do Svelte 5
- Use
$app/stateem vez de$app/stores. O acesso aopagevia$app/stateé o padrão Svelte 5. - Snippets em vez de slots. Para rich text, use
{#snippet}e<ParaglideMessage>conforme mostrado. $effectnão é para rastrear locale. A mudança de locale é uma navegação de documento, então$effectnão é acionado. Se você precisar reagir a mudanças de locale em uma superfície somente cliente, use uma prop ou$derivedde um estado gerenciado externamente.- Componentes como funções puras. Passe locale como prop para componentes que precisam dele, em vez de ler o estado global.
13. Recursos
- Documentação do Paraglide JS
- Documentação do SvelteKit
- Documentação do Svelte 5
- Benchmark de tamanho de bundle
- Exemplo de demonstração
14. Convenções de Commit
feat(i18n): adiciona suporte a localefix(paraglide): corrige hidratação de localedocs(i18n): atualiza guia de melhores práticaschore(deps): atualiza @inlang/paraglide-js