Replace entire codebase with homarr-labs/homarr

This commit is contained in:
Thomas Camlong
2026-01-15 21:54:44 +01:00
parent c5bc3b1559
commit 4fdd1fe351
4666 changed files with 409577 additions and 147434 deletions

View File

@@ -0,0 +1,4 @@
import baseConfig from "@homarr/eslint-config/base";
/** @type {import('typescript-eslint').Config} */
export default [...baseConfig];

View File

@@ -0,0 +1 @@
export * from "./src";

View File

@@ -0,0 +1,47 @@
{
"name": "@homarr/translation",
"version": "0.1.0",
"private": true,
"license": "Apache-2.0",
"type": "module",
"exports": {
".": "./index.ts",
"./client": "./src/client/index.ts",
"./server": "./src/server.ts",
"./middleware": "./src/middleware.ts",
"./request": "./src/request.ts",
"./dayjs": "./src/dayjs.ts"
},
"typesVersions": {
"*": {
"*": [
"src/*"
]
}
},
"scripts": {
"clean": "rm -rf .turbo node_modules",
"format": "prettier --check . --ignore-path ../../.gitignore",
"lint": "eslint",
"typecheck": "tsc --noEmit"
},
"prettier": "@homarr/prettier-config",
"dependencies": {
"@homarr/common": "workspace:^0.1.0",
"@homarr/definitions": "workspace:^0.1.0",
"dayjs": "^1.11.19",
"deepmerge": "4.3.1",
"mantine-react-table": "2.0.0-beta.9",
"next": "16.1.1",
"next-intl": "4.7.0",
"react": "19.2.3",
"react-dom": "19.2.3"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.39.2",
"typescript": "^5.9.3"
}
}

View File

@@ -0,0 +1,27 @@
"use client";
import { useMessages, useTranslations } from "next-intl";
import type { SupportedLanguage } from "../config";
import type englishTranslation from "../lang/en.json";
export { useChangeLocale } from "./use-change-locale";
export { useCurrentLocale } from "./use-current-locale";
declare module "next-intl" {
interface AppConfig {
Messages: typeof englishTranslation;
Locale: SupportedLanguage;
}
}
export const { useI18n, useScopedI18n } = {
useI18n: useTranslations,
useScopedI18n: useTranslations,
};
export const { useI18nMessages } = {
useI18nMessages: () => useMessages(),
};
export { useTranslations };

View File

@@ -0,0 +1,25 @@
import { useTransition } from "react";
import { usePathname, useRouter } from "next/navigation";
import type { SupportedLanguage } from "../config";
import { useCurrentLocale } from "./use-current-locale";
export const useChangeLocale = () => {
const currentLocale = useCurrentLocale();
const router = useRouter();
const pathname = usePathname();
const [isPending, startTransition] = useTransition();
return {
changeLocale: (newLocale: SupportedLanguage) => {
if (newLocale === currentLocale) {
return;
}
startTransition(() => {
router.replace(`/${newLocale}/${pathname}`);
});
},
isPending,
};
};

View File

@@ -0,0 +1,3 @@
import { useLocale } from "next-intl";
export const useCurrentLocale = useLocale;

View File

