Communitygithub.com

Paraglide JS + Svelte 5 ベストプラクティス

Paraglide JS 2.x(コンパイラベース、ツリーシェイカブル)と Svelte 5(ルーン)を使用して、型安全で高性能な i18n アプリを構築するための専門家によるガイダンス。セットアップ、メッセージ作成、ロケール戦略、i18n ルーティング、SSR/SSG、Svelte 5 スニペットを使用したリッチテキストマークアップ、リアクティビティパターン、テスト、トラブルシューティングをカバーします。

Paraglide JS + Svelte 5 ベストプラクティス とは?

このスキルは、Paraglide JS 2.x と Svelte 5 を組み合わせて国際化(i18n)アプリケーションを構築するための包括的なガイドを提供します。Paraglide JS は SvelteKit の公式 i18n 統合であり、メッセージファイルをコンパイル時にツリーシェイカブルな ESM 関数に変換することで、ランタイムライブラリと比較して i18n バンドルサイズを最大 70% 削減します。このスキルは、ロケールをアプリケーションの状態として扱うのではなく、コンパイラによって解決されるリクエスト/ビルド状態として扱うという重要な考え方を強調し、ロケールの切り替えには Paraglide の全ドキュメントナビゲーションを活用し、その他の UI 状態は Svelte 5 のルーンに任せることを推奨しています。セットアップ、メッセージ作成、ロケール戦略、i18n ルーティング、SSR/SSG、リッチテキストマークアップ、リアクティビティパターン、パフォーマンス最適化、テスト、トラブルシューティングなど、実践的なパターンとベストプラクティスを網羅しており、特に SvelteKit と Paraglide を使用する開発者にとって貴重なリソースです。

対応~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/agentmemoir/skills/tree/HEAD/skills/frontend/paraglide-svelte5-best-practices

お気に入りのAIに質問する

このエージェントスキルを事前に読み込んだ状態で新しいチャットを開きます。

ドキュメント

Paraglide JS + Svelte 5 ベストプラクティス は何をしますか?

ロケールは、リアクティブなアプリケーション状態ではなく、コンパイラによって解決されるリクエスト/ビルド状態として扱います。ロケールの切り替えは Paraglide の全ドキュメントナビゲーションに任せ、それ以外のすべては Svelte 5 のルーンに任せます。 Paraglide JS は SvelteKit の公式 i18n 統合です。messages/{locale}.json をツリーシェイカブルで完全に型付けされた ESM 関数にコンパイルし、ランタイムライブラリと比較して i18n バンドルを最大約 70% 削減します(公開ベンチマークでは、5 ロケール / 200 メッセージで約 47 KB 対約 205 KB)。非同期ウォーターフォールはなく、Svelte 5 コンポーネントに i18n プロバイダーを配線する必要もありません。


1. クイックリファレンス決定表

決定事項推奨デフォルト理由
インストール方法新しい SvelteKit アプリで npx sv add paraglide="languageTags:en,de"Vite プラグイン、フック、app.html 属性、.gitignore を 1 ステップでスキャフォールディング
パッケージ@inlang/paraglide-js v2.xv1 アダプター @inlang/paraglide-js-adapter-sveltekit は非推奨。コアパッケージを直接使用
戦略(SSR + ローカライズされた URL)["url", "cookie", "baseLocale"]URL は SEO の信頼できる情報源。Cookie はサーバーから見える形でユーザー選択を保持。baseLocale は常に最後に解決
戦略(保存されたオーバーライドを追加)["localStorage", "cookie", "preferredLanguage", "url", "baseLocale"]localStorage だけでは最初の SSR リクエストからは見えません。Cookie がサーバー/クライアントのギャップを埋める
ロケール切り替え動作デフォルトの setLocale(locale)(全ドキュメントナビゲーション)フレームワークのリアクティビティでは <html lang>、dir、<title>、ローカライズされた URL は再レンダリングされません。ナビゲーションが行います
AsyncLocalStorage有効のまま(デフォルト)Vercel Edge と Cloudflare Workers(Node 互換)がサポート。無効にすると、同時リクエスト間でロケールがリークするリスクがあります
TS 宣言emitTsDeclarations: true(TypeScript ≥ 5.6)開発中にキーが変更されたときに、Svelte + TS 言語サーバーを同期し続けます
メッセージキーフラットで安定した、人間が読めるランダムキー(calm_green_otter)公式推奨。ネストされたキーも機能しますが、フラットキーが推奨されます
プリレンダリングexport const prerender = true + ロケールごとの entries()SSG では、すべてのロケールが個別の URL ファイルに解決される必要があります
ロケールのリアクティビティデフォルトではコンポーネント内ではなしgetLocale() はリクエストスコープ。ロケールごとに新しいドキュメントがレンダリングされるため、ルーンがそれを追跡する必要はありません

