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:
Executable
+12
@@ -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
|
||||||
@@ -16,15 +16,11 @@ interface LanguageSwitcherProps {
|
|||||||
disableHover?: boolean;
|
disableHover?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function LanguageSwitcher(
|
export default function LanguageSwitcher({
|
||||||
{ disableHover = false }: LanguageSwitcherProps,
|
disableHover = false,
|
||||||
) {
|
}: LanguageSwitcherProps) {
|
||||||
const { i18n } = useTranslation("ui");
|
const { i18n } = useTranslation("ui");
|
||||||
const { set: setLanguage } = useLang();
|
const { set: setLanguage, currentLanguage } = useLang();
|
||||||
|
|
||||||
const currentLanguage =
|
|
||||||
supportedLanguages.find((lang) => lang.code === i18n.language) ||
|
|
||||||
supportedLanguages[0];
|
|
||||||
|
|
||||||
const handleLanguageChange = async (languageCode: LangCode) => {
|
const handleLanguageChange = async (languageCode: LangCode) => {
|
||||||
await setLanguage(languageCode, true);
|
await setLanguage(languageCode, true);
|
||||||
@@ -65,7 +61,7 @@ export default function LanguageSwitcher(
|
|||||||
"group-hover:text-gray-900 dark:group-hover:text-white",
|
"group-hover:text-gray-900 dark:group-hover:text-white",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{currentLanguage.name}
|
{currentLanguage?.name}
|
||||||
</Subtle>
|
</Subtle>
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
|
|||||||
@@ -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">
|
<div className="hidden md:block md:max-w-64 lg:max-w-80 w-full aspect-suqare">
|
||||||
<img
|
<img
|
||||||
src="chirpy.svg"
|
src="/chirpy.svg"
|
||||||
alt="Chirpy the Meshtastic error"
|
alt="Chirpy the Meshtastic error"
|
||||||
className="max-w-full h-auto"
|
className="max-w-full h-auto"
|
||||||
/>
|
/>
|
||||||
|
|||||||
+20
-51
@@ -1,86 +1,55 @@
|
|||||||
import { useCallback, useMemo } from "react";
|
import { useCallback, useMemo } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
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";
|
import useLocalStorage from "./useLocalStorage.ts";
|
||||||
|
|
||||||
/**
|
|
||||||
* Hook to set the i18n language
|
|
||||||
*
|
|
||||||
* @returns The `set` function
|
|
||||||
*/
|
|
||||||
const STORAGE_KEY = "language";
|
const STORAGE_KEY = "language";
|
||||||
|
|
||||||
type LanguageState = {
|
type LanguageState = {
|
||||||
language: string;
|
language: LangCode;
|
||||||
};
|
};
|
||||||
|
|
||||||
function useLang() {
|
function useLang() {
|
||||||
const { i18n } = useTranslation();
|
const { i18n } = useTranslation();
|
||||||
const [_, setLanguage] = useLocalStorage<LanguageState | null>(
|
const [_, setLanguageInStorage] = useLocalStorage<LanguageState | null>(
|
||||||
STORAGE_KEY,
|
STORAGE_KEY,
|
||||||
null,
|
null,
|
||||||
);
|
);
|
||||||
|
|
||||||
const regionNames = useMemo(() => {
|
const currentLanguage = useMemo((): Lang | undefined => {
|
||||||
return new Intl.DisplayNames(i18n.language, {
|
const lang = supportedLanguages.find((l) => l.code === i18n.language);
|
||||||
type: "region",
|
if (lang) {
|
||||||
fallback: "none",
|
return lang;
|
||||||
style: "long",
|
}
|
||||||
});
|
return supportedLanguages.find((l) => l.code === FALLBACK_LANGUAGE_CODE);
|
||||||
}, [i18n.language]);
|
}, [i18n.language]);
|
||||||
|
|
||||||
const collator = useMemo(() => {
|
const collator = useMemo(() => {
|
||||||
return new Intl.Collator(i18n.language, {});
|
return new Intl.Collator(i18n.language, { sensitivity: "base" });
|
||||||
}, [i18n.language]);
|
}, [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(
|
const set = useCallback(
|
||||||
async (lng: LangCode, persist = true) => {
|
async (lng: LangCode, persist = true) => {
|
||||||
if (i18n.language === lng) {
|
if (i18n.language === lng) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
console.info("setting language:", lng);
|
|
||||||
if (persist) {
|
if (persist) {
|
||||||
setLanguage({ language: lng });
|
setLanguageInStorage({ language: lng });
|
||||||
}
|
}
|
||||||
await i18n.changeLanguage(lng);
|
await i18n.changeLanguage(lng);
|
||||||
} catch (e) {
|
} 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(
|
const compare = useCallback(
|
||||||
(a: string, b: string) => {
|
(a: string, b: string) => {
|
||||||
return collator.compare(a, b);
|
return collator.compare(a, b);
|
||||||
@@ -88,7 +57,7 @@ function useLang() {
|
|||||||
[collator],
|
[collator],
|
||||||
);
|
);
|
||||||
|
|
||||||
return { compare, set, getCountryName };
|
return { compare, set, currentLanguage };
|
||||||
}
|
}
|
||||||
|
|
||||||
export default useLang;
|
export default useLang;
|
||||||
|
|||||||
+14
-20
@@ -7,32 +7,20 @@ export type Lang = {
|
|||||||
code: Intl.Locale["language"];
|
code: Intl.Locale["language"];
|
||||||
name: string;
|
name: string;
|
||||||
flag: string;
|
flag: string;
|
||||||
|
region?: Intl.Locale["region"];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type LangCode = Lang["code"];
|
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[] = [
|
export const supportedLanguages: Lang[] = [
|
||||||
{ code: "de-DE", name: "Deutschland", flag: getFlagEmoji("DE") },
|
{ code: "de", name: "Deutsch", flag: "🇩🇪" },
|
||||||
{ code: "en-US", name: "English", flag: getFlagEmoji("US") },
|
{ code: "en", name: "English", flag: "🇺🇸" },
|
||||||
{ code: "fi-FI", name: "Suomi", flag: getFlagEmoji("FI") },
|
{ code: "fi", name: "Suomi", flag: "🇫🇮" },
|
||||||
{ code: "sv-SE", name: "Svenska", flag: getFlagEmoji("SE") },
|
{ code: "sv", name: "Svenska", flag: "🇸🇪" },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export const FALLBACK_LANGUAGE_CODE: LangCode = "en";
|
||||||
|
|
||||||
i18next
|
i18next
|
||||||
.use(Backend)
|
.use(Backend)
|
||||||
.use(initReactI18next)
|
.use(initReactI18next)
|
||||||
@@ -50,7 +38,13 @@ i18next
|
|||||||
order: ["localStorage", "navigator"],
|
order: ["localStorage", "navigator"],
|
||||||
caches: ["localStorage"],
|
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"],
|
fallbackNS: ["common", "ui", "dialog"],
|
||||||
debug: import.meta.env.MODE === "development",
|
debug: import.meta.env.MODE === "development",
|
||||||
ns: [
|
ns: [
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { defineConfig } from "vite";
|
import { defineConfig } from "vite";
|
||||||
import react from "@vitejs/plugin-react";
|
import react from "@vitejs/plugin-react";
|
||||||
import { VitePWA } from "vite-plugin-pwa";
|
|
||||||
import { viteStaticCopy } from "vite-plugin-static-copy";
|
import { viteStaticCopy } from "vite-plugin-static-copy";
|
||||||
import { execSync } from "node:child_process";
|
import { execSync } from "node:child_process";
|
||||||
import process from "node:process";
|
import process from "node:process";
|
||||||
|
|||||||
Reference in New Issue
Block a user