@@ -0,0 +1,420 @@
import type { MRT_Localization } from "mantine-react-table";
import { objectKeys } from "@homarr/common";
export const localeConfigurations = {
ca: {
name: "Català",
translatedName: "Catalan",
icon: flagIcon("es-ct"),
importMrtLocalization() {
return import("./mantine-react-table/ca.json");
},
importDayJsLocale() {
return import("dayjs/locale/ca").then((module) => module.default);
},
},
cn: {
name: "中文",
translatedName: "Chinese (Simplified)",
icon: flagIcon("cn"),
importMrtLocalization() {
return import("mantine-react-table/locales/zh-Hans/index.esm.mjs").then(
(module) => module.MRT_Localization_ZH_HANS,
);
},
importDayJsLocale() {
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",
icon: flagIcon("cz"),
importMrtLocalization() {
return import("mantine-react-table/locales/cs/index.esm.mjs").then((module) => module.MRT_Localization_CS);
},
importDayJsLocale() {
return import("dayjs/locale/cs").then((module) => module.default);
},
},
da: {
name: "Dansk",
translatedName: "Danish",
icon: flagIcon("dk"),
importMrtLocalization() {
return import("mantine-react-table/locales/da/index.esm.mjs").then((module) => module.MRT_Localization_DA);
},
importDayJsLocale() {
return import("dayjs/locale/da").then((module) => module.default);
},
},
de: {
name: "Deutsch",
translatedName: "German",
icon: flagIcon("de"),
importMrtLocalization() {
return import("mantine-react-table/locales/de/index.esm.mjs").then((module) => module.MRT_Localization_DE);
},
importDayJsLocale() {
return import("dayjs/locale/de").then((module) => module.default);
},
},
"de-CH": {
name: "Deutsch (Schweiz)",
translatedName: "German (Swiss)",
icon: flagIcon("ch"),
importMrtLocalization() {
return import("mantine-react-table/locales/de/index.esm.mjs").then((module) => module.MRT_Localization_DE);
},
importDayJsLocale() {
return import("dayjs/locale/de-ch").then((module) => module.default);
},
},
"en-gb": {
name: "English (UK)",
translatedName: "English (UK)",
icon: flagIcon("gb"),
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);
},
},
en: {
name: "English (US)",
translatedName: "English (US)",
icon: flagIcon("us"),
importMrtLocalization() {
return import("mantine-react-table/locales/en/index.esm.mjs").then((module) => module.MRT_Localization_EN);
},
importDayJsLocale() {
return import("dayjs/locale/en").then((module) => module.default);
},
},
el: {
name: "Ελληνικά",
translatedName: "Greek",
icon: flagIcon("gr"),
importMrtLocalization() {
return import("mantine-react-table/locales/el/index.esm.mjs").then((module) => module.MRT_Localization_EL);
},
importDayJsLocale() {
return import("dayjs/locale/el").then((module) => module.default);
},
},
es: {
name: "Español",
translatedName: "Spanish",
icon: flagIcon("es"),
importMrtLocalization() {
return import("mantine-react-table/locales/es/index.esm.mjs").then((module) => module.MRT_Localization_ES);
},
importDayJsLocale() {
return import("dayjs/locale/es").then((module) => module.default);
},
},
et: {
name: "Eesti",
translatedName: "Estonian",
icon: flagIcon("ee"),
importMrtLocalization() {
return import("mantine-react-table/locales/et/index.esm.mjs").then((module) => module.MRT_Localization_ET);
},
importDayJsLocale() {
return import("dayjs/locale/et").then((module) => module.default);
},
},
fr: {
name: "Français",
translatedName: "French",
icon: flagIcon("fr"),
importMrtLocalization() {
return import("mantine-react-table/locales/fr/index.esm.mjs").then((module) => module.MRT_Localization_FR);
},
importDayJsLocale() {
return import("dayjs/locale/fr").then((module) => module.default);
},
},
he: {
name: "עברית",
translatedName: "Hebrew",
icon: flagIcon("il"),
isRTL: true,
importMrtLocalization() {
return import("mantine-react-table/locales/he/index.esm.mjs").then((module) => module.MRT_Localization_HE);
},
importDayJsLocale() {
return import("dayjs/locale/he").then((module) => module.default);
},
},
hr: {
name: "Hrvatski",
translatedName: "Croatian",
icon: flagIcon("hr"),
importMrtLocalization() {
return import("mantine-react-table/locales/hr/index.esm.mjs").then((module) => module.MRT_Localization_HR);
},
importDayJsLocale() {
return import("dayjs/locale/hr").then((module) => module.default);
},
},
hu: {
name: "Magyar",
translatedName: "Hungarian",
icon: flagIcon("hu"),
importMrtLocalization() {
return import("mantine-react-table/locales/hu/index.esm.mjs").then((module) => module.MRT_Localization_HU);
},
importDayJsLocale() {
return import("dayjs/locale/hu").then((module) => module.default);
},
},
it: {
name: "Italiano",
translatedName: "Italian",
icon: flagIcon("it"),
importMrtLocalization() {
return import("mantine-react-table/locales/it/index.esm.mjs").then((module) => module.MRT_Localization_IT);
},
importDayJsLocale() {
return import("dayjs/locale/it").then((module) => module.default);
},
},
ja: {
name: "日本語",
translatedName: "Japanese",
icon: flagIcon("jp"),
importMrtLocalization() {
return import("mantine-react-table/locales/ja/index.esm.mjs").then((module) => module.MRT_Localization_JA);
},
importDayJsLocale() {
return import("dayjs/locale/ja").then((module) => module.default);
},
},
ko: {
name: "한국어",
translatedName: "Korean",
icon: flagIcon("kr"),
importMrtLocalization() {
return import("mantine-react-table/locales/ko/index.esm.mjs").then((module) => module.MRT_Localization_KO);
},
importDayJsLocale() {
return import("dayjs/locale/ko").then((module) => module.default);
},
},
lt: {
name: "Lietuvių",
translatedName: "Lithuanian",
icon: flagIcon("lt"),
importMrtLocalization() {
return import("./mantine-react-table/lt.json");
},
importDayJsLocale() {
return import("dayjs/locale/lt").then((module) => module.default);
},
},
lv: {
name: "Latviešu",
translatedName: "Latvian",
icon: flagIcon("lv"),
importMrtLocalization() {
return import("./mantine-react-table/lv.json");
},
importDayJsLocale() {
return import("dayjs/locale/lv").then((module) => module.default);
},
},
nl: {
name: "Nederlands",
translatedName: "Dutch",
icon: flagIcon("nl"),
importMrtLocalization() {
return import("mantine-react-table/locales/nl/index.esm.mjs").then((module) => module.MRT_Localization_NL);
},
importDayJsLocale() {
return import("dayjs/locale/nl").then((module) => module.default);
},
},
no: {
name: "Norsk",
translatedName: "Norwegian",
icon: flagIcon("no"),
importMrtLocalization() {
return import("mantine-react-table/locales/no/index.esm.mjs").then((module) => module.MRT_Localization_NO);
},
importDayJsLocale() {
return import("dayjs/locale/nb").then((module) => module.default);
},
},
pl: {
name: "Polski",
translatedName: "Polish",
icon: flagIcon("pl"),
importMrtLocalization() {
return import("mantine-react-table/locales/pl/index.esm.mjs").then((module) => module.MRT_Localization_PL);
},
importDayJsLocale() {
return import("dayjs/locale/pl").then((module) => module.default);
},
},
pt: {
name: "Português",
translatedName: "Portuguese",
icon: flagIcon("pt"),
importMrtLocalization() {
return import("mantine-react-table/locales/pt/index.esm.mjs").then((module) => module.MRT_Localization_PT);
},
importDayJsLocale() {
return import("dayjs/locale/pt").then((module) => module.default);
},
},
ro: {
name: "Românesc",
translatedName: "Romanian",
icon: flagIcon("ro"),
importMrtLocalization() {
return import("mantine-react-table/locales/ro/index.esm.mjs").then((module) => module.MRT_Localization_RO);
},
importDayJsLocale() {
return import("dayjs/locale/ro").then((module) => module.default);
},
},
ru: {
name: "Русский",
translatedName: "Russian",
icon: flagIcon("ru"),
importMrtLocalization() {
return import("mantine-react-table/locales/ru/index.esm.mjs").then((module) => module.MRT_Localization_RU);
},
importDayJsLocale() {
return import("dayjs/locale/ru").then((module) => module.default);
},
},
sk: {
name: "Slovenčina",
translatedName: "Slovak",
icon: flagIcon("sk"),
importMrtLocalization() {
return import("mantine-react-table/locales/sk/index.esm.mjs").then((module) => module.MRT_Localization_SK);
},
importDayJsLocale() {
return import("dayjs/locale/sk").then((module) => module.default);
},
},
sl: {
name: "Slovenščina",
translatedName: "Slovenian",
icon: flagIcon("si"),
importMrtLocalization() {
return import("./mantine-react-table/sl.json");
},
importDayJsLocale() {
return import("dayjs/locale/sl").then((module) => module.default);
},
},
sv: {
name: "Svenska",
translatedName: "Swedish",
icon: flagIcon("se"),
importMrtLocalization() {
return import("mantine-react-table/locales/sv/index.esm.mjs").then((module) => module.MRT_Localization_SV);
},
importDayJsLocale() {
return import("dayjs/locale/sv").then((module) => module.default);
},
},
tr: {
name: "Türkçe",
translatedName: "Turkish",
icon: flagIcon("tr"),
importMrtLocalization() {
return import("mantine-react-table/locales/tr/index.esm.mjs").then((module) => module.MRT_Localization_TR);
},
importDayJsLocale() {
return import("dayjs/locale/tr").then((module) => module.default);
},
},
zh: {
name: "中文",
translatedName: "Chinese (Traditional)",
icon: flagIcon("tw"),
importMrtLocalization() {
return import("mantine-react-table/locales/zh-Hant/index.esm.mjs").then(
(module) => module.MRT_Localization_ZH_HANT,
);
},
importDayJsLocale() {
return import("dayjs/locale/zh-tw").then((module) => module.default);
},
},
uk: {
name: "Українська",
translatedName: "Ukrainian",
icon: flagIcon("ua"),
importMrtLocalization() {
return import("mantine-react-table/locales/uk/index.esm.mjs").then((module) => module.MRT_Localization_UK);
},
importDayJsLocale() {
return import("dayjs/locale/uk").then((module) => module.default);
},
},
vi: {
name: "Tiếng Việt",
translatedName: "Vietnamese",
icon: flagIcon("vn"),
importMrtLocalization() {
return import("mantine-react-table/locales/vi/index.esm.mjs").then((module) => module.MRT_Localization_VI);
},
importDayJsLocale() {
return import("dayjs/locale/vi").then((module) => module.default);
},
},
} satisfies Record<
string,
{
name: string;
translatedName: string;
icon: LanguageIconDefinition;
importMrtLocalization: () => Promise<MRT_Localization>;
importDayJsLocale: () => Promise<ILocale>;
isRTL?: boolean;
}
>;
function flagIcon<TCode extends string>(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];
export const fallbackLocale = "en" satisfies SupportedLanguage;
export const isLocaleRTL = (locale: SupportedLanguage) =>
"isRTL" in localeConfigurations[locale] && localeConfigurations[locale].isRTL;