2. セットアップと設定(Paraglide 2.x + SvelteKit + Svelte 5)

2.1 最速のパス

npx sv create my-app
npx sv add paraglide="languageTags:en,de,demo:yes"

Svelte CLI アドオンは、inlang プロジェクト、Vite プラグイン、SvelteKit handle/reroute フック、app.html の %lang%/%dir%、オプションのデモページを設定します。

2.2 手動セットアップ(完全なボイラープレート)

プロジェクトをインストールしてスキャフォールディングした後、4 つのファイルを配線します。

vite.config.ts — 明示的な戦略を持つプラグイン:

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 はメッセージを編集するたびに .d.ts を再生成し、Svelte と TypeScript サーバーの同期を維持します。TypeScript 5.6+ が必要です(高速な JSDoc 推論パスの場合は false に設定)。

src/app.html — ハードコードされた lang を置き換える:

<!doctype html>
<html lang="%lang%" dir="%dir%">

src/hooks.server.ts — リクエストスコープのロケールを持つサーバーミドルウェア:

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;

ミドルウェアはロケールを検出し(戦略の順序に従う)、URL が検出されたロケールと一致しない場合にドキュメントリクエストをリダイレクトし、/de/about → /about を SvelteKit が認識する前にローカライズ解除し、リクエストを AsyncLocalStorage でラップして、getLocale() が同時リクエストごとに正しいロケールを返すようにします。

src/hooks.ts — ルーティング境界(hooks.server.ts ではなく hooks.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(.*)?'],
      ],
    },
  ],
});

優先順位: CLI フラグ / プラグインオプション > 設定ファイル > 組み込みデフォルト。ウォッチモードは設定をホットリロードします。


3. コア使用パターン(メッセージ、ロケール、ルーティング)

3.1 メッセージ

import { m } from '$lib/paraglide/messages';
import type { LocalizedString } from '$lib/paraglide/runtime';

m.hello_world();                       // "Hello World!"
m.greeting({ name: 'Ada' });           // "Hello Ada!" — パラメータは型付けされています
m.greeting({ name: 'Ada' }, { locale: 'de' }); // 1 回の呼び出しでロケールを強制
const title: LocalizedString = m.welcome_title(); // ブランドセーフな文字列型

メッセージは messages/{locale}.json にあります。ロケールは project.inlang/settings.json(baseLocale + locales)で宣言されます。フラットで安定したキーを優先します。ネストされたキーは m['user.profile.title']() を介してサポートされています。動的なキーアクセスはツリーシェイキングを妨げるため、動的 ID を明示的なメッセージ参照にマッピングしてください。

3.2 ロケールの取得と設定

import { getLocale, locales, setLocale, getTextDirection } from '$lib/paraglide/runtime';

getLocale();          // 例: "de" — サーバー上ではリクエストスコープ
getTextDirection();   // 現在のロケールの "ltr" | "rtl"
setLocale('de');      // ローカライズされた URL への全ドキュメントナビゲーション

setLocale() は設定された戦略を更新し、その後 全ドキュメントナビゲーション を実行します。url 戦略がアクティブな場合はローカライズされた URL へ、そうでない場合はリロードします。これは意図的です。<html lang>、dir、<title>、メタデータ、およびローカライズされた URL はドキュメントレベルの状態であり、フレームワークのリアクティビティでは更新できません。

