From e57dde7bb0bbdd1ce33f3c4dc7e9c2fb8e811b25 Mon Sep 17 00:00:00 2001 From: Nicolas Mowen Date: Tue, 29 Apr 2025 09:02:50 -0600 Subject: [PATCH] Support automatic language selection based on system language (#17953) * Support automatic language selection * Handle non-matching keys * Cleanup * Handle region specific language codes from browser * Fix passing in requestor --- frigate/embeddings/maintainer.py | 2 +- web/src/components/menu/GeneralSettings.tsx | 32 +++++++++++---------- web/src/context/language-provider.tsx | 32 +++++++++++++++------ web/src/lib/const.ts | 14 +++++++++ 4 files changed, 55 insertions(+), 25 deletions(-) create mode 100644 web/src/lib/const.ts diff --git a/frigate/embeddings/maintainer.py b/frigate/embeddings/maintainer.py index 07115ca94..d9363d1d4 100644 --- a/frigate/embeddings/maintainer.py +++ b/frigate/embeddings/maintainer.py @@ -120,7 +120,7 @@ class EmbeddingMaintainer(threading.Thread): if self.config.face_recognition.enabled: self.realtime_processors.append( FaceRealTimeProcessor( - self.config, self.event_metadata_publisher, metrics + self.config, self.requestor, self.event_metadata_publisher, metrics ) ) diff --git a/web/src/components/menu/GeneralSettings.tsx b/web/src/components/menu/GeneralSettings.tsx index f0c55f659..af5855815 100644 --- a/web/src/components/menu/GeneralSettings.tsx +++ b/web/src/components/menu/GeneralSettings.tsx @@ -34,7 +34,7 @@ import { useTheme, } from "@/context/theme-provider"; import { IoColorPalette } from "react-icons/io5"; -import { useState } from "react"; +import { useMemo, useState } from "react"; import { useRestart } from "@/api/ws"; import { Tooltip, @@ -62,6 +62,7 @@ import { toast } from "sonner"; import axios from "axios"; import { FrigateConfig } from "@/types/frigateConfig"; import { useTranslation } from "react-i18next"; +import { supportedLanguageKeys } from "@/lib/const"; type GeneralSettingsProps = { className?: string; @@ -75,20 +76,21 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) { // languages - const languages = [ - { code: "en", label: t("menu.language.en") }, - { code: "es", label: t("menu.language.es") }, - { code: "fr", label: t("menu.language.fr") }, - { code: "de", label: t("menu.language.de") }, - { code: "it", label: t("menu.language.it") }, - { code: "nl", label: t("menu.language.nl") }, - { code: "nb-NO", label: t("menu.language.nb") }, - { code: "tr", label: t("menu.language.tr") }, - { code: "pl", label: t("menu.language.pl") }, - { code: "zh-CN", label: t("menu.language.zhCN") }, - { code: "yue-Hant", label: t("menu.language.yue") }, - { code: "ru", label: t("menu.language.ru") }, - ]; + const languages = useMemo(() => { + // Handle language keys that aren't directly used for translation key + const specialKeyMap: { [key: string]: string } = { + "nb-NO": "nb", + "yue-Hant": "yue", + "zh-CN": "zhCN", + }; + + return supportedLanguageKeys.map((key) => { + return { + code: key, + label: t(`menu.language.${specialKeyMap[key] || key}`), + }; + }); + }, [t]); // settings diff --git a/web/src/context/language-provider.tsx b/web/src/context/language-provider.tsx index 4d935da97..88ade5032 100644 --- a/web/src/context/language-provider.tsx +++ b/web/src/context/language-provider.tsx @@ -1,15 +1,14 @@ import { createContext, useContext, useState, useEffect, useMemo } from "react"; import i18next from "i18next"; +import { supportedLanguageKeys } from "@/lib/const"; type LanguageProviderState = { language: string; - systemLanguage: string; setLanguage: (language: string) => void; }; const initialState: LanguageProviderState = { language: i18next.language || "en", - systemLanguage: "en", setLanguage: () => null, }; @@ -26,10 +25,31 @@ export function LanguageProvider({ defaultLanguage?: string; storageKey?: string; }) { + const systemLanguage = useMemo(() => { + if (typeof window === "undefined") return defaultLanguage; + + const systemLanguage = window.navigator.language; + + if (supportedLanguageKeys.includes(systemLanguage)) { + return systemLanguage; + } + + // browser languages may include a -REGION (ex: en-US) + if (systemLanguage.includes("-")) { + const shortenedSystemLanguage = systemLanguage.split("-")[0]; + + if (supportedLanguageKeys.includes(shortenedSystemLanguage)) { + return shortenedSystemLanguage; + } + } + + return defaultLanguage; + }, [defaultLanguage]); + const [language, setLanguage] = useState(() => { try { const storedData = localStorage.getItem(storageKey); - const newLanguage = storedData || defaultLanguage; + const newLanguage = storedData || systemLanguage; i18next.changeLanguage(newLanguage); return newLanguage; } catch (error) { @@ -39,11 +59,6 @@ export function LanguageProvider({ } }); - const systemLanguage = useMemo(() => { - if (typeof window === "undefined") return "en"; - return window.navigator.language; - }, []); - useEffect(() => { // set document lang for smart capitalization document.documentElement.lang = language; @@ -54,7 +69,6 @@ export function LanguageProvider({ const value = { language, - systemLanguage, setLanguage: (language: string) => { localStorage.setItem(storageKey, language); setLanguage(language); diff --git a/web/src/lib/const.ts b/web/src/lib/const.ts new file mode 100644 index 000000000..bd16d8c39 --- /dev/null +++ b/web/src/lib/const.ts @@ -0,0 +1,14 @@ +export const supportedLanguageKeys = [ + "en", + "es", + "fr", + "de", + "it", + "nl", + "nb-NO", + "tr", + "pl", + "zh-CN", + "yue-Hant", + "ru", +];