From 514034d184d1654518295e847da3183cc7da2c91 Mon Sep 17 00:00:00 2001 From: Meier Lukas Date: Thu, 2 Oct 2025 19:54:08 +0200 Subject: [PATCH] feat(translations): add crowdin live support (#4177) --- apps/nextjs/src/app/[locale]/layout.tsx | 6 +- .../language/language-combobox.module.css | 3 - .../components/language/language-combobox.tsx | 5 +- .../layout/crowdin-live-translation.tsx | 17 ++++ .../src/modes/command/children/language.tsx | 3 +- packages/translation/src/config.ts | 96 ++++++++++++------- packages/ui/src/components/index.tsx | 1 + packages/ui/src/components/language-icon.tsx | 11 +++ 8 files changed, 100 insertions(+), 42 deletions(-) delete mode 100644 apps/nextjs/src/components/language/language-combobox.module.css create mode 100644 apps/nextjs/src/components/layout/crowdin-live-translation.tsx create mode 100644 packages/ui/src/components/language-icon.tsx 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 Language icon; +};