setLocale(locale, { reload: false }) エスケープハッチは、完全にクライアントレンダリングされたサーフェス(埋め込みウィジェット、拡張機能ページ)でのみ存在し、復元不可能なインメモリ状態を保持する必要がある場合に使用します。通常のロケールピッカー、SSR/SSG ドキュメント、または戦略に url が含まれるルートでは絶対に使用しないでください。古い URL とシェルがそのまま残り、getLocale() が古いロケールを解決し続けることになります。

3.3 ローカライズされた URL とリンク

import { localizeHref, deLocalizeUrl, localizeUrl } from '$lib/paraglide/runtime';

localizeHref('/blog');                          // 現在のロケールでは "/de/blog"
localizeHref('/blog', { locale: 'de' });        // 明示的
localizeUrl('https://example.com/about', { locale: 'de' }).href; // 完全な URL 形式

URL パターンは標準の URLPattern 構文を使用します。順序が重要です。特定のパターンをワイルカードの前に配置し、urlpattern.com でテストしてください。

3.4 Svelte 5 ロケールスイッチャー(慣用的)

<script lang="ts">
  import { page } from '$app/state';                       // Svelte 5: オブジェクト、ストアではない
  import { resolve } from '$app/paths';
  import { locales, localizeHref } from '$lib/paraglide/runtime';
</script>

<nav aria-label="言語">
  {#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>

ここでは 2 つの Svelte 5 の詳細が重要です。$app/state は page をリアクティブオブジェクトとして公開します($page ストアと $app/stores はレガシーパターンです)。また、クロスロケールリンクの data-sveltekit-reload は、ローカライズされた URL、サーバーレンダリングされたドキュメント、およびランタイムロケールの同期を維持します。これはプリレンダリングされたクローリングにも必要です。


4. Svelte 5 スニペットを使用したリッチテキスト(ParaglideMessage)

マークアップにより、翻訳者は構造を制御し、アプリはレンダリングを制御できます。タグ名はアプリ定義です。何も自動的に HTML にマッピングされるわけではありません。

// messages/en.json
{
  "cta": "{#link to=|/docs|}ドキュメントを読む{/link}",
  "welcome": "{#b}こんにちは {name}{/b}{#icon/}"
}
<script lang="ts">
  import { ParaglideMessage } from '@inlang/paraglide-js-svelte';
  import { m } from '$lib/paraglide/messages';
  const name = $props().name; // または任意の $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> は、マークアップが存在する場合は message.parts() を使用し、プレーンテキストの場合は message() にフォールバックします。{#snippet tag({ children, options, attributes })} 構文は、まさに Svelte 5 のスニペットパターンです。スロットも <slot> レガシーもありません。リテラル構文 to=|/docs|(パイプ区切り)に注意してください。また、m.welcome() 単独では、マークアップが除去されたプレーンテキストを返します。


5. Svelte 5 ルーン統合: リアクティブなもの(とそうでないもの)

重要なメンタルモデル: Paraglide はロケールをドキュメント/リクエスト状態として所有し、Svelte 5 ルーンは UI 状態を所有します。 ロケールの変更は新しいドキュメントを提供するため、getLocale() を追跡するためにルーンが必要になることはほとんどありません。実際に発生するパターン:

単一ドキュメント内のロケール依存の派生値(例: reload: false を使用するクライアント専用サーフェス、または複数のロケールを明示的にレンダリングするプレビューペイン):

<script lang="ts">
  import { getLocale, locales } from '$lib/paraglide/runtime';
  import { m } from '$lib/paraglide/messages';

  let { locale = getLocale() } = $props();          // 型付けされた props
  // $derived は `locale` prop が変更されたときに再評価されます — Paraglide の内部ロケールが変更されたときではありません(ナビゲーションなしでは変更されません):
  const greeting = $derived(m.greeting({ name: 'Ada' }, { locale }));
  const isRtl = $derived(getTextDirection(locale) === 'rtl');
</script>

グローバル状態を読み取る代わりに、強制されたロケールを下位に渡す ことがルーンフレンドリーなパターンです。各メッセージ呼び出しの { locale } オプションにより、コンポーネントは props の純粋関数になります。これはまさに Svelte 5 が期待する状態の流れ方です。$effect は本当の副作用(例: プレビューロケールの永続化)のために予約してください。$derived に決して代入せず、$effect 内から $state に書き込んでロケールを「プッシュ」しないでください。代わりに props を渡してください。

グローバルクライアント状態(例: アプリ内翻訳ツールの「最後にプレビューされたロケール」):

// src/lib/locale-preview.svelte.ts — .svelte.ts ファイルはモジュールスコープでのルーンをサポート
import { getLocale } from '$lib/paraglide/runtime';

export const preview = $state({ locale: getLocale() ?? 'en' });

$state をモジュールレベルで使用する場合、.svelte.ts ファイル内のモジュールレベルの $state は、すべてのインポーター間で共有されます。アプリ全体のシングルトンには問題ありませんが、これが本番ロケールの動作方法(戦略チェーン + ドキュメントナビゲーション)ではないことを覚えておいてください。


6. SSR、ハイドレーション、SSG

6.1 SSR ルール

  • 1 つの handle チェーンの順序: paraglideHandle を最初に(またはロケールでキー付けされた event.locals を必要とする認証ハンドラの前に)配置し、ダウンストリームフックがローカライズ解除されたリクエストを認識できるようにします。
  • TLS 終端プロキシの背後では、effectiveRequestUrl を渡して、URL マッチングとリダイレクトがブラウザ側のオリジンを使用するようにします。
  • AsyncLocalStorage はオンのままにします。これにより、getLocale() が同時リクエストとストリーミング SSR のもとで安全になります。disableAsyncLocalStorage フォールバックは、node:async_hooks を持たず、かつ リクエストごとの分離が保証されているランタイムのみを対象としています。それ以外の場所で使用すると、ユーザー間でロケールがリークする可能性があります。

6.2 ハイドレーションの正確性

サーバーとクライアントは、同じ戦略順序からロケールを解決する必要があります。典型的な不一致: サーバーは URL から解決するが、クライアント戦略は最初に localStorage を読み取り、これは SSR 時には存在しません。修正方法は、永続化された選択が最初の応答を形作る必要がある場合に、サーバーから見える cookie 戦略を含めることです。

6.3 SSG(adapter-static / prerender)

プリレンダリングはミドルウェアを実行しません。ビルド中にページごとにロケールを設定します:

// 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 }));
}

