Fix language default in picker. Misc i18n fixes (#664)

* fix: fix language default in picker. Misc i18n fixes

* Update src/i18n/config.ts

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* PR fixes

* duplicate key

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Dan Ditomaso
2025-06-17 16:23:57 -04:00
committed by GitHub
parent c91e5e6b7b
commit a5339af0dd
6 changed files with 52 additions and 82 deletions

12
.githooks/_/pre-commit Executable file
View File

@@ -0,0 +1,12 @@
#!/bin/sh
if [ "$SKIP_SIMPLE_GIT_HOOKS" = "1" ]; then
echo "[INFO] SKIP_SIMPLE_GIT_HOOKS is set to 1, skipping hook."
exit 0
fi
if [ -f "$SIMPLE_GIT_HOOKS_RC" ]; then
. "$SIMPLE_GIT_HOOKS_RC"
fi
deno task lint:fix && deno task format

View File

@@ -16,15 +16,11 @@ interface LanguageSwitcherProps {
disableHover?: boolean;
}
export default function LanguageSwitcher(
{ disableHover = false }: LanguageSwitcherProps,
) {
export default function LanguageSwitcher({
disableHover = false,
}: LanguageSwitcherProps) {
const { i18n } = useTranslation("ui");
const { set: setLanguage } = useLang();
const currentLanguage =
supportedLanguages.find((lang) => lang.code === i18n.language) ||
supportedLanguages[0];
const { set: setLanguage, currentLanguage } = useLang();
const handleLanguageChange = async (languageCode: LangCode) => {
await setLanguage(languageCode, true);
@@ -65,7 +61,7 @@ export default function LanguageSwitcher(
"group-hover:text-gray-900 dark:group-hover:text-white",
)}
>
{currentLanguage.name}
{currentLanguage?.name}
</Subtle>
</Button>
</DropdownMenuTrigger>

View File

@@ -60,7 +60,7 @@ export function ErrorPage({ error }: { error: Error }) {
<div className="hidden md:block md:max-w-64 lg:max-w-80 w-full aspect-suqare">
<img
src="chirpy.svg"
src="/chirpy.svg"
alt="Chirpy the Meshtastic error"
className="max-w-full h-auto"
/>

View File

@@ -1,86 +1,55 @@
import { useCallback, useMemo } from "react";
import { useTranslation } from "react-i18next";
import { LangCode } from "@app/i18n/config.ts";
import {
FALLBACK_LANGUAGE_CODE,
Lang,
LangCode,
supportedLanguages,
} from "../../i18n/config.ts";
import useLocalStorage from "./useLocalStorage.ts";
/**
* Hook to set the i18n language
*
* @returns The `set` function
*/
const STORAGE_KEY = "language";
type LanguageState = {
language: string;
language: LangCode;
};
function useLang() {
const { i18n } = useTranslation();
const [_, setLanguage] = useLocalStorage<LanguageState | null>(
const [_, setLanguageInStorage] = useLocalStorage<LanguageState | null>(
STORAGE_KEY,
null,
);
const regionNames = useMemo(() => {
return new Intl.DisplayNames(i18n.language, {
type: "region",
fallback: "none",
style: "long",
});
const currentLanguage = useMemo((): Lang | undefined => {
const lang = supportedLanguages.find((l) => l.code === i18n.language);
if (lang) {
return lang;
}
return supportedLanguages.find((l) => l.code === FALLBACK_LANGUAGE_CODE);
}, [i18n.language]);
const collator = useMemo(() => {
return new Intl.Collator(i18n.language, {});
return new Intl.Collator(i18n.language, { sensitivity: "base" });
}, [i18n.language]);
/**
* Sets the i18n language.
*
* @param lng - The language tag to set
* @param persist - Whether to persist the language setting in local storage
*/
const set = useCallback(
async (lng: LangCode, persist = true) => {
if (i18n.language === lng) {
return;
}
try {
console.info("setting language:", lng);
if (persist) {
setLanguage({ language: lng });
setLanguageInStorage({ language: lng });
}
await i18n.changeLanguage(lng);
} catch (e) {
console.warn(e);
console.warn("Failed to change language:", e);
}
},
[i18n],
[i18n, setLanguageInStorage],
);
/**
* Get the localized country name
*
* @param code - Two-letter country code
*/
const getCountryName = useCallback(
(code: LangCode) => {
let name = null;
try {
name = regionNames.of(code);
} catch (e) {
console.warn(e);
}
return name;
},
[regionNames],
);
/**
* Compare two strings according to the sort order of the current language
*
* @param a - The first string to compare
* @param b - The second string to compare
*/
const compare = useCallback(
(a: string, b: string) => {
return collator.compare(a, b);
@@ -88,7 +57,7 @@ function useLang() {
[collator],
);
return { compare, set, getCountryName };
return { compare, set, currentLanguage };
}
export default useLang;

View File

@@ -7,32 +7,20 @@ export type Lang = {
code: Intl.Locale["language"];
name: string;
flag: string;
region?: Intl.Locale["region"];
};
export type LangCode = Lang["code"];
/**
* Generates a flag emoji from a two-letter country code.
* @param regionCode - The two-letter, uppercase country code (e.g., "US", "FI").
* @returns A string containing the flag emoji.
*/
function getFlagEmoji(regionCode: string): string {
const A_LETTER_CODE = 0x1F1E6;
const a_char_code = "A".charCodeAt(0);
const codePoints = regionCode
.toUpperCase()
.split("")
.map((char) => A_LETTER_CODE + char.charCodeAt(0) - a_char_code);
return String.fromCodePoint(...codePoints);
}
export const supportedLanguages: Lang[] = [
{ code: "de-DE", name: "Deutschland", flag: getFlagEmoji("DE") },
{ code: "en-US", name: "English", flag: getFlagEmoji("US") },
{ code: "fi-FI", name: "Suomi", flag: getFlagEmoji("FI") },
{ code: "sv-SE", name: "Svenska", flag: getFlagEmoji("SE") },
{ code: "de", name: "Deutsch", flag: "🇩🇪" },
{ code: "en", name: "English", flag: "🇺🇸" },
{ code: "fi", name: "Suomi", flag: "🇫🇮" },
{ code: "sv", name: "Svenska", flag: "🇸🇪" },
];
export const FALLBACK_LANGUAGE_CODE: LangCode = "en";
i18next
.use(Backend)
.use(initReactI18next)
@@ -50,7 +38,13 @@ i18next
order: ["localStorage", "navigator"],
caches: ["localStorage"],
},
fallbackLng: "en-US", // Default to US English if detection fails
fallbackLng: {
default: [FALLBACK_LANGUAGE_CODE],
"en-GB": [FALLBACK_LANGUAGE_CODE],
"fi": ["fi-FI"],
"sv": ["sv-SE"],
"de": ["de-DE"],
},
fallbackNS: ["common", "ui", "dialog"],
debug: import.meta.env.MODE === "development",
ns: [

View File

@@ -1,6 +1,5 @@
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import { VitePWA } from "vite-plugin-pwa";
import { viteStaticCopy } from "vite-plugin-static-copy";
import { execSync } from "node:child_process";
import process from "node:process";