Advent of SvelteKit 2022
Happy Holidays!

Wednesday, December 25, 2024 is in 252 days

English

Challenge

Source code

+page.svelte

<script lang="ts">
	import LL, { locale } from '$i18n/i18n-svelte';
	import Tree from '~icons/twemoji/christmas-tree';
	import Map from '~icons/twemoji/world-map';
	import CarbonLanguage from '~icons/carbon/language';
	import USFlag from '~icons/twemoji/flag-united-states?raw';
	import GermanFlag from '~icons/twemoji/flag-germany?raw';
	import JapaneseFlag from '~icons/twemoji/flag-japan?raw';
	import WrapTranslation from './WrapTranslation.svelte';
	import { submitReplaceState } from '$lib/util';
	import type { PageData } from './$types';

	export let data: PageData;

	const flagMap = {
		en: USFlag,
		de: GermanFlag,
		'ja-JP': JapaneseFlag
	};

	const daysUntilChristmas = Math.floor(
		(data.target.getTime() - new Date().getTime()) / (1000 * 60 * 60 * 24)
	);
</script>

<section>
	<div class="header">
		<Tree />
		{$LL.happyHolidays()}
		<Map />
	</div>
	<p>
		<WrapTranslation
			message={$LL.christmasIsComing({
				date: data.target,
				time: $LL.day({ days: daysUntilChristmas })
			})}
			let:infix
		>
			<span class="days">{infix}</span>
		</WrapTranslation>
	</p>
	<form class="lang-picker" on:submit|preventDefault={submitReplaceState}>
		<input type="hidden" name="locale" value={data.nextLocale} />
		<button class="icon-button" aria-label="Change language"><CarbonLanguage /></button>
		<p class="lang">{@html flagMap[$locale]} {$LL.language()}</p>
	</form>
</section>

<style>
	section {
		display: flex;
		flex-direction: column;
		align-items: center;
		text-align: center;
		line-height: var(--font-lineheight-5);
	}

	.header {
		font-size: var(--font-size-5);
		display: flex;
		align-items: center;
		gap: 0.5rem;
	}
	.icon-button {
		width: 40px;
		height: 40px;
		border-radius: var(--radius-round);
		border: var(--border-size-1) solid black;
		cursor: pointer;
		transition: all 300ms;
		background-color: transparent;
		display: grid;
		place-items: center;
		padding: 0;
		color: black;
	}

	.icon-button:hover {
		border-color: var(--green-7);
		color: var(--green-6);
	}

	.days {
		color: var(--green-7);
	}

	.lang-picker {
		display: grid;
		grid-template-columns: 120px 120px;
		width: 100%;
		justify-content: center;
		justify-items: center;
	}

	.lang {
		display: flex;
		align-items: center;
		gap: 5px;
	}
</style>

+page.ts

import { loadLocaleAsync } from '$i18n/i18n-util.async';
import { setLocale } from '$i18n/i18n-svelte';

import type { PageLoad } from './$types';
import { baseLocale } from '$i18n/i18n-util.js';
import type { Locales } from '$i18n/i18n-types';

const locales: Locales[] = ['en', 'de', 'ja-JP'];

// based on https://github.com/ivanhofer/typesafe-i18n-demo-sveltekit example
// not using layout loads because I only need it at this route
export const load: PageLoad = async ({ data, url }) => {
	let locale = (url.searchParams.get('locale') as Locales) ?? baseLocale;
	if (!locales.includes(locale)) {
		locale = baseLocale;
	}
	const nextLocaleIdx = (locales.indexOf(locale) + 1) % locales.length;
	await loadLocaleAsync(locale);
	setLocale(locale);
	return {
		...data,
		locale,
		nextLocale: locales[nextLocaleIdx]
	};
};

WrapTranslation.svelte

<script lang="ts">
	// based on https://github.com/ivanhofer/typesafe-i18n#how-do-i-render-a-component-inside-a-translation
	import type { LocalizedString } from 'typesafe-i18n';
	export let message: LocalizedString;

	$: [prefix, infix, postfix] = message.split('<>') as LocalizedString[];

	// render infix only if the message doesn't have any split characters
	$: if (!infix && !postfix) {
		infix = prefix;
		prefix = '' as LocalizedString;
	}
</script>

{prefix}<slot {infix} />{postfix}