View File

@@ -0,0 +1,48 @@
import { useParams } from "next/navigation";
import dayjs from "dayjs";
import type { SupportedLanguage } from "./config";
import { localeConfigurations } from "./config";
let promise: Promise<void> | null = null;
let loading = true;
let previousLocale: SupportedLanguage | null = null;
const load = () => {
if (loading) {
// eslint-disable-next-line @typescript-eslint/only-throw-error
throw promise;
}
};
const dayJsLocalization = (locale: SupportedLanguage) => {
if (promise && previousLocale === locale) {
return {
load,
};
}
promise = localeConfigurations[locale]
.importDayJsLocale()
.then((dayJsLocale) => {
dayjs.locale(dayJsLocale);
loading = false;
previousLocale = locale;
})
.catch(() => {
loading = false;
});
return {
load,
};
};
/**
* Load the dayjs localization for the current locale with suspense
* This allows us to have the loading spinner shown until the localization is loaded and applied.
* Suspense works by throwing a promise, which is caught by the nearest Suspense boundary.
*/
export const useSuspenseDayJsLocalization = () => {
const { locale } = useParams<{ locale: SupportedLanguage }>();
const resource = dayJsLocalization(locale);
resource.load();
};

View File

@@ -0,0 +1,19 @@
import type { SupportedLanguage } from "./config";
import { supportedLanguages } from "./config";
import type { stringOrTranslation, TranslationFunction } from "./type";
export * from "./type";
export * from "./config";
export { createLanguageMapping } from "./mapping";
export type { TranslationKeys } from "./mapping";
export const translateIfNecessary = (t: TranslationFunction, value: stringOrTranslation | undefined) => {
if (typeof value === "function") {
return value(t);
}
return value;
};
export const isLocaleSupported = (locale: string): locale is SupportedLanguage => {
return supportedLanguages.includes(locale as SupportedLanguage);
};

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,93 @@
{
"actions": "",
"and": "",
"cancel": "",
"changeFilterMode": "",
"changeSearchMode": "",
"clearFilter": "",
"clearSearch": "",
"clearSelection": "",
"clearSort": "",
"clickToCopy": "",
"copy": "",
"collapse": "",
"collapseAll": "",
"columnActions": "",
"copiedToClipboard": "",
"dropToGroupBy": "",
"edit": "",
"expand": "",
"expandAll": "",
"filterArrIncludes": "",
"filterArrIncludesAll": "",
"filterArrIncludesSome": "",
"filterBetween": "",
"filterBetweenInclusive": "",
"filterByColumn": "",
"filterContains": "",
"filterEmpty": "",
"filterEndsWith": "",
"filterEquals": "",
"filterEqualsString": "",
"filterFuzzy": "",
"filterGreaterThan": "",
"filterGreaterThanOrEqualTo": "",
"filterInNumberRange": "",
"filterIncludesString": "",
"filterIncludesStringSensitive": "",
"filterLessThan": "",
"filterLessThanOrEqualTo": "",
"filterMode": "",
"filterNotEmpty": "",
"filterNotEquals": "",
"filterStartsWith": "",
"filterWeakEquals": "",
"filteringByColumn": "",
"goToFirstPage": "",
"goToLastPage": "",
"goToNextPage": "",
"goToPreviousPage": "",
"grab": "",
"groupByColumn": "",
"groupedBy": "",
"hideAll": "",
"hideColumn": "",
"max": "",
"min": "",
"move": "",
"noRecordsToDisplay": "",
"noResultsFound": "",
"of": "",
"or": "",
"pin": "",
"pinToLeft": "",
"pinToRight": "",
"resetColumnSize": "",
"resetOrder": "",
"rowActions": "",
"rowNumber": "",
"rowNumbers": "",
"rowsPerPage": "",
"save": "",
"search": "",
"selectedCountOfRowCountRowsSelected": "",
"select": "",
"showAll": "",
"showAllColumns": "",
"showHideColumns": "",
"showHideFilters": "",
"showHideSearch": "",
"sortByColumnAsc": "",
"sortByColumnDesc": "",
"sortedByColumnAsc": "",
"sortedByColumnDesc": "",
"thenBy": "",
"toggleDensity": "",
"toggleFullScreen": "",
"toggleSelectAll": "",
"toggleSelectRow": "",
"toggleVisibility": "",
"ungroupByColumn": "",
"unpin": "",
"unpinAll": ""
}

