Skip to content
Open
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
374 changes: 373 additions & 1 deletion packages/adapter-svelte/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -259,12 +259,17 @@ You may run into the error **`Error [ERR_MODULE_NOT_FOUND]: Cannot find package
<!-- ------------------------------------------------------------------------------------------ -->
<!-- ------------------------------------------------------------------------------------------ -->

## recipes
## Recipes

### How do I render a component inside a Translation?

By default `typesafe-i18n` at this time does not provide such a functionality. But you could easily write a function like this:

### Wrap Piece of Translation with a component:

<details>
<summary>Single Text Wrapper</summary>

```svelte
<script lang="ts">
import type { LocalizedString } from 'typesafe-i18n'
Expand Down Expand Up @@ -309,3 +314,370 @@ Use it inside your application
</WrapTranslation>
</main>
```
</details>

### Wrapping Multiple Pieces of Translation using snippets.

<details>
<summary>Type Safe Version</summary>


#### This component requires a couple of things:

##### 1. Valid Translation Object to infer the keys from

<details>
<summary>i18n/en/index.ts</summary>

Your translation **keys** will serve as the **type** for creating the typed component
The snippet types need to include the keys as if it's jsx as the type is inferred from the occurance of `</${string}>`
They need to look something like this:
```ts
import type { BaseTranslation } from '../i18n-types';

const en = {
'Hi {name:string}, click <someSnippet>here</someSnippet> to create your <anotherSnippet>first</anotherSnippet> project':
'Hi {name:string}, click <someSnippet>here</someSnippet> to create your <anotherSnippet>first</anotherSnippet> project',
'Goodbye, click <someSnippet>here</someSnippet> to delete your <anotherSnippet>first</anotherSnippet> project':
'Goodbye, click <someSnippet>here</someSnippet> to delete your <anotherSnippet>first</anotherSnippet> project',
} satisfies BaseTranslation;

export default en;
```
</details>

---

##### 2. `Prettify` Type _(if you like somewhat readible types)_
<details>
<summary>Prettify.ts</summary>


For example under `$lib/utilities/typeUtils/Prettify.ts`
```ts
/* eslint-disable @typescript-eslint/ban-types */
export type Prettify<T> = {
[K in keyof T]: T[K];
} & {};
```
Credits to [TanStack](https://github.com/TanStack)?
</details>

---

##### 3. `InferSnippets` Type to infer the correct snippets from the translation key

<details>
<summary>InferSnippets.ts</summary>


For example under `$lib/utilities/typeUtils/InferSnippets.ts`
```ts
/* eslint-disable @typescript-eslint/ban-types */
import type { Prettify } from '$lib/utilities/typeUtils/Prettify';

export type InferSnippets<TMessage extends string> =
TMessage extends `${string}</${infer TSnippetName extends string}>${infer TRemainingMessage extends string}`
? Prettify<{ [key in TSnippetName]: string } & InferSnippets<TRemainingMessage>>
: {};

```
Credits to [@ViewableGravy](https://github.com/ViewableGravy).
</details>

---

##### 4. `LL` Type Override "`TLL`"

<details>
<summary>i18n-svelte-tll.ts</summary>


For example next to `i18n-svelte.ts` in your `i18n` folder: `$i18n/i18n-svelte-tll`
```ts
import type { Prettify } from '$lib/utilities/typeUtils/Prettify';
import type { LocalizedString } from 'typesafe-i18n';
import type { TranslationFunctions } from './i18n-types';

import { LL } from '$i18n/i18n-svelte';
import { type Readable } from 'svelte/store';

type CreateInferredLLInstance<TRecord extends Record<string, unknown>> = {
[TKey in keyof TRecord]: TRecord[TKey] extends () => LocalizedString
? () => Prettify<TKey>
: TRecord[TKey] extends (arg: infer TObject) => LocalizedString
? (arg: TObject) => Prettify<TKey>
: TRecord[TKey] extends Record<string, unknown>
? CreateInferredLLInstance<TRecord[TKey]>
: 'Error: Unhandled type';
};

type InferredLLData = Prettify<CreateInferredLLInstance<TranslationFunctions>>;

const TLL = LL as unknown as Readable<InferredLLData>;

export default TLL;
```

</details>

---

##### 5. `TranslationSnippetWrapper` component

<details>
<summary>TranslationSnippetWrapper.svelte</summary>


```svelte
<script context="module" lang="ts">
import type { InferSnippets } from '$lib/utilities/typeUtils/InferSnippets';
import type { Snippet } from 'svelte';

type Replacers<TReplacers extends Object> = {
[key in keyof TReplacers]: Snippet<[string]>;
};

type Props<TMessage extends string> = {
message: TMessage;
replacers: Replacers<Prettify<InferSnippets<TMessage>>>;
};

type ReplacerDataEntry = string | { method: Snippet<[string]>; content: string };

/**
* Get all indices of a substring in a string
* @link https://stackoverflow.com/a/3410557
*/
function getIndicesOf(searchStr: string, str: string) {
const searchStrLen = searchStr.length;
if (searchStrLen == 0) {
return [];
}

let startIndex = 0;
let index;
let indices = [];

while ((index = str.indexOf(searchStr, startIndex)) > -1) {
indices.push(index);
startIndex = index + searchStrLen;
}
return indices;
}
</script>

<script lang="ts" generics="TMessage extends string">
import type { Prettify } from '$lib/utilities/typeUtils/Prettify';
import sortBy from 'lodash/sortBy';

