All files / apps/web/src/utils i18n.ts

90.69% Statements 39/43
100% Branches 21/21
91.66% Functions 11/12
90.47% Lines 38/42

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107                      332x 6103x   332x 6084x       5760x 5760x             5760x       332x 332x 6103x 12705x   6103x 6103x 6103x 19x   6084x     6084x 6084x 11834x 5566x     518x     518x           332x     6865x 6865x   6865x 6865x       6865x 6865x 6865x     91x 91x     6103x               6956x 6956x                   332x         2652x 2652x    
/// <reference types="vite/client" />
 
import { createIsomorphicFn } from "@tanstack/react-start";
import { parse } from "cookie";
import type { BackendModule, ParseKeys, ResourceKey } from "i18next";
 
import type { I18nContext } from "~app/utils/i18n";
import type { Language, Namespace } from "~app/utils/i18n-data";
import { baseLanguage, isLanguage } from "~app/utils/i18n-data";
import { LANGUAGE_STORE_NAME } from "~app/utils/store/language";
 
const getCookie = (headers: Headers | undefined) =>
	headers ? (headers.get("cookie") ?? "") : document.cookie;
 
const getHeaderLanguages = (headers: Headers | undefined) =>
	headers
		? (headers.get("accept-language") ?? "")
				.split(",")
				.map((lang) => {
					const [tag = "", q = "1"] = lang.trim().split(";q=");
					return {
						fullTag: tag.toLowerCase(),
						baseTag: tag.split("-")[0]?.toLowerCase() || "",
						q: Number(q),
					};
				})
				.toSorted((a, b) => b.q - a.q)
				.flatMap(({ fullTag, baseTag }) => [fullTag, baseTag])
		: window.navigator.languages;
 
type Strategy = "cookie" | "header" | "baseLocale";
const strategies: Strategy[] = ["cookie", "header", "baseLocale"];
export const getLanguageFromRequest = (headers: Headers | undefined) => {
	for (const strategy of strategies) {
		switch (strategy) {
			case "cookie": {
				const cookies = parse(getCookie(headers));
				const cookieLanguage = cookies[LANGUAGE_STORE_NAME] ?? "";
				if (isLanguage(cookieLanguage)) {
					return cookieLanguage;
				}
				break;
			}
			case "header": {
				const headerLanguages = getHeaderLanguages(headers);
				for (const headerLanguage of headerLanguages) {
					if (isLanguage(headerLanguage)) {
						return headerLanguage;
					}
				}
				break;
			}
			case "baseLocale":
				return baseLanguage;
		}
	}
	return baseLanguage;
};
 
const readBackend = createIsomorphicFn()
	.server(async (language: Language, namespace: Namespace) => {
		/* oxlint-disable import/no-nodejs-modules */
		const fs = await import("node:fs/promises");
		const url = await import("node:url");
		/* oxlint-enable import/no-nodejs-modules */
		const publicPath = "../../public";
		const jsonUrl = new url.URL(
			`${publicPath}/locales/${language}/${namespace}.json`,
			import.meta.url,
		);
		const fileContent = await fs.readFile(url.fileURLToPath(jsonUrl));
		const resource = JSON.parse(fileContent.toString("utf8")) as ResourceKey;
		return resource;
	})
	.client(async (language, namespace) => {
		const response = await fetch(`/locales/${language}/${namespace}.json`);
		return response.json() as Promise<ResourceKey>;
	});
 
export const getBackendModule = (): BackendModule => ({
	type: "backend",
	init: () => {
		/* empty */
	},
	// Improper types in i18next
	// oxlint-disable-next-line typescript/no-misused-promises typescript/strict-void-return
	read: async (language, namespace) => {
		try {
			return await readBackend(language as Language, namespace as Namespace);
		} catch (error) {
			// oxlint-disable-next-line no-console
			console.error(`Failed to load ${language}/${namespace} i18n translation`);
			throw error;
		}
	},
});
 
type TitleKey<T extends ParseKeys> = T extends `titles.${infer X}` ? X : never;
export const getTitle = (
	i18nContext: I18nContext,
	pageKey: TitleKey<ParseKeys>,
	params?: Record<string, unknown>,
) => {
	const t = i18nContext.getTranslation();
	return t("titles.template", { page: t(`titles.${pageKey}`, params) });
};