Skip to content

Commit

Permalink
Merge pull request #2277 from opral/2216-fix-language-switcher
Browse files Browse the repository at this point in the history
SvelteKit Adapter Update
  • Loading branch information
LorisSigrist authored Feb 22, 2024
2 parents 37ae367 + 48e52f2 commit 03c8095
Show file tree
Hide file tree
Showing 20 changed files with 534 additions and 30 deletions.
5 changes: 5 additions & 0 deletions .changeset/gold-tomatoes-join.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@inlang/paraglide-js-adapter-sveltekit": patch
---

fix reactivity issue in Svelte 5 [#2270](https://github.com/opral/monorepo/issues/2270)
5 changes: 5 additions & 0 deletions .changeset/lovely-tips-compare.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@inlang/paraglide-js-adapter-sveltekit": minor
---

feat: Automatically call `invalidate("paraglide:lang")` when the language changes. You can now call `depends("paraglide:lang")` in your server-load functions to have them re-run on language changes.
5 changes: 5 additions & 0 deletions .changeset/proud-doors-crash.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@inlang/paraglide-js-adapter-sveltekit": patch
---

fix: Corrected comments saying the default placeholder for the text-direction is `%paraglide.dir%` when it's `%paraglide.textDirection%`
5 changes: 5 additions & 0 deletions .changeset/red-dogs-pump.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@inlang/paraglide-js-adapter-sveltekit": patch
---

fix: `i18n.resolveRoute` now ignores paths that don't start with the base
5 changes: 5 additions & 0 deletions .changeset/young-forks-unite.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@inlang/paraglide-js-adapter-sveltekit": patch
---

fix: Alternate links will not include the origin during prerendering, unless one is explicitly specified in `kit.prerender.origin`
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,7 @@ export const i18n = createI18n(runtime, {
})
```

## Customizing Link Translation
### Customizing Link Translation

Links are translated automatically using a preprocessor. This means that you can use the normal `a` tag and the adapter will translate it for you.

Expand Down Expand Up @@ -344,10 +344,57 @@ export const i18n = createI18n(runtime, {
})
```


### Accessing `lang` and `textDirection`

You can access the current language and text direction on `event.locals.paraglide` anywhere on your server. On the client, you can use the `languageTag()` function from `./paraglide/runtime.js` to access the current language.
You can access the current language and text direction on `event.locals.paraglide` object anywhere on your server. On the client, you can use the `languageTag()` function from `./paraglide/runtime.js` to access the current language.

### Using the Language on the Server

#### Avoiding Cross-Talk

The work SvelteKit does on the server can be split into two parts: Loading and Rendering. Data-Loading includes tasks like running your `load` functions, `actions` or server-hooks. Rendering is anything that happens in, or is called from, a `.svelte` file.

In general: loading is asynchronous & rendering is synchronous.

During the asynchronous loading, there is danger of crosstalk. Crosstalkt is when one request affectst the other. The danger during loading is that one requests overrides the language of another & causes it to return the wrong language. We need to take care to make sure this doesn't happen.

Because rendering is synchronous there is no danger of crosstalk & you can safely use messages and the `langaugeTag()` function without worrying about anything. The correct `languageTag()` will be set by the `<ParaglideJs>` component.

Here are some strategies to avoid cross-talk during loading:

By default, paraglide makes the language of the current request available in `locals.paraglide.lang`. You can explicitly specify which langauge a message should be in by passing in the language tag as an argument.

```ts
import * as m from "../paraglide/messages.js"

export async function load({ locals }) {
const translatedText = m.some_message({ ...message_params }, { languageTag: locals.paraglide.lang })
return { translatedText }
}
```

Unfortunately, if you have a lot of messages this can get quite tedious. Fortunately there is an easier way.

#### Re-Loading Language-Dependent data

Sometimes a piece of data needs to be reloaded when the language changes. You can do that by calling the `depends("paraglide:lang")` function during `load`

```ts
export async function load({ depends }) {
// The Adapter automatically calls `invalidate("paraglide:lang")` whenever the langauge changes
// This tells SvelteKit to re-run this function whenever that happens
depends("paraglide:lang")
return await myLanguageSpecificData();
}
```

## Caveats

Because we are using a Preprocessor for link localisation there are a few caveats to be aware of:

1. Links in the same Layout Component as `<ParagldieJS>` will not be translated.
2. Using a `{...speread}` operator on an element will cause the preprocessor to place all props on that element into one giant `{...spread}`. If you are using proxies that may cause issues.
3. If you are using a function-call as the value to `hreflang` the function will be called twice per render. If it has side-effects this may cause issues.

## FAQ

Expand All @@ -356,7 +403,6 @@ You can access the current language and text direction on `event.locals.paraglid
text="Yes, you can also include the default language in the URL by passing prefixDefaultLanguage: 'always' to createI18n.">
</doc-accordion>


<doc-accordion
heading="Can I change default language?"
text="Yes, using the 'defaultLanguage' option on 'createI18n'.">
Expand All @@ -368,17 +414,14 @@ You can access the current language and text direction on `event.locals.paraglid
better off using ParaglideJS directly.">
</doc-accordion>


<doc-accordion
heading="'Can't find module $paraglide/runtime.js' - What do I do?"
text="This likely means that you haven't registered the $paraglide alias for src/paraglide in svelte.config.js. Try adding that. Check the example if you're stuck">
</doc-accordion>


<doc-accordion
heading="My prerenderd pages include 'http://sveltekit-prerender', what's going on?"
text="There are some URLs that need to be fully qualified to be spec compliant. Usually SvelteKit
can guess the URL based on your current page, but during prerendering it has no idea where the files will be deployed, so it defaults to 'http://sveltekit-prerender'. You need to explicity tell it the URL of your Site. You can do this with the 'kit.prerender.origin' option in 'svelte.config.js'.">
heading="How can I make my alternate links full urls when prerendering?"
text="According to the spec, alternate links should be full urls that include the protocol and origin. By default the adapter can't know which URL your page will be deployed to while prerendering, so it only includes the path in the alternate url, not the origin or protocol. This works, but is suboptimal. You can tell the adapter which url you will be deploying to by setting kit.prerender.origin in your svelte.config.js">
</doc-accordion>

<doc-accordion
Expand All @@ -391,12 +434,20 @@ You can access the current language and text direction on `event.locals.paraglid
text="Paraglide is a compiler, so all translations need to be known at build time. You can of course manually react to the current language & fetch external content, but you will end up implementing your own solution for dynamically fetched translations.">
</doc-accordion>

## Roadmap
<doc-accordion
heading="Help! Links in +layout.svelte aren't being translated"
text="As stated in the caveats, <a> tags are not translated if they are in the same component as the <ParaglideJS> component. Move your Links into a different component and it should work.">
</doc-accordion>


## Roadmap to 1.0

- [ ] Expand the route features in Path translation
- [ ] Optional parameters
- [ ] Catch-all parameters
- [ ] Parameter matchers
- Expand the route features in Path translation
- Optional parameters
- Catch-all parameters
- Parameter matchers
- Improve Stability
- Improve Useability

## Playground

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
"@sveltejs/adapter-auto": "^3.0.0",
"@sveltejs/adapter-static": "^2.0.3",
"@sveltejs/kit": "^2.4.3",
"@sveltejs/vite-plugin-svelte": "^3.0.0",
"@sveltejs/vite-plugin-svelte": "^3.0.2",
"mdsvex": "^0.11.0",
"svelte": "^4.2.7",
"svelte-check": "^3.6.0",
Expand Down
Original file line number Diff line number Diff line change
@@ -1 +1,13 @@
export const prerender = true

/**
* @type { import("./$types").LayoutServerLoad}
*/
export function load({ locals, depends }) {
// This tells SvelteKit to re-run this load function when the language changes
depends("paraglide:lang")

return {
serverLang: `The language on the server is ${locals.paraglide.lang}`,
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
<script>
import { ParaglideJS } from "@inlang/paraglide-js-adapter-sveltekit"
import { i18n } from "$lib/i18n.js"
export let data;
</script>

<p>{data.serverLang}</p>

<ParaglideJS {i18n}>
<slot />
</ParaglideJS>
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,15 @@

<br/>

<ul>
{#each availableLanguageTags as lang}
<li>
<a href={i18n.route($page.url.pathname)} hreflang={lang}>
{m.change_language_to({ languageTag: lang })}
</a>
<br />
</li>
{/each}
</ul>

<a href="{base}/element">Element</a>

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
/* eslint-disable */
/** @type {((tag: AvailableLanguageTag) => void) | undefined} */
let _onSetLanguageTag

/**
* The project's source language tag.
*
* @example
* if (newlySelectedLanguageTag === sourceLanguageTag){
* // do nothing as the source language tag is the default language
* return
* }
*/
export const sourceLanguageTag = "en"

/**
* The project's available language tags.
*
* @example
* if (availableLanguageTags.includes(userSelectedLanguageTag) === false){
* throw new Error("Language tag not available")
* }
*/
export const availableLanguageTags = /** @type {const} */ (["de", "en"])

/**
* Get the current language tag.
*
* @example
* if (languageTag() === "de"){
* console.log("Germany 🇩🇪")
* } else if (languageTag() === "nl"){
* console.log("Netherlands 🇳🇱")
* }
*
* @type {() => AvailableLanguageTag}
*/
export let languageTag = () => sourceLanguageTag

/**
* Set the language tag.
*
* @example
*
* // changing to language
* setLanguageTag("en")
*
* // passing a getter function also works.
* //
* // a getter function is useful for resolving a language tag
* // on the server where every request has a different language tag
* setLanguageTag(() => {
* return request.langaugeTag
* })
*
* @param {AvailableLanguageTag | (() => AvailableLanguageTag)} tag
*/
export const setLanguageTag = (tag) => {
if (typeof tag === "function") {
languageTag = enforceLanguageTag(tag)
} else {
languageTag = enforceLanguageTag(() => tag)
}
// call the callback function if it has been defined
if (_onSetLanguageTag !== undefined) {
_onSetLanguageTag(languageTag())
}
}

/**
* Wraps an untrusted function and enforces that it returns a language tag.
* @param {() => AvailableLanguageTag} unsafeLanguageTag
* @returns {() => AvailableLanguageTag}
*/
function enforceLanguageTag(unsafeLanguageTag) {
return () => {
const tag = unsafeLanguageTag()
if (!isAvailableLanguageTag(tag)) {
throw new Error(
`languageTag() didn't return a valid language tag. Check your setLanguageTag call`
)
}
return tag
}
}

