diff --git a/apps/nextjs/src/app/[locale]/layout.tsx b/apps/nextjs/src/app/[locale]/layout.tsx
index afb773f72..3ee6daf89 100644
--- a/apps/nextjs/src/app/[locale]/layout.tsx
+++ b/apps/nextjs/src/app/[locale]/layout.tsx
@@ -23,6 +23,7 @@ import type { SupportedLanguage } from "@homarr/translation";
import { isLocaleRTL, isLocaleSupported } from "@homarr/translation";
import { Analytics } from "~/components/layout/analytics";
+import { CrowdinLiveTranslation } from "~/components/layout/crowdin-live-translation";
import { SearchEngineOptimization } from "~/components/layout/search-engine-optimization";
import { getCurrentColorSchemeAsync } from "~/theme/color-scheme";
import { DayJsLoader } from "./_client-providers/dayjs-loader";
@@ -118,10 +119,12 @@ export default async function Layout(props: {
(innerProps) => ,
]);
+ const { locale } = await props.params;
+
return (
// Instead of ColorSchemScript we use data-mantine-color-scheme to prevent flickering
+
diff --git a/apps/nextjs/src/components/language/language-combobox.module.css b/apps/nextjs/src/components/language/language-combobox.module.css
deleted file mode 100644
index fe98524ad..000000000
--- a/apps/nextjs/src/components/language/language-combobox.module.css
+++ /dev/null
@@ -1,3 +0,0 @@
-.flagIcon {
- border-radius: 4px;
-}
diff --git a/apps/nextjs/src/components/language/language-combobox.tsx b/apps/nextjs/src/components/language/language-combobox.tsx
index 80ea2549b..6f4b39b33 100644
--- a/apps/nextjs/src/components/language/language-combobox.tsx
+++ b/apps/nextjs/src/components/language/language-combobox.tsx
@@ -6,8 +6,7 @@ import { IconCheck } from "@tabler/icons-react";
import type { SupportedLanguage } from "@homarr/translation";
import { localeConfigurations, supportedLanguages } from "@homarr/translation";
-
-import classes from "./language-combobox.module.css";
+import { LanguageIcon } from "@homarr/ui";
import "flag-icons/css/flag-icons.min.css";
@@ -84,7 +83,7 @@ const OptionItem = ({
return (
-
+
{localeConfigurations[localeKey].name}
diff --git a/apps/nextjs/src/components/layout/crowdin-live-translation.tsx b/apps/nextjs/src/components/layout/crowdin-live-translation.tsx
new file mode 100644
index 000000000..93b4a9571
--- /dev/null
+++ b/apps/nextjs/src/components/layout/crowdin-live-translation.tsx
@@ -0,0 +1,17 @@
+import Script from "next/script";
+
+import type { SupportedLanguage } from "@homarr/translation";
+
+export const CrowdinLiveTranslation = (props: { locale: SupportedLanguage }) => {
+ if (props.locale !== "cr") return null;
+
+ return (
+ <>
+
+
+
+ >
+ );
+};
diff --git a/packages/spotlight/src/modes/command/children/language.tsx b/packages/spotlight/src/modes/command/children/language.tsx
index f861746dd..6178872b8 100644
--- a/packages/spotlight/src/modes/command/children/language.tsx
+++ b/packages/spotlight/src/modes/command/children/language.tsx
@@ -3,6 +3,7 @@ import { IconCheck } from "@tabler/icons-react";
import { localeConfigurations, supportedLanguages } from "@homarr/translation";
import { useChangeLocale, useCurrentLocale, useI18n } from "@homarr/translation/client";
+import { LanguageIcon } from "@homarr/ui";
import { createChildrenOptions } from "../../../lib/children";
@@ -34,7 +35,7 @@ export const languageChildrenOptions = createChildrenOptions
-
+
{configuration.name}
diff --git a/packages/translation/src/config.ts b/packages/translation/src/config.ts
index e1bc750c2..fafbc1f61 100644
--- a/packages/translation/src/config.ts
+++ b/packages/translation/src/config.ts
@@ -6,7 +6,7 @@ export const localeConfigurations = {
ca: {
name: "Català",
translatedName: "Catalan",
- flagIcon: "es-ct",
+ icon: flagIcon("es-ct"),
importMrtLocalization() {
return import("./mantine-react-table/ca.json");
},
@@ -17,7 +17,7 @@ export const localeConfigurations = {
cn: {
name: "中文",
translatedName: "Chinese (Simplified)",
- flagIcon: "cn",
+ icon: flagIcon("cn"),
importMrtLocalization() {
return import("mantine-react-table/locales/zh-Hans/index.esm.mjs").then(
(module) => module.MRT_Localization_ZH_HANS,
@@ -27,10 +27,24 @@ export const localeConfigurations = {
return import("dayjs/locale/zh-cn").then((module) => module.default);
},
},
+ cr: {
+ name: "Crowdin",
+ translatedName: "Live translation",
+ icon: {
+ type: "custom" as const,
+ url: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/crowdin.svg",
+ },
+ importMrtLocalization() {
+ return import("mantine-react-table/locales/en/index.esm.mjs").then((module) => module.MRT_Localization_EN);
+ },
+ importDayJsLocale() {
+ return import("dayjs/locale/en-gb").then((module) => module.default);
+ },
+ },
cs: {
name: "Čeština",
translatedName: "Czech",
- flagIcon: "cz",
+ icon: flagIcon("cz"),
importMrtLocalization() {
return import("mantine-react-table/locales/cs/index.esm.mjs").then((module) => module.MRT_Localization_CS);
},
@@ -41,7 +55,7 @@ export const localeConfigurations = {
da: {
name: "Dansk",
translatedName: "Danish",
- flagIcon: "dk",
+ icon: flagIcon("dk"),
importMrtLocalization() {
return import("mantine-react-table/locales/da/index.esm.mjs").then((module) => module.MRT_Localization_DA);
},
@@ -52,7 +66,7 @@ export const localeConfigurations = {
de: {
name: "Deutsch",
translatedName: "German",
- flagIcon: "de",
+ icon: flagIcon("de"),
importMrtLocalization() {
return import("mantine-react-table/locales/de/index.esm.mjs").then((module) => module.MRT_Localization_DE);
},
@@ -63,7 +77,7 @@ export const localeConfigurations = {
"de-CH": {
name: "Deutsch (Schweiz)",
translatedName: "German (Swiss)",
- flagIcon: "ch",
+ icon: flagIcon("ch"),
importMrtLocalization() {
return import("mantine-react-table/locales/de/index.esm.mjs").then((module) => module.MRT_Localization_DE);
},
@@ -74,7 +88,7 @@ export const localeConfigurations = {
"en-gb": {
name: "English (UK)",
translatedName: "English (UK)",
- flagIcon: "gb",
+ icon: flagIcon("gb"),
importMrtLocalization() {
return import("mantine-react-table/locales/en/index.esm.mjs").then((module) => module.MRT_Localization_EN);
},
@@ -85,7 +99,7 @@ export const localeConfigurations = {
en: {
name: "English (US)",
translatedName: "English (US)",
- flagIcon: "us",
+ icon: flagIcon("us"),
importMrtLocalization() {
return import("mantine-react-table/locales/en/index.esm.mjs").then((module) => module.MRT_Localization_EN);
},
@@ -96,7 +110,7 @@ export const localeConfigurations = {
el: {
name: "Ελληνικά",
translatedName: "Greek",
- flagIcon: "gr",
+ icon: flagIcon("gr"),
importMrtLocalization() {
return import("mantine-react-table/locales/el/index.esm.mjs").then((module) => module.MRT_Localization_EL);
},
@@ -107,7 +121,7 @@ export const localeConfigurations = {
es: {
name: "Español",
translatedName: "Spanish",
- flagIcon: "es",
+ icon: flagIcon("es"),
importMrtLocalization() {
return import("mantine-react-table/locales/es/index.esm.mjs").then((module) => module.MRT_Localization_ES);
},
@@ -118,7 +132,7 @@ export const localeConfigurations = {
et: {
name: "Eesti",
translatedName: "Estonian",
- flagIcon: "ee",
+ icon: flagIcon("ee"),
importMrtLocalization() {
return import("mantine-react-table/locales/et/index.esm.mjs").then((module) => module.MRT_Localization_ET);
},
@@ -129,7 +143,7 @@ export const localeConfigurations = {
fr: {
name: "Français",
translatedName: "French",
- flagIcon: "fr",
+ icon: flagIcon("fr"),
importMrtLocalization() {
return import("mantine-react-table/locales/fr/index.esm.mjs").then((module) => module.MRT_Localization_FR);
},
@@ -140,7 +154,7 @@ export const localeConfigurations = {
he: {
name: "עברית",
translatedName: "Hebrew",
- flagIcon: "il",
+ icon: flagIcon("il"),
isRTL: true,
importMrtLocalization() {
return import("mantine-react-table/locales/he/index.esm.mjs").then((module) => module.MRT_Localization_HE);
@@ -152,7 +166,7 @@ export const localeConfigurations = {
hr: {
name: "Hrvatski",
translatedName: "Croatian",
- flagIcon: "hr",
+ icon: flagIcon("hr"),
importMrtLocalization() {
return import("mantine-react-table/locales/hr/index.esm.mjs").then((module) => module.MRT_Localization_HR);
},
@@ -163,7 +177,7 @@ export const localeConfigurations = {
hu: {
name: "Magyar",
translatedName: "Hungarian",
- flagIcon: "hu",
+ icon: flagIcon("hu"),
importMrtLocalization() {
return import("mantine-react-table/locales/hu/index.esm.mjs").then((module) => module.MRT_Localization_HU);
},
@@ -174,7 +188,7 @@ export const localeConfigurations = {
it: {
name: "Italiano",
translatedName: "Italian",
- flagIcon: "it",
+ icon: flagIcon("it"),
importMrtLocalization() {
return import("mantine-react-table/locales/it/index.esm.mjs").then((module) => module.MRT_Localization_IT);
},
@@ -185,7 +199,7 @@ export const localeConfigurations = {
ja: {
name: "日本語",
translatedName: "Japanese",
- flagIcon: "jp",
+ icon: flagIcon("jp"),
importMrtLocalization() {
return import("mantine-react-table/locales/ja/index.esm.mjs").then((module) => module.MRT_Localization_JA);
},
@@ -196,7 +210,7 @@ export const localeConfigurations = {
ko: {
name: "한국어",
translatedName: "Korean",
- flagIcon: "kr",
+ icon: flagIcon("kr"),
importMrtLocalization() {
return import("mantine-react-table/locales/ko/index.esm.mjs").then((module) => module.MRT_Localization_KO);
},
@@ -207,7 +221,7 @@ export const localeConfigurations = {
lt: {
name: "Lietuvių",
translatedName: "Lithuanian",
- flagIcon: "lt",
+ icon: flagIcon("lt"),
importMrtLocalization() {
return import("./mantine-react-table/lt.json");
},
@@ -218,7 +232,7 @@ export const localeConfigurations = {
lv: {
name: "Latviešu",
translatedName: "Latvian",
- flagIcon: "lv",
+ icon: flagIcon("lv"),
importMrtLocalization() {
return import("./mantine-react-table/lv.json");
},
@@ -229,7 +243,7 @@ export const localeConfigurations = {
nl: {
name: "Nederlands",
translatedName: "Dutch",
- flagIcon: "nl",
+ icon: flagIcon("nl"),
importMrtLocalization() {
return import("mantine-react-table/locales/nl/index.esm.mjs").then((module) => module.MRT_Localization_NL);
},
@@ -240,7 +254,7 @@ export const localeConfigurations = {
no: {
name: "Norsk",
translatedName: "Norwegian",
- flagIcon: "no",
+ icon: flagIcon("no"),
importMrtLocalization() {
return import("mantine-react-table/locales/no/index.esm.mjs").then((module) => module.MRT_Localization_NO);
},
@@ -251,7 +265,7 @@ export const localeConfigurations = {
pl: {
name: "Polski",
translatedName: "Polish",
- flagIcon: "pl",
+ icon: flagIcon("pl"),
importMrtLocalization() {
return import("mantine-react-table/locales/pl/index.esm.mjs").then((module) => module.MRT_Localization_PL);
},
@@ -262,7 +276,7 @@ export const localeConfigurations = {
pt: {
name: "Português",
translatedName: "Portuguese",
- flagIcon: "pt",
+ icon: flagIcon("pt"),
importMrtLocalization() {
return import("mantine-react-table/locales/pt/index.esm.mjs").then((module) => module.MRT_Localization_PT);
},
@@ -273,7 +287,7 @@ export const localeConfigurations = {
ro: {
name: "Românesc",
translatedName: "Romanian",
- flagIcon: "ro",
+ icon: flagIcon("ro"),
importMrtLocalization() {
return import("mantine-react-table/locales/ro/index.esm.mjs").then((module) => module.MRT_Localization_RO);
},
@@ -284,7 +298,7 @@ export const localeConfigurations = {
ru: {
name: "Русский",
translatedName: "Russian",
- flagIcon: "ru",
+ icon: flagIcon("ru"),
importMrtLocalization() {
return import("mantine-react-table/locales/ru/index.esm.mjs").then((module) => module.MRT_Localization_RU);
},
@@ -295,7 +309,7 @@ export const localeConfigurations = {
sk: {
name: "Slovenčina",
translatedName: "Slovak",
- flagIcon: "sk",
+ icon: flagIcon("sk"),
importMrtLocalization() {
return import("mantine-react-table/locales/sk/index.esm.mjs").then((module) => module.MRT_Localization_SK);
},
@@ -306,7 +320,7 @@ export const localeConfigurations = {
sl: {
name: "Slovenščina",
translatedName: "Slovenian",
- flagIcon: "si",
+ icon: flagIcon("si"),
importMrtLocalization() {
return import("./mantine-react-table/sl.json");
},
@@ -317,7 +331,7 @@ export const localeConfigurations = {
sv: {
name: "Svenska",
translatedName: "Swedish",
- flagIcon: "se",
+ icon: flagIcon("se"),
importMrtLocalization() {
return import("mantine-react-table/locales/sv/index.esm.mjs").then((module) => module.MRT_Localization_SV);
},
@@ -328,7 +342,7 @@ export const localeConfigurations = {
tr: {
name: "Türkçe",
translatedName: "Turkish",
- flagIcon: "tr",
+ icon: flagIcon("tr"),
importMrtLocalization() {
return import("mantine-react-table/locales/tr/index.esm.mjs").then((module) => module.MRT_Localization_TR);
},
@@ -339,7 +353,7 @@ export const localeConfigurations = {
zh: {
name: "中文",
translatedName: "Chinese (Traditional)",
- flagIcon: "tw",
+ icon: flagIcon("tw"),
importMrtLocalization() {
return import("mantine-react-table/locales/zh-Hant/index.esm.mjs").then(
(module) => module.MRT_Localization_ZH_HANT,
@@ -352,7 +366,7 @@ export const localeConfigurations = {
uk: {
name: "Українська",
translatedName: "Ukrainian",
- flagIcon: "ua",
+ icon: flagIcon("ua"),
importMrtLocalization() {
return import("mantine-react-table/locales/uk/index.esm.mjs").then((module) => module.MRT_Localization_UK);
},
@@ -363,7 +377,7 @@ export const localeConfigurations = {
vi: {
name: "Tiếng Việt",
translatedName: "Vietnamese",
- flagIcon: "vn",
+ icon: flagIcon("vn"),
importMrtLocalization() {
return import("mantine-react-table/locales/vi/index.esm.mjs").then((module) => module.MRT_Localization_VI);
},
@@ -376,13 +390,27 @@ export const localeConfigurations = {
{
name: string;
translatedName: string;
- flagIcon: string;
+ icon: LanguageIconDefinition;
importMrtLocalization: () => Promise;
importDayJsLocale: () => Promise;
isRTL?: boolean;
}
>;
+function flagIcon(flag: TCode) {
+ return { type: "flag" as const, flag };
+}
+
+export type LanguageIconDefinition =
+ | {
+ type: "flag";
+ flag: string;
+ }
+ | {
+ type: "custom";
+ url: string;
+ };
+
export const supportedLanguages = objectKeys(localeConfigurations);
export type SupportedLanguage = (typeof supportedLanguages)[number];
diff --git a/packages/ui/src/components/index.tsx b/packages/ui/src/components/index.tsx
index d54afd905..ff7201e28 100644
--- a/packages/ui/src/components/index.tsx
+++ b/packages/ui/src/components/index.tsx
@@ -14,3 +14,4 @@ export { IntegrationAvatar } from "./integration-avatar";
export { BetaBadge } from "./beta-badge";
export { MaskedImage } from "./masked-image";
export { MaskedOrNormalImage } from "./masked-or-normal-image";
+export { LanguageIcon } from "./language-icon";
diff --git a/packages/ui/src/components/language-icon.tsx b/packages/ui/src/components/language-icon.tsx
new file mode 100644
index 000000000..87cad5d64
--- /dev/null
+++ b/packages/ui/src/components/language-icon.tsx
@@ -0,0 +1,11 @@
+import { Image } from "@mantine/core";
+
+import type { LanguageIconDefinition } from "@homarr/translation";
+
+export const LanguageIcon = ({ icon }: { icon: LanguageIconDefinition }) => {
+ if (icon.type === "flag") {
+ return ;
+ }
+
+ return ;
+};