View File

@@ -0,0 +1,93 @@
{
"actions": "Actions",
"and": "and",
"cancel": "Cancel",
"changeFilterMode": "Change filter mode",
"changeSearchMode": "Change search mode",
"clearFilter": "Clear filter",
"clearSearch": "Clear search",
"clearSelection": "Clear selection",
"clearSort": "Clear sort",
"clickToCopy": "Click to copy",
"copy": "Copy",
"collapse": "Collapse",
"collapseAll": "Collapse all",
"columnActions": "Column Actions",
"copiedToClipboard": "Copied to clipboard",
"dropToGroupBy": "Drop to group by {column}",
"edit": "Edit",
"expand": "Expand",
"expandAll": "Expand all",
"filterArrIncludes": "Includes",
"filterArrIncludesAll": "Includes all",
"filterArrIncludesSome": "Includes",
"filterBetween": "Between",
"filterBetweenInclusive": "Between Inclusive",
"filterByColumn": "Filter by {column}",
"filterContains": "Contains",
"filterEmpty": "Empty",
"filterEndsWith": "Ends With",
"filterEquals": "Equals",
"filterEqualsString": "Equals",
"filterFuzzy": "Fuzzy",
"filterGreaterThan": "Greater Than",
"filterGreaterThanOrEqualTo": "Greater Than Or Equal To",
"filterInNumberRange": "Between",
"filterIncludesString": "Contains",
"filterIncludesStringSensitive": "Contains",
"filterLessThan": "Less Than",
"filterLessThanOrEqualTo": "Less Than Or Equal To",
"filterMode": "Filter Mode: {filterType}",
"filterNotEmpty": "Not Empty",
"filterNotEquals": "Not Equals",
"filterStartsWith": "Starts With",
"filterWeakEquals": "Equals",
"filteringByColumn": "Filtering by {column} - {filterType} {filterValue}",
"goToFirstPage": "Go to first page",
"goToLastPage": "Go to last page",
"goToNextPage": "Go to next page",
"goToPreviousPage": "Go to previous page",
"grab": "Grab",
"groupByColumn": "Group by {column}",
"groupedBy": "Grouped by ",
"hideAll": "Hide all",
"hideColumn": "Hide {column} column",
"max": "Max",
"min": "Min",
"move": "Move",
"noRecordsToDisplay": "No records to display",
"noResultsFound": "No results found",
"of": "of",
"or": "or",
"pin": "Pin",
"pinToLeft": "Pin to left",
"pinToRight": "Pin to right",
"resetColumnSize": "Reset column size",
"resetOrder": "Reset order",
"rowActions": "Row Actions",
"rowNumber": "#",
"rowNumbers": "Row Numbers",
"rowsPerPage": "Rows per page",
"save": "Save",
"search": "Search",
"selectedCountOfRowCountRowsSelected": "{selectedCount} of {rowCount} row(s) selected",
"select": "Select",
"showAll": "Show all",
"showAllColumns": "Show all columns",
"showHideColumns": "Show/Hide columns",
"showHideFilters": "Show/Hide filters",
"showHideSearch": "Show/Hide search",
"sortByColumnAsc": "Sort by {column} ascending",
"sortByColumnDesc": "Sort by {column} descending",
"sortedByColumnAsc": "Sorted by {column} ascending",
"sortedByColumnDesc": "Sorted by {column} descending",
"thenBy": ", then by ",
"toggleDensity": "Toggle density",
"toggleFullScreen": "Toggle full screen",
"toggleSelectAll": "Toggle select all",
"toggleSelectRow": "Toggle select row",
"toggleVisibility": "Toggle visibility",
"ungroupByColumn": "Ungroup by {column}",
"unpin": "Unpin",
"unpinAll": "Unpin all"
}