const { message, replacers }: Props<TMessage> = $props();

type ReplacerKeys = keyof Prettify<InferSnippets<TMessage>>;

const replacerData: ReplacerDataEntry[] = $derived.by(() => {
const replacerKeys = Object.keys(replacers);

if (replacerKeys.length === 0) {
return [message];
}

// First get the index of the replacer keys
const allReplacerKeyIndexes = replacerKeys.reduce(
(previous, key) => {
const indeces = getIndicesOf(`<${key}>`, message);
indeces.forEach((index) => {
previous[index] = key as ReplacerKeys;
});
return previous;
},
{} as Record<number, ReplacerKeys>,
);

// Sort the replacer keys by their index
const sorted = sortBy(Object.entries(allReplacerKeyIndexes), ([index]) => index);

let cuttableMessage = message as string;

const finalRenderArray = sorted.reduce((current, [, key], index) => {
// Split the message into two parts: before and after the current replacer key
const [before, infixRaw] = cuttableMessage.split(`<${key as string}>`);
const [infix, after] = infixRaw.split(`</${key as string}>`);

// Get the remaining part of the message after the current replacer key
const subMessage = cuttableMessage.substring(cuttableMessage.indexOf(after));

// Push the before part and the current replacer data to the final render array
current.push(before, {
method: replacers[key],
content: infix,
});

// If it's the last replacer key, push the after part to the final render array
if (index === sorted.length - 1) {
current.push(after);
}

// Update the cuttableMessage to the remaining part
cuttableMessage = subMessage;

return current;
}, [] as ReplacerDataEntry[]);

return finalRenderArray;
});
</script>

{#each replacerData as part}
{#if typeof part === 'string'}
{part}
{:else}
{@render part.method(part.content)}
{/if}
{/each}
```

</details>

</details>

---

<details>
<summary>Basic Version</summary>

#### TranslationSnippetWrapper

_This example uses lodash for the sorting_

```svelte
<script context="module" lang="ts">
import type { Snippet } from 'svelte';
import type { LocalizedString } from 'typesafe-i18n';

type Props = {
message: LocalizedString | string;
replacers: Record<string, Snippet<[string]>>;
};

type ReplacerDataEntry = string | { method: Snippet<[string]>; content: string };

/**
* Get all indices of a substring in a string
* @link https://stackoverflow.com/a/3410557
*/
function getIndicesOf(searchStr: string, str: string) {
const searchStrLen = searchStr.length;
if (searchStrLen == 0) {
return [];
}

let startIndex = 0;
let index;
let indices = [];

while ((index = str.indexOf(searchStr, startIndex)) > -1) {
indices.push(index);
startIndex = index + searchStrLen;
}
return indices;
}
</script>

<script lang="ts">
import sortBy from 'lodash/sortBy';

const { message, replacers }: Props = $props();

const replacerData: ReplacerDataEntry[] = $derived.by(() => {
const replacerKeys = Object.keys(replacers);
// First get the index of the replacer keys
const allReplacerKeyIndexes = replacerKeys.reduce(
(previous, key) => {
const indeces = getIndicesOf(`<${key}>`, message);
indeces.forEach((index) => {
previous[index] = key;
});
return previous;
},
{} as Record<number, string>,
);

// Sort the replacer keys by their index
const sorted = sortBy(Object.entries(allReplacerKeyIndexes), ([index]) => index);

let cuttableMessage = message as string;

const finalRenderArray = sorted.reduce((current, [, key], index) => {
// Split the message into two parts: before and after the current replacer key
const [before, infixRaw] = cuttableMessage.split(`<${key}>`);
const [infix, after] = infixRaw.split(`</${key}>`);

// Get the remaining part of the message after the current replacer key
const subMessage = cuttableMessage.substring(cuttableMessage.indexOf(after));

// Push the before part and the current replacer data to the final render array
current.push(before, {
method: replacers[key],
content: infix,
});

// If it's the last replacer key, push the after part to the final render array
if (index === sorted.length - 1) {
current.push(after);
}

// Update the cuttableMessage to the remaining part
cuttableMessage = subMessage;

return current;
}, [] as ReplacerDataEntry[]);

return finalRenderArray;
});
</script>

{#each replacerData as part}
{#if typeof part === 'string'}
{part}
{:else}
{@render part.method(part.content)}
{/if}
{/each}
```

Your translations would look something like this:
```ts
const en = {
'Hi {name:string}, click <someSnippet>here</someSnippet> to create your <anotherSnippet>first</anotherSnippet> project':
'Hi {name:string}, click <someSnippet>here</someSnippet> to create your <anotherSnippet>first</anotherSnippet> project',
'Goodbye, click <someSnippet>here</someSnippet> to delete your <anotherSnippet>first</anotherSnippet> project':
'Goodbye, click <someSnippet>here</someSnippet> to delete your <anotherSnippet>first</anotherSnippet> project',
}
```
_(By using the same value for the translation as the key you have an easy overview of what snippets can be used)_

Use it inside your application

```svelte
{#snippet someSnippet(text: string)}
<p class="bold">
{text}
</p>
{/snippet}

{#snippet anotherSnippet(text: string)}
<p class="italic">
{text}
</p>
{/snippet}
<TranslationSnippetWrapper
message={$LL["Hi {name:string}, click <someSnippet>here</someSnippet> to create your <anotherSnippet>first</anotherSnippet> project"]("SanCoca")}
replacers={{ someSnippet, anotherSnippet }}
/>
```
</details>