SSG では、すべての ロケールが URL プレフィックスを持つ必要があります(urlPatterns を設定して /en/… と /de/… の両方が存在するようにします)。そうしないと、SvelteKit はロケールごとに個別のファイルを出力できません。静的アダプターの SPA フォールバックでは、ロケールプレフィックス付きアセットの 404 を回避するために kit.paths.relative = false を設定し、サイトマップには generateStaticLocalizedUrls() を使用します。


7. パフォーマンスとツリーシェイキング

  • メッセージ関数を直接呼び出します。 m.hello() は 1 つの小さな ESM 関数にコンパイルされます。バンドラーは未使用のメッセージとその翻訳を自動的に削除します。バンドルサイズはカタログが成長しても一定です(ベンチマーク: プロジェクトが 200 でも 1,000 のメッセージを持っていても 47 KB)。
  • 動的なキーアクセスを避けます。 m[dynamicKey]() は、すべてのメッセージがインポートされる原因となるため、ツリーシェイキングを無効にします。代わりに、動的 ID を静的なメッセージ参照にマッピングします。
  • @inlang/paraglide-js のバージョンを 2.x に保ちます。 v1 は大幅に遅く、バンドルサイズも大きくなります。
  • メッセージ関数はデフォルトでメモ化されています。 同じ引数で複数回呼び出すと、キャッシュされた結果が返されます。

8. テスト

8.1 ユニットテスト

// src/lib/paraglide/messages.test.ts
import { describe, it, expect } from 'vitest';
import { m } from './messages';