View File

@@ -0,0 +1,93 @@
{
"actions": "",
"and": "",
"cancel": "",
"changeFilterMode": "",
"changeSearchMode": "",
"clearFilter": "",
"clearSearch": "",
"clearSelection": "",
"clearSort": "",
"clickToCopy": "",
"copy": "",
"collapse": "",
"collapseAll": "",
"columnActions": "",
"copiedToClipboard": "",
"dropToGroupBy": "",
"edit": "",
"expand": "",
"expandAll": "",
"filterArrIncludes": "",
"filterArrIncludesAll": "",
"filterArrIncludesSome": "",
"filterBetween": "",
"filterBetweenInclusive": "",
"filterByColumn": "",
"filterContains": "",
"filterEmpty": "",
"filterEndsWith": "",
"filterEquals": "",
"filterEqualsString": "",
"filterFuzzy": "",
"filterGreaterThan": "",
"filterGreaterThanOrEqualTo": "",
"filterInNumberRange": "",
"filterIncludesString": "",
"filterIncludesStringSensitive": "",
"filterLessThan": "",
"filterLessThanOrEqualTo": "",
"filterMode": "",
"filterNotEmpty": "",
"filterNotEquals": "",
"filterStartsWith": "",
"filterWeakEquals": "",
"filteringByColumn": "",
"goToFirstPage": "",
"goToLastPage": "",
"goToNextPage": "",
"goToPreviousPage": "",
"grab": "",
"groupByColumn": "",
"groupedBy": "",
"hideAll": "",
"hideColumn": "",
"max": "",
"min": "",
"move": "",
"noRecordsToDisplay": "",
"noResultsFound": "",
"of": "",
"or": "",
"pin": "",
"pinToLeft": "",
"pinToRight": "",
"resetColumnSize": "",
"resetOrder": "",
"rowActions": "",
"rowNumber": "",
"rowNumbers": "",
"rowsPerPage": "",
"save": "",
"search": "",
"selectedCountOfRowCountRowsSelected": "",
"select": "",
"showAll": "",
"showAllColumns": "",
"showHideColumns": "",
"showHideFilters": "",
"showHideSearch": "",
"sortByColumnAsc": "",
"sortByColumnDesc": "",
"sortedByColumnAsc": "",
"sortedByColumnDesc": "",
"thenBy": "",
"toggleDensity": "",
"toggleFullScreen": "",
"toggleSelectAll": "",
"toggleSelectRow": "",
"toggleVisibility": "",
"ungroupByColumn": "",
"unpin": "",
"unpinAll": ""
}