/**
* Set the `onSetLanguageTag()` callback function.
*
* The function can be used to trigger client-side side-effects such as
* making a new request to the server with the updated language tag,
* or re-rendering the UI on the client (SPA apps).
*
* - Don't use this function on the server (!).
* Triggering a side-effect is only useful on the client because a server-side
* environment doesn't need to re-render the UI.
*
* - The `onSetLanguageTag()` callback can only be defined once to avoid unexpected behavior.
*
* @example
* // if you use inlang paraglide on the server, make sure
* // to not call `onSetLanguageTag()` on the server
* if (isServer === false) {
* onSetLanguageTag((tag) => {
* // (for example) make a new request to the
* // server with the updated language tag
* window.location.href = `/${tag}/${window.location.pathname}`
* })
* }
*
* @param {(languageTag: AvailableLanguageTag) => void} fn
*/
export const onSetLanguageTag = (fn) => {
_onSetLanguageTag = fn
}

/**
* Check if something is an available language tag.
*
* @example
* if (isAvailableLanguageTag(params.locale)) {
* setLanguageTag(params.locale)
* } else {
* setLanguageTag("en")
* }
*
* @param {any} thing
* @returns {thing is AvailableLanguageTag}
*/
export function isAvailableLanguageTag(thing) {
return availableLanguageTags.includes(thing)
}

// ------ TYPES ------

/**
* A language tag that is available in the project.
*
* @example
* setLanguageTag(request.languageTag as AvailableLanguageTag)
*
* @typedef {typeof availableLanguageTags[number]} AvailableLanguageTag
*/
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,7 @@ export const NO_TRANSLATE_ATTRIBUTE = "data-no-translate"
/** The path suffix SvelteKit adds on Data requests */
export const DATA_SUFFIX = "__data.json"
export const HTML_DATA_SUFFIX = ".html__data.json"


/** The key with which `invalidate` is called when the language changes */
export const LANGUAGE_CHANGE_INVALIDATION_KEY = "paraglide:lang";
Loading

0 comments on commit 03c8095

Please sign in to comment.