Compare commits

..

No commits in common. "41585a9cd33d534111530383e58f88c9fdab9615" and "cf231feb8e5a7fc372df44ea7cc3e2ed5643d70a" have entirely different histories.

7 changed files with 77 additions and 95 deletions

21
hooks.server.ts Normal file
View file

@ -0,0 +1,21 @@
import type { Handle } from '@sveltejs/kit';
export const handle: Handle = async ({ event, resolve }) => {
// Correct origin behind Nginx
const host = event.request.headers.get('x-forwarded-host') ?? event.request.headers.get('host');
const proto = event.request.headers.get('x-forwarded-proto') ?? 'https';
const origin = `${proto}://${host}`;
// Example: force HTTPS (optional, Nginx should already do this)
if (proto === 'http') {
return new Response(null, {
status: 308,
headers: {
Location: origin + event.url.pathname + event.url.search
}
});
}
// Proceed with default behavior
return resolve(event);
};

View file

@ -1,7 +1,7 @@
{ {
"name": "embroidery-viewer", "name": "embroidery-viewer",
"private": true, "private": true,
"version": "2.1.0", "version": "0.0.1",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite dev", "dev": "vite dev",

View file

@ -12,7 +12,7 @@
"paypal.link": "Open Donation link", "paypal.link": "Open Donation link",
"seo.title": "💖 Donate Support Embroidery Viewer", "seo.title": "💖 Donate Support Embroidery Viewer",
"seo.description": "Help keep Embroidery Viewer free and improving by making a donation. Choose from Bitcoin, Monero, PayPal, or other secure options to support ongoing development and hosting.", "seo.description": "Help keep Embroidery Viewer free and improving by making a donation. Choose from Bitcoin, Monero, PayPal, or other secure options to support ongoing development and hosting.",
"seo.keywords": "donate embroidery viewer, support embroidery viewer, embroidery viewer donations, help embroidery viewer, fund embroidery viewer, bitcoin donation embroidery, monero donation embroidery, paypal donation embroidery", "keywords": "donate embroidery viewer, support embroidery viewer, embroidery viewer donations, help embroidery viewer, fund embroidery viewer, bitcoin donation embroidery, monero donation embroidery, paypal donation embroidery",
"url": "https://embroideryviewer.xyz/donate", "url": "https://embroideryviewer.xyz/donate",
"image": "https://embroideryviewer.xyz/og/donate.png" "image": "https://embroideryviewer.xyz/og/donate.png"
} }

View file

@ -127,10 +127,7 @@ export const {
} = new i18n(config); } = new i18n(config);
locale.subscribe(($locale) => { locale.subscribe(($locale) => {
if (typeof localStorage !== 'undefined' && $locale) { // if (typeof document !== 'undefined') {
const existing = localStorage.getItem('locale'); // document.cookie = `locale=${$locale}; path=/; SameSite=None; Secure`;
if (existing !== $locale) { // }
localStorage.setItem('locale', $locale);
}
}
}); });

View file

@ -1,51 +1,17 @@
import { browser } from '$app/environment'; import { setLocale, setRoute } from '$lib/translations';
import { loadTranslations, setLocale, setRoute } from '$lib/translations';
import { SUPPORTED_LOCALES } from '$lib/translations';
/** /**
* Type guard that checks if a string is a supported locale. * @typedef {Object} LayoutData
* @param {string | null} locale * @property {string} route
* @returns {locale is "en-US" | "pt-BR"} * @property {string} language
*/ */
function isSupportedLocale(locale) {
// @ts-ignore
return locale !== null && Object.values(SUPPORTED_LOCALES).includes(locale);
}
/** /** @type {import('@sveltejs/kit').Load<LayoutData>} */
* Client-side load function to initialize translations based on localStorage or server fallback. export const load = async ({ data }) => {
* const { route, language } = data ?? {};
* This function runs in the browser and:
* - Prioritizes locale from localStorage (if valid)
* - Falls back to the server-provided fallbackLanguage (from Accept-Language)
* - Initializes translations and routing
*
* @type {import('@sveltejs/kit').Load}
*/
export const load = async ({ data, url }) => {
/** @type {string} */
const route = url.pathname;
/** @type {"en-US" | "pt-BR"} */ if (route) await setRoute(route);
let language; if (language) await setLocale(language);
if (browser) { return data ?? {};
/**
* Locale stored in the browser, if any.
* @type {string | null}
*/
const stored = localStorage.getItem('locale');
if (isSupportedLocale(stored)) {
language = stored; // Type narrowed here
} else {
language = data?.fallbackLanguage ?? SUPPORTED_LOCALES.EN_US;
}
await loadTranslations(language, route);
await setLocale(language);
await setRoute(route);
}
return {};
}; };