View File

@@ -0,0 +1,93 @@
{
"actions": "",
"and": "",
"cancel": "",
"changeFilterMode": "",
"changeSearchMode": "",
"clearFilter": "",
"clearSearch": "",
"clearSelection": "",
"clearSort": "",
"clickToCopy": "",
"copy": "",
"collapse": "",
"collapseAll": "",
"columnActions": "",
"copiedToClipboard": "",
"dropToGroupBy": "",
"edit": "",
"expand": "",
"expandAll": "",
"filterArrIncludes": "",
"filterArrIncludesAll": "",
"filterArrIncludesSome": "",
"filterBetween": "",
"filterBetweenInclusive": "",
"filterByColumn": "",
"filterContains": "",
"filterEmpty": "",
"filterEndsWith": "",
"filterEquals": "",
"filterEqualsString": "",
"filterFuzzy": "",
"filterGreaterThan": "",
"filterGreaterThanOrEqualTo": "",
"filterInNumberRange": "",
"filterIncludesString": "",
"filterIncludesStringSensitive": "",
"filterLessThan": "",
"filterLessThanOrEqualTo": "",
"filterMode": "",
"filterNotEmpty": "",
"filterNotEquals": "",
"filterStartsWith": "",
"filterWeakEquals": "",
"filteringByColumn": "",
"goToFirstPage": "",
"goToLastPage": "",
"goToNextPage": "",
"goToPreviousPage": "",
"grab": "",
"groupByColumn": "",
"groupedBy": "",
"hideAll": "",
"hideColumn": "",
"max": "",
"min": "",
"move": "",
"noRecordsToDisplay": "",
"noResultsFound": "",
"of": "",
"or": "",
"pin": "",
"pinToLeft": "",
"pinToRight": "",
"resetColumnSize": "",
"resetOrder": "",
"rowActions": "",
"rowNumber": "",
"rowNumbers": "",
"rowsPerPage": "",
"save": "",
"search": "",
"selectedCountOfRowCountRowsSelected": "",
"select": "",
"showAll": "",
"showAllColumns": "",
"showHideColumns": "",
"showHideFilters": "",
"showHideSearch": "",
"sortByColumnAsc": "",
"sortByColumnDesc": "",
"sortedByColumnAsc": "",
"sortedByColumnDesc": "",
"thenBy": "",
"toggleDensity": "",
"toggleFullScreen": "",
"toggleSelectAll": "",
"toggleSelectRow": "",
"toggleVisibility": "",
"ungroupByColumn": "",
"unpin": "",
"unpinAll": ""
}

