Skip to content

Commit c6af663

Browse files
committed
Fixes for new cloud applications project
1 parent 6a82e5d commit c6af663

File tree

8 files changed

+109
-11
lines changed

8 files changed

+109
-11
lines changed
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<script lang="ts">
2+
import { availableLanguages, currentLanguage, getI18n, t } from "$lib/stores/i18n.store";
3+
4+
const i18n = getI18n();
5+
6+
function changeLanguage(event: Event & { currentTarget: EventTarget & HTMLSelectElement }) {
7+
$i18n.changeLanguage(event.currentTarget.value);
8+
}
9+
</script>
10+
11+
<label class="form-control">
12+
<div class="label">
13+
<span class="label-text">{$t("i18n.language")}</span>
14+
</div>
15+
<select
16+
class="select select-bordered"
17+
value={$currentLanguage}
18+
onchange={changeLanguage}
19+
data-testid="language-selector"
20+
>
21+
<option disabled>{$t("i18n.noLanguageSelected")}</option>
22+
{#each $availableLanguages as lang}
23+
<option value={lang} data-testid={`language-selector-option-${lang}`}
24+
>{$t(`i18n.languages.${lang}`)}</option
25+
>
26+
{/each}
27+
</select>
28+
</label>
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { createI18nStore, currentLanguage } from "$lib/i18n/i18n.store";
2+
import fetchMockVitest from "@fetch-mock/vitest";
3+
import { fireEvent, render, screen } from "@testing-library/svelte";
4+
import { get } from "svelte/store";
5+
import LanguageSelector from "./LanguageSelector.svelte";
6+
7+
describe("LanguageSelector", () => {
8+
beforeEach(() => {
9+
fetchMockVitest.mockGlobal();
10+
fetchMockVitest.get("/api/i18n/translations", {
11+
elements: []
12+
});
13+
});
14+
it("should render correctly", () => {
15+
const { getByText } = render(LanguageSelector, { context: new Map([["i18n", createI18nStore()]]) });
16+
expect(getByText("i18n.language")).toBeInTheDocument();
17+
});
18+
19+
it("should change language on select change", async () => {
20+
render(LanguageSelector, { context: new Map([["i18n", createI18nStore()]]) });
21+
const select = screen.getByTestId("language-selector") as HTMLSelectElement;
22+
await fireEvent.change(select, { target: { value: "de" } });
23+
expect(get(currentLanguage)).toBe("de");
24+
await fireEvent.change(select, { target: { value: "en" } });
25+
expect(get(currentLanguage)).toBe("en");
26+
});
27+
});

src/lib/modules/i18n/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export { default as LanguageSelector } from "./LanguageSelector.svelte";
2+

src/lib/modules/index.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1+
export * from "./chat";
12
export * from "./forms";
3+
export * from "./i18n";
24
export * from "./layout";
35
export * from "./modals";
46
export * from "./navigation";
57
export * from "./utils";
6-
export * from "./chat";
8+

src/lib/modules/navigation/Navbar.svelte

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
</script>
66

77
<div
8-
class="w-full flex flex-row gap-2 justify-between items-center"
8+
class="w-full flex flex-row gap-2 justify-between items-center px-5"
99
style="background-color: {options?.bgColor}; color: {options?.textColor}; height:{options.height ||
1010
64}px; --navbarBgColor: {options?.bgColor}; --navbarTextColor: {options?.textColor}"
1111
>
@@ -20,7 +20,7 @@
2020
</div>
2121
</a>
2222
<div class="flex flex-row gap-8 items-center">
23-
{#if options?.showNavigation}
23+
{#if options?.showNavigation !== false}
2424
{#each options?.navigation.entries as entry}
2525
<a class="border-b-2 menu-entry" href={entry.link}>
2626
{$t(entry.text)}

src/lib/modules/navigation/SideNavbar.svelte

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,14 +39,18 @@
3939
<span>{$t(entry.text)}</span>
4040
</div>
4141
</a>
42+
{#if Array.isArray(entry.entries)}
43+
<div class="flex flex-col gap-1 w-full">
44+
{#each entry.entries as subEntry}
45+
{@render menuEntry(subEntry, "pl-8 text-md")}
46+
{/each}
47+
</div>
48+
{/if}
4249
{/snippet}
4350

4451
{#each options?.navigation?.entries as entry}
4552
<div class="flex flex-col gap-1 w-full">
4653
{@render menuEntry(entry, "p-2 text-lg")}
47-
{#each entry.entries as subEntry}
48-
{@render menuEntry(subEntry, "pl-8 text-md")}
49-
{/each}
5054
</div>
5155
{/each}
5256
</div>

src/lib/modules/navigation/breakpoint.service.svelte.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,9 @@ class BreakpointService {
3737
}
3838
}
3939

40-
public matches(breakpoints: boolean | string | string[]) {
41-
if (typeof breakpoints === "boolean" || breakpoints === undefined) return breakpoints;
40+
public matches(breakpoints: boolean | string | string[]): boolean {
41+
if (typeof breakpoints === "boolean" || breakpoints === undefined)
42+
return breakpoints as boolean;
4243
if (typeof breakpoints === "string") breakpoints = [breakpoints];
4344
for (const breakpoint of breakpoints) {
4445
const dimensions = BREAKPOINT_DIMENSIONS[breakpoint];

src/lib/stores/i18n.store.ts

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { getContext, setContext } from "svelte";
55
import type { Readable } from "svelte/store";
66
import { derived, get, writable } from "svelte/store";
77

8-
export const getI18nStore = (i18nNextOptions?: Partial<InitOptions>) => {
8+
const createI18nStore = (i18nNextOptions?: Partial<InitOptions>) => {
99
i18nNext.use(LanguageDetector).init({
1010
fallbackLng: "en",
1111
detection: {
@@ -15,13 +15,37 @@ export const getI18nStore = (i18nNextOptions?: Partial<InitOptions>) => {
1515
interpolation: {
1616
escapeValue: false // not needed for svelte as it escapes by default
1717
},
18+
defaultNS: "translation",
1819
...i18nNextOptions
1920
});
2021
return derived([currentLanguage], () => i18nNext);
2122
};
2223

2324
export function initI18n(i18nNextOptions?: Partial<InitOptions>) {
24-
setContext("i18n", getI18nStore(i18nNextOptions));
25+
setContext("i18n", createI18nStore(i18nNextOptions));
26+
}
27+
28+
export function addTranslations(locale: string, translations: Record<string, unknown>) {
29+
i18nNext.addResourceBundle(locale, "translation", translations, true, true);
30+
translationsUpdated.set(true);
31+
}
32+
33+
export async function addTranslationsFromUrl(url: string) {
34+
// load translations async
35+
await fetch(url).then(async (response) => {
36+
const translations = await response.json();
37+
for (const translation of translations.elements) {
38+
const keyValueMap = translation.translations.reduce(
39+
(acc, curr) => {
40+
acc[curr.key] = curr.value;
41+
return acc;
42+
},
43+
{} as Record<string, string>
44+
);
45+
i18nNext.addResourceBundle(translation.language, "translation", keyValueMap, true, true);
46+
}
47+
translationsUpdated.set(true);
48+
});
2549
}
2650

2751
export function getI18n(): Readable<typeof i18nNext> {
@@ -37,8 +61,11 @@ export type InlineTranslation = {
3761

3862
export const currentLanguage = writable("de");
3963
i18nNext.on("languageChanged", (lng) => {
40-
currentLanguage.set(lng);
64+
const formattedLanguage = lng.split("-")[0];
65+
currentLanguage.set(formattedLanguage);
4166
});
67+
export const availableLanguages = writable(["de"]);
68+
export const translationsUpdated = writable(undefined);
4269

4370
export function translateInlineTranslation(
4471
translation: InlineTranslation | string,
@@ -63,3 +90,10 @@ export const t = derived(currentLanguage, (currentLanguage: string) => {
6390
return translateInlineTranslation(key, { language: currentLanguage });
6491
};
6592
});
93+
94+
export function isInlineTranslation(obj: InlineTranslation): obj is InlineTranslation {
95+
if (!(obj instanceof Object)) return false;
96+
for (const [key, value] of Object.entries(obj))
97+
if (typeof key !== "string" || typeof value !== "string") return false;
98+
return true;
99+
}

0 commit comments

Comments
 (0)