View file

@ -1,44 +1,55 @@
import { parse } from 'accept-language-parser'; import { parse } from 'accept-language-parser';
import { loadTranslations, setLocale, setRoute } from '$lib/translations';
import { SUPPORTED_LOCALES } from '$lib/translations'; import { SUPPORTED_LOCALES } from '$lib/translations';
/** /**
* A Set of all supported locale codes for quick validation. * A set of all supported locale codes, used to validate and match against
* user preferences from cookies or Accept-Language headers. We're using a
* Set for better performance in lookup.
*
* Example values: "en-US", "pt-BR" * Example values: "en-US", "pt-BR"
* @type {Set<string>} * @type {Set<string>}
*/ */
const SUPPORTED_LOCALE_SET = new Set(Object.values(SUPPORTED_LOCALES)); const SUPPORTED_LOCALE_SET = new Set(Object.values(SUPPORTED_LOCALES));
/** /**
* Extracts the best matching locale from an Accept-Language HTTP header. * Returns a valid locale from cookies, or null if not valid/found.
* * @param {{ get: (cookies: string) => any; }} cookies
* @param {string | null} header - The Accept-Language header value.
* @returns {string | null} - A supported locale string like "en-US", or null if none matched.
*/ */
function localeFromHeader(header) { function localeFromCookies(cookies) {
if (!header) return null; const locale = cookies.get('locale');
const parsed = parse(header); return locale && SUPPORTED_LOCALE_SET.has(locale) ? locale : null;
for (const { code, region } of parsed) {
const locale = region ? `${code}-${region}` : code;
if (SUPPORTED_LOCALE_SET.has(locale)) return locale;
}
return null;
} }
/** /**
* Server-side load function that returns the initial route and fallback language. * Parses the Accept-Language header and returns the best matching locale.
* The language is inferred from the Accept-Language header. * @param {string | null | undefined} header
* `localStorage` will take precedence on the client.
*
* @type {import('@sveltejs/kit').ServerLoad}
*/ */
export async function load({ url, request }) { function localeFromHeader(header) {
/** @type {string} */ if (!header) return null;
const parsedLanguages = parse(header);
for (const { code, region } of parsedLanguages) {
const locale = region ? `${code}-${region}` : code;
if (SUPPORTED_LOCALE_SET.has(locale)) {
return locale;
}
}
return null;
}
/** @type {import('@sveltejs/kit').ServerLoad}*/
export async function load({ url, request, cookies }) {
// const cookieLocale = localeFromCookies(cookies);
// const headerLocale = localeFromHeader(request.headers.get('accept-language'));
// const language = cookieLocale || headerLocale || SUPPORTED_LOCALES.EN_US;
const language = SUPPORTED_LOCALES.EN_US;
const route = url.pathname; const route = url.pathname;
/** @type {string} */ await loadTranslations(language, route);
const fallbackLanguage = setLocale(language);
localeFromHeader(request.headers.get('accept-language')) || setRoute(route);
SUPPORTED_LOCALES.EN_US;
return { fallbackLanguage, route }; return { language, route };
} }

View file

@ -1,26 +1,13 @@
<script> <script>
import { browser } from '$app/environment';
import { onMount } from 'svelte';
import Header from '$lib/components/Header.svelte'; import Header from '$lib/components/Header.svelte';
import Footer from '$lib/components/Footer.svelte'; import Footer from '$lib/components/Footer.svelte';
let mounted = false;
if (browser) {
onMount(() => {
mounted = true;
});
}
</script> </script>
{#if mounted} <Header />
<Header /> <main>
<main> <slot />
<slot /> </main>
</main> <Footer />
<Footer />
{/if}
<style> <style>
main { main {