describe('メッセージ', () => {
  it('hello_world が正しい文字列を返す', () => {
    expect(m.hello_world()).toBe('Hello World!');
  });

  it('greeting がパラメータを受け入れる', () => {
    expect(m.greeting({ name: 'Ada' })).toBe('Hello Ada!');
  });

  it('greeting がロケールオーバーライドを受け入れる', () => {
    expect(m.greeting({ name: 'Ada' }, { locale: 'de' })).toBe('Hallo Ada!');
  });
});

8.2 統合テスト

// src/routes/+page.test.ts
import { describe, it, expect } from 'vitest';
import { render } from '@testing-library/svelte';
import Page from './+page.svelte';

describe('ホームページ', () => {
  it('ローカライズされたメッセージをレンダリングする', () => {
    const { getByText } = render(Page);
    expect(getByText('Hello World!')).toBeDefined();
  });
});

8.3 E2E テスト

// tests/i18n.spec.ts
import { test, expect } from '@playwright/test';

test('ロケール切り替えが正しく機能する', async ({ page }) => {
  await page.goto('/');
  await page.click('text=DE');
  await expect(page).toHaveURL('/de');
  await expect(page.locator('html')).toHaveAttribute('lang', 'de');
  await expect(page.getByText('Hallo Welt!')).toBeVisible();
});

9. トラブルシューティング

9.1 よくある問題

問題原因解決策
getLocale() がサーバーとクライアントで異なる値を返す戦略の順序の不一致cookie 戦略を追加して、サーバーがクライアントの選択を認識できるようにする
setLocale() がナビゲーションをトリガーしないreload: false の誤用通常のロケールスイッチャーでは reload を省略する
SSG がロケールごとにファイルを生成しないurlPatterns にロケールプレフィックスがないすべてのロケールに URL プレフィックスを設定する
メッセージ関数が TypeScript エラーをスローする古い .d.ts ファイルemitTsDeclarations: true を有効にするか、手動で outdir を削除して再構築する
Paraglide ミドルウェアがリクエストをリダイレクトし続ける設定ミスまたは戦略の競合戦略の順序を確認し、localStorage と cookie の値が一致していることを確認する

9.2 デバッグ

// 現在のロケールと戦略状態をログに記録する
console.log('Current locale:', getLocale());
console.log('All locales:', locales);
console.log('Text direction:', getTextDirection());

10. 付録: リソースと参考資料

Individual skills in this repo

This repo contains 2 individual skills — each has its own dedicated page.

agentmemoir/skills

>- Build, migrate, or review server-rendered Svelte 5 apps using runes ($state, $derived, $effect, $props, $bindable) with SvelteKit SSR (adapter-auto/node/cloudflare/vercel/netlify) or plain Vite SSR using svelte/server render() and hydrate(). Use whenever I mention Svelte 5 SSR, server-side rendering, SvelteKit load functions, +page.server.js, +layout.server.js, hooks.server.js, form actions, streaming SSR, CSP nonces, hydration mismatches, or request-scoped state. Also use to review .svelte and server files for SSR-safety bugs: effects that never run on the server, module-level state leaking between users, non-deterministic render values, and secrets reaching the client. Trigger even on a bare pasted component that "renders differently on the server and client".

agentmemoir/skills

>- Build, migrate, or review static (no server) Svelte 5 apps using runes ($state, $derived, $effect, $props, $bindable) instead of legacy Svelte 4 reactivity (export let, reactive dollar-colon statements, stores-as-default). Use whenever I mention Svelte 5, runes, a "static Svelte site", "Svelte SPA", "SvelteKit static site"/"adapter-static"/prerendered SvelteKit, deploying Svelte to GitHub Pages/Netlify/Cloudflare Pages/S3/any CDN, or converting Svelte 4 code (export let, on:click, writable stores) to Svelte 5. Also use when writing or reviewing .svelte files that must ship with no backend, to flag accidental server-only SvelteKit files (+page.server.js, +server.js, form actions) or exposed secrets, and to apply the bundled XSS/CSP/secrets security checklist. Trigger even for a bare pasted .svelte file or a vague "make this a static site" request.

関連スキル