View File

@@ -0,0 +1,93 @@
{
"actions": "",
"and": "",
"cancel": "",
"changeFilterMode": "",
"changeSearchMode": "",
"clearFilter": "",
"clearSearch": "",
"clearSelection": "",
"clearSort": "",
"clickToCopy": "",
"copy": "",
"collapse": "",
"collapseAll": "",
"columnActions": "",
"copiedToClipboard": "",
"dropToGroupBy": "",
"edit": "",
"expand": "",
"expandAll": "",
"filterArrIncludes": "",
"filterArrIncludesAll": "",
"filterArrIncludesSome": "",
"filterBetween": "",
"filterBetweenInclusive": "",
"filterByColumn": "",
"filterContains": "",
"filterEmpty": "",
"filterEndsWith": "",
"filterEquals": "",
"filterEqualsString": "",
"filterFuzzy": "",
"filterGreaterThan": "",
"filterGreaterThanOrEqualTo": "",
"filterInNumberRange": "",
"filterIncludesString": "",
"filterIncludesStringSensitive": "",
"filterLessThan": "",
"filterLessThanOrEqualTo": "",
"filterMode": "",
"filterNotEmpty": "",
"filterNotEquals": "",
"filterStartsWith": "",
"filterWeakEquals": "",
"filteringByColumn": "",
"goToFirstPage": "",
"goToLastPage": "",
"goToNextPage": "",
"goToPreviousPage": "",
"grab": "",
"groupByColumn": "",
"groupedBy": "",
"hideAll": "",
"hideColumn": "",
"max": "",
"min": "",
"move": "",
"noRecordsToDisplay": "",
"noResultsFound": "",
"of": "",
"or": "",
"pin": "",
"pinToLeft": "",
"pinToRight": "",
"resetColumnSize": "",
"resetOrder": "",
"rowActions": "",
"rowNumber": "",
"rowNumbers": "",
"rowsPerPage": "",
"save": "",
"search": "",
"selectedCountOfRowCountRowsSelected": "",
"select": "",
"showAll": "",
"showAllColumns": "",
"showHideColumns": "",
"showHideFilters": "",
"showHideSearch": "",
"sortByColumnAsc": "",
"sortByColumnDesc": "",
"sortedByColumnAsc": "",
"sortedByColumnDesc": "",
"thenBy": "",
"toggleDensity": "",
"toggleFullScreen": "",
"toggleSelectAll": "",
"toggleSelectRow": "",
"toggleVisibility": "",
"ungroupByColumn": "",
"unpin": "",
"unpinAll": ""
}

View File

@@ -0,0 +1,22 @@
import { supportedLanguages } from "./config";
const _enTranslations = () => import("./lang/en.json");
type EnTranslation = typeof _enTranslations;
export const createLanguageMapping = () => {
const mapping: Record<string, unknown> = {};
for (const language of supportedLanguages) {
mapping[language] = () => import(`./lang/${language}.json`);
}
return mapping as Record<(typeof supportedLanguages)[number], () => ReturnType<EnTranslation>>;
};
type NestedKeyOf<ObjectType extends object> = {
[Key in keyof ObjectType & (string | number)]: ObjectType[Key] extends object
? `${Key}` | `${Key}.${NestedKeyOf<ObjectType[Key]>}`
: `${Key}`;
}[keyof ObjectType & (string | number)];
export type TranslationKeys = NestedKeyOf<EnTranslation>;

