Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

i18n: localized / translated routes #1130

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions packages/create-svelte/templates/default/i18n.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import locales from './src/locales.js';

export const defaultLocale = locales[0];

/** @typedef {{
* content: string;
* dynamic: boolean;
* spread: boolean;
* }} Part */

/**
* Create localized routes prefixed with locale
* @param {Part[][]} segments
* @param {'page' | 'endpoint'} type
* @returns {Part[][][]}
*/
export function localizeRoutes(segments, type) {
if (type === 'endpoint') return [segments];
return locales.map((locale) =>
locale === defaultLocale
? segments
: [
[{ content: locale, dynamic: false, spread: false }],
...segments.map((segment) => segment.map((part) => translate(part)))
]
);
}

/**
* Translate part of a route segment
* @param {Part} part
* @returns {Part}
*/
function translate(part) {
if (part.content === 'about') return { ...part, content: 'ueber' };
return part;
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,21 @@
<script lang="ts">
import { page } from '$app/stores';
import logo from './svelte-logo.svg';
import { l, localizedPaths, locale as currentLocale, defaultLocale } from '$lib/i18n';

$: alternatePaths = $localizedPaths($page.url.pathname);
$: defaultPath = alternatePaths[defaultLocale];
</script>

<svelte:head>
{#if defaultPath}
<link rel="alternate" hreflang="x-default" href={defaultPath} />
{/if}
{#each Object.entries(alternatePaths) as [locale, path]}
<link rel="alternate" hreflang={locale} href={path} />
{/each}
</svelte:head>

<header>
<div class="corner">
<a href="https://kit.svelte.dev">
Expand All @@ -15,12 +28,14 @@
<path d="M0,0 L1,2 C1.5,3 1.5,3 2,3 L2,0 Z" />
</svg>
<ul>
<li class:active={$page.url.pathname === '/'}><a sveltekit:prefetch href="/">Home</a></li>
<li class:active={$page.url.pathname === '/about'}>
<a sveltekit:prefetch href="/about">About</a>
<li class:active={$page.url.pathname === $l('/')}>
<a sveltekit:prefetch href={$l('/')}>Home</a>
</li>
<li class:active={$page.url.pathname === $l('/about')}>
<a sveltekit:prefetch href={$l('/about')}>About</a>
</li>
<li class:active={$page.url.pathname === '/todos'}>
<a sveltekit:prefetch href="/todos">Todos</a>
<li class:active={$page.url.pathname === $l('/todos')}>
<a sveltekit:prefetch href={$l('/todos')}>Todos</a>
</li>
</ul>
<svg viewBox="0 0 2 3" aria-hidden="true">
Expand All @@ -30,6 +45,11 @@

<div class="corner">
<!-- TODO put something else here? github link? -->
<nav>
{#each Object.entries(alternatePaths) as [locale, path]}
<a class:active={$currentLocale === locale} href={path}>{locale}</a>
{/each}
</nav>
</div>
</header>

Expand All @@ -40,7 +60,7 @@
}

.corner {
width: 3em;
display: flex;
height: 3em;
}

Expand All @@ -50,6 +70,23 @@
justify-content: center;
width: 100%;
height: 100%;
text-transform: uppercase;
}

.corner nav a {
position: relative;
}

.corner a.active::before {
--size: 6px;
content: '';
width: 0;
height: 0;
position: absolute;
top: 0;
left: calc(50% - var(--size));
border: var(--size) solid transparent;
border-top: var(--size) solid var(--accent-color);
}

.corner img {
Expand Down
29 changes: 29 additions & 0 deletions packages/create-svelte/templates/default/src/lib/i18n.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { derived, readable } from 'svelte/store';
import { page } from '$app/stores';
import { alternates } from '$app/navigation';

import locales from '../locales';

export const defaultLocale = locales[0];

export const locale = derived(
page,
(page) => page.url.pathname.match(/^\/([a-z]{2})(\/|$)/)?.[1] || defaultLocale
);

export const localizedPaths = readable(
(path: string): Record<string, string> =>
alternates(path)?.reduce((result, alt) => {
result[alt.match(/^\/([a-z]{2})(\/|$)/)?.[1] || defaultLocale] = alt;
return result;
}, {})
);

export const l = derived(
[localizedPaths, locale],
([localizedPaths, locale]) =>
(path: string): string =>
localizedPaths(path)?.[locale] || path
);

export { l as localize };
1 change: 1 addition & 0 deletions packages/create-svelte/templates/default/src/locales.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default ['en', 'de'];
5 changes: 4 additions & 1 deletion packages/create-svelte/templates/default/svelte.config.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import adapter from '@sveltejs/adapter-auto';
import preprocess from 'svelte-preprocess';
import { localizeRoutes } from './i18n.config.js';

// This config is ignored and replaced with one of the configs in the shared folder when a project is created.

Expand All @@ -18,7 +19,9 @@ const config = {
// Override http methods in the Todo forms
methodOverride: {
allowed: ['PATCH', 'DELETE']
}
},

alternateRoutes: localizeRoutes
}
};

Expand Down
1 change: 1 addition & 0 deletions packages/kit/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@
/client/**/*.d.ts
/test/**/.svelte-kit
/test/**/build
/test/**/errors.json
!/src/core/adapt/test/fixtures/*/.svelte-kit
!/test/node_modules
6 changes: 6 additions & 0 deletions packages/kit/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# @sveltejs/kit

## 1.0.0-next.252
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

feature PRs must not modify the changelog


### Patch Changes

- remove nonexistent `url` store from `$app/stores` ambient types ([#3640](https://github.com/sveltejs/kit/pull/3640))

## 1.0.0-next.251

### Patch Changes
Expand Down
2 changes: 1 addition & 1 deletion packages/kit/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@sveltejs/kit",
"version": "1.0.0-next.251",
"version": "1.0.0-next.252",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this does not belong in a feature PR

"repository": {
"type": "git",
"url": "https://github.com/sveltejs/kit",
Expand Down
1 change: 1 addition & 0 deletions packages/kit/src/core/config/index.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const get_defaults = (prefix = '') => ({
kit: {
adapter: null,
amp: false,
alternateRoutes: null,
appDir: '_app',
browser: {
hydrate: true,
Expand Down
8 changes: 8 additions & 0 deletions packages/kit/src/core/config/options.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,14 @@ const options = object(

amp: boolean(false),

alternateRoutes: validate(null, (option, keypath) => {
if (typeof option !== 'function') {
throw new Error(`${keypath} must be a function that processes route segments`);
}

return option;
}),

appDir: validate('_app', (input, keypath) => {
assert_string(input, keypath);

Expand Down
6 changes: 5 additions & 1 deletion packages/kit/src/core/create_app/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,8 @@ function generate_client_manifest(manifest_data, base) {
'})';

const tuple = [route.pattern, get_indices(route.a), get_indices(route.b)];
if (params) tuple.push(params);
tuple.push(params || '');
tuple.push(route.id);

return `// ${route.a[route.a.length - 1]}\n\t\t[${tuple.join(', ')}]`;
}
Expand Down Expand Up @@ -149,6 +150,7 @@ function generate_app(manifest_data) {
// stores
export let stores;
export let page;
export let routes;

export let components;
${levels.map((l) => `export let props_${l} = null;`).join('\n\t\t\t')}
Expand All @@ -158,6 +160,8 @@ function generate_app(manifest_data) {
$: stores.page.set(page);
afterUpdate(stores.page.notify);

if (routes) setContext('__svelte_routes__', routes);

let mounted = false;
let navigated = false;
let title = null;
Expand Down
88 changes: 54 additions & 34 deletions packages/kit/src/core/create_manifest_data/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -193,50 +193,70 @@ export default function create_manifest_data({
layout_reset ? [error] : error_stack.concat(error)
);
} else if (item.is_page) {
const id = components.length.toString();
const alternates = config.kit.alternateRoutes
? config.kit.alternateRoutes(segments, 'page')
: [segments];

components.push(item.file);

const concatenated = layout_stack.concat(item.file);
const errors = error_stack.slice();

const pattern = get_pattern(segments, true);

let i = concatenated.length;
while (i--) {
if (!errors[i] && !concatenated[i]) {
errors.splice(i, 1);
concatenated.splice(i, 1);
alternates.forEach((segments) => {
const pattern = get_pattern(segments, true);
const params = segments.flatMap((parts) =>
parts.filter((p) => p.dynamic).map((p) => p.content)
);

let i = concatenated.length;
while (i--) {
if (!errors[i] && !concatenated[i]) {
errors.splice(i, 1);
concatenated.splice(i, 1);
}
}
}

i = errors.length;
while (i--) {
if (errors[i]) break;
}

errors.splice(i + 1);

const path = segments.every((segment) => segment.length === 1 && !segment[0].dynamic)
? `/${segments.map((segment) => segment[0].content).join('/')}`
: '';
i = errors.length;
while (i--) {
if (errors[i]) break;
}

routes.push({
type: 'page',
segments: simple_segments,
pattern,
params,
path,
a: /** @type {string[]} */ (concatenated),
b: /** @type {string[]} */ (errors)
errors.splice(i + 1);

const path = segments.every((segment) => segment.length === 1 && !segment[0].dynamic)
? `/${segments.map((segment) => segment[0].content).join('/')}`
: '';

routes.push({
id,
type: 'page',
segments: simple_segments,
pattern,
params,
path,
a: /** @type {string[]} */ (concatenated),
b: /** @type {string[]} */ (errors)
});
});
} else {
const pattern = get_pattern(segments, !item.route_suffix);

routes.push({
type: 'endpoint',
segments: simple_segments,
pattern,
file: item.file,
params
const alternates = config.kit.alternateRoutes
? config.kit.alternateRoutes(segments, 'endpoint')
: [segments];

alternates.forEach((segments) => {
const pattern = get_pattern(segments, !item.route_suffix);
const params = segments.flatMap((parts) =>
parts.filter((p) => p.dynamic).map((p) => p.content)
);

routes.push({
type: 'endpoint',
segments: simple_segments,
pattern,
file: item.file,
params
});
});
}
});
Expand Down
Loading
Loading