Happy Holidays!
Thursday, December 25, 2025 is in 338 days
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}