View File

@@ -0,0 +1,13 @@
import createMiddleware from "next-intl/middleware";
import type { SupportedLanguage } from ".";
import { supportedLanguages } from "./config";
import { createRouting } from "./routing";
export const createI18nMiddleware = (defaultLocale: SupportedLanguage) =>
createMiddleware(createRouting(defaultLocale));
export const config = {
// Match only internationalized pathnames
matcher: ["/", `/(${supportedLanguages.join("|")})/:path*`],
};

View File

@@ -0,0 +1,57 @@
import deepmerge from "deepmerge";
import { getRequestConfig } from "next-intl/server";
import type { TranslationObject } from ".";
import { fallbackLocale, isLocaleSupported } from ".";
import type { SupportedLanguage } from "./config";
import { createLanguageMapping } from "./mapping";
// This file is referenced in the `next.config.js` file. See https://next-intl-docs.vercel.app/docs/usage/configuration
export default getRequestConfig(async ({ requestLocale }) => {
let currentLocale = await requestLocale;
if (!currentLocale || !isLocaleSupported(currentLocale)) {
currentLocale = fallbackLocale;
}
const typedLocale = currentLocale as SupportedLanguage;
const languageMap = createLanguageMapping();
const currentMessages = removeEmptyTranslations((await languageMap[typedLocale]()).default) as TranslationObject;
// Fallback to default locale if the current locales messages if not all messages are present
if (currentLocale !== fallbackLocale) {
const fallbackMessages = (await languageMap[fallbackLocale]()).default;
return {
locale: typedLocale,
messages: deepmerge(fallbackMessages, currentMessages),
};
}
return {
locale: currentLocale,
messages: currentMessages,
};
});
const removeEmptyTranslations = (translations: Record<string, unknown>): Record<string, unknown> => {
return Object.entries(translations).reduce(
(acc, [key, value]) => {
if (typeof value !== "string") {
return {
...acc,
[key]: removeEmptyTranslations(value as Record<string, unknown>),
};
}
if (value.trim() === "") {
return acc;
}
return {
...acc,
[key]: value,
};
},
{} as Record<string, unknown>,
);
};

View File

@@ -0,0 +1,20 @@
import { defineRouting } from "next-intl/routing";
import { localeCookieKey } from "@homarr/definitions";
import type { SupportedLanguage } from "./config";
import { supportedLanguages } from "./config";
export const createRouting = (defaultLocale: SupportedLanguage) =>
defineRouting({
locales: supportedLanguages,
defaultLocale,
localeCookie: {
name: localeCookieKey,
// 1 year
maxAge: 60 * 60 * 24 * 365,
},
localePrefix: {
mode: "never", // Rewrite the URL with locale parameter but without shown in url
},
});

View File

@@ -0,0 +1,18 @@
import { getTranslations } from "next-intl/server";
import type { SupportedLanguage } from "./config";
import type englishTranslation from "./lang/en.json";
declare module "next-intl" {
interface AppConfig {
Messages: typeof englishTranslation;
Locale: SupportedLanguage;
}
}
export const { getI18n, getScopedI18n } = {
getI18n: getTranslations,
getScopedI18n: getTranslations,
};
export { getMessages as getI18nMessages } from "next-intl/server";

View File

@@ -0,0 +1,19 @@
import type { NamespaceKeys, NestedKeyOf } from "next-intl";
import type { RemoveReadonly } from "@homarr/common/types";
import type { useI18n, useScopedI18n } from "./client";
import type enTranslation from "./lang/en.json";
export type TranslationFunction = ReturnType<typeof useI18n<never>>;
export type ScopedTranslationFunction<
NestedKey extends NamespaceKeys<IntlMessages, NestedKeyOf<IntlMessages>> = never,
> = ReturnType<typeof useScopedI18n<NestedKey>>;
export type TranslationObject = typeof enTranslation;
export type stringOrTranslation = string | ((t: TranslationFunction) => string);
declare global {
// Use type safe message keys with `next-intl`
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
interface IntlMessages extends RemoveReadonly<TranslationObject> {}
}

View File

@@ -0,0 +1,8 @@
{
"extends": "@homarr/tsconfig/base.json",
"compilerOptions": {
"tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json"
},
"include": ["*.ts", "src"],
"exclude": ["node_modules"]
}