Merge branch 'dev' into feat/appearance-updates
This commit is contained in:
84 files changed
+1805
-1470
No files matched your search
@@ -9,7 +9,8 @@
|
||||
"./client": "./src/client.ts",
|
||||
"./server": "./src/server.ts",
|
||||
"./websocket": "./src/websocket.ts",
|
||||
"./shared": "./src/shared.ts"
|
||||
"./shared": "./src/shared.ts",
|
||||
"./open-api": "./src/open-api.ts"
|
||||
},
|
||||
"main": "./index.ts",
|
||||
"types": "./index.ts",
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { inferRouterInputs, inferRouterOutputs } from "@trpc/server";
|
||||
|
||||
import { openApiDocument } from "./open-api";
|
||||
import type { AppRouter } from "./root";
|
||||
import { appRouter } from "./root";
|
||||
import { createCallerFactory, createTRPCContext } from "./trpc";
|
||||
@@ -30,5 +29,5 @@ type RouterInputs = inferRouterInputs<AppRouter>;
|
||||
**/
|
||||
type RouterOutputs = inferRouterOutputs<AppRouter>;
|
||||
|
||||
export { createTRPCContext, appRouter, createCaller, openApiDocument };
|
||||
export { createTRPCContext, appRouter, createCaller };
|
||||
export type { AppRouter, RouterInputs, RouterOutputs };
|
||||
@@ -2,10 +2,21 @@ import { generateOpenApiDocument } from "trpc-to-openapi";
|
||||
|
||||
import { API_KEY_HEADER_NAME } from "@homarr/auth/api-key";
|
||||
|
||||
import { appRouter } from "./root";
|
||||
import { appRouter } from "./router/app";
|
||||
import { infoRouter } from "./router/info";
|
||||
import { inviteRouter } from "./router/invite";
|
||||
import { userRouter } from "./router/user";
|
||||
import { createTRPCRouter } from "./trpc";
|
||||
|
||||
export const openApiRouter = createTRPCRouter({
|
||||
appRouter,
|
||||
infoRouter,
|
||||
inviteRouter,
|
||||
userRouter,
|
||||
});
|
||||
|
||||
export const openApiDocument = (base: string) =>
|
||||
generateOpenApiDocument(appRouter, {
|
||||
generateOpenApiDocument(openApiRouter, {
|
||||
title: "Homarr API documentation",
|
||||
version: "1.0.0",
|
||||
baseUrl: base,
|
||||
|
||||
@@ -2,7 +2,7 @@ import { observable } from "@trpc/server/observable";
|
||||
import z from "zod/v4";
|
||||
|
||||
import { createLogger } from "@homarr/core/infrastructure/logs";
|
||||
import { cronExpressionSchema, jobGroupKeys, jobNameSchema } from "@homarr/cron-job-api";
|
||||
import { jobGroupKeys, jobNameSchema } from "@homarr/cron-job-api";
|
||||
import { cronJobApi } from "@homarr/cron-job-api/client";
|
||||
import type { TaskStatus } from "@homarr/cron-job-status";
|
||||
import { createCronJobStatusChannel } from "@homarr/cron-job-status";
|
||||
@@ -35,7 +35,7 @@ export const cronJobsRouter = createTRPCRouter({
|
||||
.input(
|
||||
z.object({
|
||||
name: jobNameSchema,
|
||||
cron: cronExpressionSchema,
|
||||
cron: z.string(),
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ input }) => {
|
||||
|
||||
@@ -110,7 +110,7 @@ export const userRouter = createTRPCRouter({
|
||||
const queryResult = (await trx.delete(schema.invites).where(inviteWhere)) as
|
||||
| QueryResult
|
||||
| MySqlRawQueryResult;
|
||||
let count = 0;
|
||||
let count: number;
|
||||
if (Array.isArray(queryResult)) {
|
||||
count = queryResult[0].affectedRows;
|
||||
} else {
|
||||
|
||||
@@ -41,6 +41,10 @@ export class Stopwatch {
|
||||
return dayjs().millisecond(this.startTime).fromNow(true);
|
||||
}
|
||||
|
||||
getElapsedInFormattedMilliseconds() {
|
||||
return `${this.getElapsedInMilliseconds().toFixed(0)}ms`;
|
||||
}
|
||||
|
||||
getElapsedInMilliseconds() {
|
||||
return performance.now() - this.startTime;
|
||||
}
|
||||
|
||||
@@ -32,7 +32,6 @@
|
||||
"@trpc/client": "catalog:",
|
||||
"@trpc/server": "catalog:",
|
||||
"@trpc/tanstack-react-query": "catalog:",
|
||||
"node-cron": "catalog:",
|
||||
"react": "catalog:",
|
||||
"zod": "catalog:"
|
||||
},
|
||||
@@ -40,7 +39,6 @@
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"@types/node-cron": "catalog:",
|
||||
"@types/react": "catalog:",
|
||||
"eslint": "catalog:",
|
||||
"typescript": "catalog:"
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { initTRPC, TRPCError } from "@trpc/server";
|
||||
import { validate } from "node-cron";
|
||||
import { z } from "zod/v4";
|
||||
|
||||
import type { JobGroupKeys } from "@homarr/cron-jobs";
|
||||
@@ -13,7 +12,7 @@ export const jobNameSchema = z.enum(jobGroup.getKeys());
|
||||
export interface IJobManager {
|
||||
startAsync(name: JobGroupKeys): Promise<void>;
|
||||
triggerAsync(name: JobGroupKeys): Promise<void>;
|
||||
stopAsync(name: JobGroupKeys): Promise<void>;
|
||||
stop(name: JobGroupKeys): void;
|
||||
updateIntervalAsync(name: JobGroupKeys, cron: string): Promise<void>;
|
||||
disableAsync(name: JobGroupKeys): Promise<void>;
|
||||
enableAsync(name: JobGroupKeys): Promise<void>;
|
||||
@@ -52,10 +51,6 @@ const apiKeyProcedure = t.procedure.use(({ ctx, next }) => {
|
||||
});
|
||||
});
|
||||
|
||||
export const cronExpressionSchema = z.string().refine((expression) => validate(expression), {
|
||||
error: "Invalid cron expression",
|
||||
});
|
||||
|
||||
export const jobRouter = createTrpcRouter({
|
||||
start: apiKeyProcedure.input(jobNameSchema).mutation(async ({ input, ctx }) => {
|
||||
await ctx.manager.startAsync(input);
|
||||
@@ -63,14 +58,14 @@ export const jobRouter = createTrpcRouter({
|
||||
trigger: apiKeyProcedure.input(jobNameSchema).mutation(async ({ input, ctx }) => {
|
||||
await ctx.manager.triggerAsync(input);
|
||||
}),
|
||||
stop: apiKeyProcedure.input(jobNameSchema).mutation(async ({ input, ctx }) => {
|
||||
await ctx.manager.stopAsync(input);
|
||||
stop: apiKeyProcedure.input(jobNameSchema).mutation(({ input, ctx }) => {
|
||||
ctx.manager.stop(input);
|
||||
}),
|
||||
updateInterval: apiKeyProcedure
|
||||
.input(
|
||||
z.object({
|
||||
name: jobNameSchema,
|
||||
cron: cronExpressionSchema,
|
||||
cron: z.string(),
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
|
||||
@@ -27,13 +27,12 @@
|
||||
"@homarr/common": "workspace:^0.1.0",
|
||||
"@homarr/core": "workspace:^",
|
||||
"@homarr/db": "workspace:^0.1.0",
|
||||
"node-cron": "catalog:"
|
||||
"croner": "catalog:"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"@types/node-cron": "catalog:",
|
||||
"eslint": "catalog:",
|
||||
"typescript": "catalog:"
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { createTask, validate } from "node-cron";
|
||||
import { Cron } from "croner";
|
||||
|
||||
import { Stopwatch } from "@homarr/common";
|
||||
import type { MaybePromise } from "@homarr/common/types";
|
||||
@@ -83,10 +83,15 @@ const createCallback = <TAllowedNames extends string, TName extends TAllowedName
|
||||
? defaultCronExpression
|
||||
: (configuration?.cronExpression ?? defaultCronExpression);
|
||||
|
||||
const scheduledTask = createTask(cronExpression, () => void catchingCallbackAsync(), {
|
||||
name,
|
||||
timezone: creatorOptions.timezone,
|
||||
});
|
||||
const scheduledTask = new Cron(
|
||||
cronExpression,
|
||||
{
|
||||
name,
|
||||
timezone: creatorOptions.timezone,
|
||||
paused: true,
|
||||
},
|
||||
() => void catchingCallbackAsync(),
|
||||
);
|
||||
creatorOptions.logger.logDebug("The scheduled task for cron job was created", {
|
||||
name,
|
||||
cronExpression: defaultCronExpression,
|
||||
@@ -116,6 +121,7 @@ const createCallback = <TAllowedNames extends string, TName extends TAllowedName
|
||||
},
|
||||
preventManualExecution: options.preventManualExecution ?? false,
|
||||
preventCustomInterval: options.preventCustomInterval ?? false,
|
||||
timezone: creatorOptions.timezone,
|
||||
};
|
||||
};
|
||||
};
|
||||
@@ -132,13 +138,6 @@ export const createCronJobCreator = <TAllowedNames extends string = string>(
|
||||
defaultCronExpression: TExpression,
|
||||
options: CreateCronJobOptions = { runOnStart: false },
|
||||
) => {
|
||||
creatorOptions.logger.logDebug("Validating cron expression for cron job", {
|
||||
name,
|
||||
cronExpression: defaultCronExpression,
|
||||
});
|
||||
if (!validate(defaultCronExpression)) {
|
||||
throw new Error(`Invalid cron expression '${defaultCronExpression}' for job '${name}'`);
|
||||
}
|
||||
creatorOptions.logger.logDebug("Cron job expression for cron job is valid", {
|
||||
name,
|
||||
cronExpression: defaultCronExpression,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { ScheduledTask } from "node-cron";
|
||||
import type { Cron } from "croner";
|
||||
|
||||
import { objectEntries, objectKeys } from "@homarr/common";
|
||||
import { db } from "@homarr/db";
|
||||
@@ -34,7 +34,7 @@ export const createJobGroupCreator = <TAllowedNames extends string = string>(
|
||||
});
|
||||
}
|
||||
|
||||
const tasks = new Map<string, ScheduledTask>();
|
||||
const tasks = new Map<string, Cron>();
|
||||
|
||||
return {
|
||||
initializeAsync: async () => {
|
||||
@@ -62,7 +62,7 @@ export const createJobGroupCreator = <TAllowedNames extends string = string>(
|
||||
name: job.name,
|
||||
});
|
||||
await job.onStartAsync();
|
||||
await tasks.get(name as string)?.start();
|
||||
tasks.get(name as string)?.resume();
|
||||
},
|
||||
startAllAsync: async () => {
|
||||
for (const job of jobRegistry.values()) {
|
||||
@@ -74,7 +74,7 @@ export const createJobGroupCreator = <TAllowedNames extends string = string>(
|
||||
name: job.name,
|
||||
});
|
||||
await job.onStartAsync();
|
||||
await tasks.get(job.name)?.start();
|
||||
tasks.get(job.name)?.resume();
|
||||
}
|
||||
},
|
||||
runManuallyAsync: async (name: keyof TJobs) => {
|
||||
@@ -87,23 +87,23 @@ export const createJobGroupCreator = <TAllowedNames extends string = string>(
|
||||
options.logger.logInfo("Running schedule cron job manually.", {
|
||||
name: job.name,
|
||||
});
|
||||
await tasks.get(name as string)?.execute();
|
||||
await tasks.get(name as string)?.trigger();
|
||||
},
|
||||
stopAsync: async (name: keyof TJobs) => {
|
||||
stop: (name: keyof TJobs) => {
|
||||
const job = jobRegistry.get(name as string);
|
||||
if (!job) return;
|
||||
|
||||
options.logger.logInfo("Stopping schedule of cron job.", {
|
||||
name: job.name,
|
||||
});
|
||||
await tasks.get(name as string)?.stop();
|
||||
tasks.get(name as string)?.pause();
|
||||
},
|
||||
stopAllAsync: async () => {
|
||||
stopAll: () => {
|
||||
for (const job of jobRegistry.values()) {
|
||||
options.logger.logInfo("Stopping schedule of cron job.", {
|
||||
name: job.name,
|
||||
});
|
||||
await tasks.get(job.name)?.stop();
|
||||
tasks.get(job.name)?.pause();
|
||||
}
|
||||
},
|
||||
getJobRegistry() {
|
||||
@@ -112,8 +112,8 @@ export const createJobGroupCreator = <TAllowedNames extends string = string>(
|
||||
getTask(name: keyof TJobs) {
|
||||
return tasks.get(name as string) ?? null;
|
||||
},
|
||||
setTask(name: keyof TJobs, task: ScheduledTask) {
|
||||
tasks.set(name as string, task);
|
||||
setTask(name: keyof TJobs, cron: Cron) {
|
||||
tasks.set(name as string, cron);
|
||||
},
|
||||
getKeys() {
|
||||
return objectKeys(jobs);
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import type { MySqlRawQueryResult } from "drizzle-orm/mysql2";
|
||||
import type { QueryResult } from "pg";
|
||||
|
||||
import { createId, splitToNChunks, Stopwatch } from "@homarr/common";
|
||||
import { env } from "@homarr/common/env";
|
||||
import { createLogger } from "@homarr/core/infrastructure/logs";
|
||||
import { EVERY_WEEK } from "@homarr/cron-jobs-core/expressions";
|
||||
import type { InferInsertModel } from "@homarr/db";
|
||||
import { db, handleTransactionsAsync, inArray, sql } from "@homarr/db";
|
||||
import { and, db, eq, handleTransactionsAsync, inArray, notInArray, or } from "@homarr/db";
|
||||
import { iconRepositories, icons } from "@homarr/db/schema";
|
||||
import { fetchIconsAsync } from "@homarr/icons";
|
||||
|
||||
@@ -26,7 +29,7 @@ export const iconsUpdaterJob = createCronJob("iconsUpdater", EVERY_WEEK, {
|
||||
logger.info("Fetched icons from repositories", {
|
||||
repositoryCount: repositoryIconGroups.length,
|
||||
iconCount: countIcons,
|
||||
duration: stopWatch.getElapsedInHumanWords(),
|
||||
duration: stopWatch.getElapsedInFormattedMilliseconds(),
|
||||
});
|
||||
|
||||
const databaseIconRepositories = await db.query.iconRepositories.findMany({
|
||||
@@ -35,7 +38,7 @@ export const iconsUpdaterJob = createCronJob("iconsUpdater", EVERY_WEEK, {
|
||||
},
|
||||
});
|
||||
|
||||
const skippedChecksums: `${string}.${string}`[] = [];
|
||||
const skippedChecksums = new Map<string, string[]>();
|
||||
let countDeleted = 0;
|
||||
let countInserted = 0;
|
||||
|
||||
@@ -50,6 +53,7 @@ export const iconsUpdaterJob = createCronJob("iconsUpdater", EVERY_WEEK, {
|
||||
if (!repositoryIconGroup.success) {
|
||||
continue;
|
||||
}
|
||||
const localSkippedChecksums: string[] = [];
|
||||
|
||||
const repositoryInDb = databaseIconRepositories.find(
|
||||
(dbIconGroup) => dbIconGroup.slug === repositoryIconGroup.slug,
|
||||
@@ -66,7 +70,7 @@ export const iconsUpdaterJob = createCronJob("iconsUpdater", EVERY_WEEK, {
|
||||
|
||||
for (const icon of repositoryIconGroup.icons) {
|
||||
if (dbIconsInRepository.some((dbIcon) => dbIcon.checksum === icon.checksum)) {
|
||||
skippedChecksums.push(`${iconRepositoryId}.${icon.checksum}`);
|
||||
localSkippedChecksums.push(icon.checksum);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -79,38 +83,28 @@ export const iconsUpdaterJob = createCronJob("iconsUpdater", EVERY_WEEK, {
|
||||
});
|
||||
countInserted++;
|
||||
}
|
||||
}
|
||||
|
||||
const deadIcons = allDbIcons.filter(
|
||||
(icon) => !skippedChecksums.includes(`${icon.iconRepositoryId}.${icon.checksum}`),
|
||||
);
|
||||
skippedChecksums.set(iconRepositoryId, localSkippedChecksums);
|
||||
}
|
||||
|
||||
const deadIconRepositories = databaseIconRepositories.filter(
|
||||
(iconRepository) => !repositoryIconGroups.some((group) => group.slug === iconRepository.slug),
|
||||
);
|
||||
|
||||
const deadIconsFilter = or(
|
||||
...[...skippedChecksums.entries()].map(([iconRepositoryId, checksums]) =>
|
||||
and(eq(icons.iconRepositoryId, iconRepositoryId), notInArray(icons.checksum, checksums)),
|
||||
),
|
||||
);
|
||||
|
||||
await handleTransactionsAsync(db, {
|
||||
async handleAsync(db, schema) {
|
||||
await db.transaction(async (transaction) => {
|
||||
if (newIconRepositories.length >= 1) {
|
||||
await transaction.insert(schema.iconRepositories).values(newIconRepositories);
|
||||
}
|
||||
const result = (await transaction.delete(schema.icons).where(deadIconsFilter)) as
|
||||
| MySqlRawQueryResult
|
||||
| QueryResult;
|
||||
|
||||
if (newIcons.length >= 1) {
|
||||
// We only insert 5000 icons at a time to avoid SQLite limitations
|
||||
for (const chunck of splitToNChunks(newIcons, Math.ceil(newIcons.length / 5000))) {
|
||||
await transaction.insert(schema.icons).values(chunck);
|
||||
}
|
||||
}
|
||||
if (deadIcons.length >= 1) {
|
||||
await transaction.delete(schema.icons).where(
|
||||
inArray(
|
||||
// Combine iconRepositoryId and checksum to allow same icons on different repositories
|
||||
sql`concat(${icons.iconRepositoryId}, '.', ${icons.checksum})`,
|
||||
deadIcons.map((icon) => `${icon.iconRepositoryId}.${icon.checksum}`),
|
||||
),
|
||||
);
|
||||
}
|
||||
countDeleted += Array.isArray(result) ? result[0].affectedRows : (result.rowCount ?? 0);
|
||||
|
||||
if (deadIconRepositories.length >= 1) {
|
||||
await transaction.delete(schema.iconRepositories).where(
|
||||
@@ -121,33 +115,22 @@ export const iconsUpdaterJob = createCronJob("iconsUpdater", EVERY_WEEK, {
|
||||
);
|
||||
}
|
||||
|
||||
countDeleted += deadIcons.length;
|
||||
});
|
||||
},
|
||||
handleSync() {
|
||||
db.transaction((transaction) => {
|
||||
if (newIconRepositories.length >= 1) {
|
||||
transaction.insert(iconRepositories).values(newIconRepositories).run();
|
||||
await transaction.insert(schema.iconRepositories).values(newIconRepositories);
|
||||
}
|
||||
|
||||
if (newIcons.length >= 1) {
|
||||
// We only insert 5000 icons at a time to avoid SQLite limitations
|
||||
for (const chunck of splitToNChunks(newIcons, Math.ceil(newIcons.length / 5000))) {
|
||||
transaction.insert(icons).values(chunck).run();
|
||||
await transaction.insert(schema.icons).values(chunck);
|
||||
}
|
||||
}
|
||||
if (deadIcons.length >= 1) {
|
||||
transaction
|
||||
.delete(icons)
|
||||
.where(
|
||||
inArray(
|
||||
// Combine iconRepositoryId and checksum to allow same icons on different repositories
|
||||
sql`concat(${icons.iconRepositoryId}, '.', ${icons.checksum})`,
|
||||
deadIcons.map((icon) => `${icon.iconRepositoryId}.${icon.checksum}`),
|
||||
),
|
||||
)
|
||||
.run();
|
||||
}
|
||||
});
|
||||
},
|
||||
handleSync() {
|
||||
db.transaction((transaction) => {
|
||||
const result = transaction.delete(icons).where(deadIconsFilter).run();
|
||||
countDeleted += result.changes;
|
||||
|
||||
if (deadIconRepositories.length >= 1) {
|
||||
transaction
|
||||
@@ -161,13 +144,22 @@ export const iconsUpdaterJob = createCronJob("iconsUpdater", EVERY_WEEK, {
|
||||
.run();
|
||||
}
|
||||
|
||||
countDeleted += deadIcons.length;
|
||||
if (newIconRepositories.length >= 1) {
|
||||
transaction.insert(iconRepositories).values(newIconRepositories).run();
|
||||
}
|
||||
|
||||
if (newIcons.length >= 1) {
|
||||
// We only insert 5000 icons at a time to avoid SQLite limitations
|
||||
for (const chunck of splitToNChunks(newIcons, Math.ceil(newIcons.length / 5000))) {
|
||||
transaction.insert(icons).values(chunck).run();
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
logger.info("Updated icons in database", {
|
||||
duration: stopWatch.getElapsedInHumanWords(),
|
||||
duration: stopWatch.getElapsedInFormattedMilliseconds(),
|
||||
added: countInserted,
|
||||
deleted: countDeleted,
|
||||
});
|
||||
|
||||
@@ -37,6 +37,8 @@
|
||||
"push:sqlite": "pnpm with-env drizzle-kit push --config ./configs/sqlite.config.ts",
|
||||
"seed": "pnpm with-env tsx ./migrations/run-seed.ts",
|
||||
"studio": "pnpm with-env drizzle-kit studio --config ./configs/sqlite.config.ts",
|
||||
"studio:mysql": "pnpm with-env drizzle-kit studio --config ./configs/mysql.config.ts",
|
||||
"studio:postgresql": "pnpm with-env drizzle-kit studio --config ./configs/postgresql.config.ts",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"with-env": "dotenv -e ../../.env --"
|
||||
},
|
||||
|
||||
@@ -77,7 +77,7 @@ export class AnchorIntegration extends Integration {
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
let errorBody: string | null = null;
|
||||
let errorBody: string | null;
|
||||
try {
|
||||
errorBody = await response.text();
|
||||
} catch {
|
||||
|
||||
@@ -3790,6 +3790,10 @@
|
||||
"title": "",
|
||||
"subtitle": ""
|
||||
},
|
||||
"sponsors": {
|
||||
"title": "",
|
||||
"subtitle": ""
|
||||
},
|
||||
"libraries": {
|
||||
"title": "",
|
||||
"subtitle": ""
|
||||
|
||||
@@ -1614,22 +1614,22 @@
|
||||
"label": ""
|
||||
},
|
||||
"showTitle": {
|
||||
"label": ""
|
||||
"label": "显示标题"
|
||||
},
|
||||
"showUpdatedAt": {
|
||||
"label": ""
|
||||
"label": "显示最近更新"
|
||||
}
|
||||
},
|
||||
"empty": "",
|
||||
"emptyContent": "",
|
||||
"notFound": "",
|
||||
"empty": "选择要显示的笔记",
|
||||
"emptyContent": "此笔记没有内容",
|
||||
"notFound": "未找到笔记",
|
||||
"noNotes": "",
|
||||
"integrationRequired": "",
|
||||
"updatedAt": "",
|
||||
"untitled": "",
|
||||
"edit": "",
|
||||
"save": "",
|
||||
"cancel": "",
|
||||
"updatedAt": "更新时间:{date}",
|
||||
"untitled": "未命名",
|
||||
"edit": "编辑",
|
||||
"save": "保存",
|
||||
"cancel": "取消",
|
||||
"readOnlyViewer": "",
|
||||
"saveForbidden": "",
|
||||
"saveFailed": ""
|
||||
@@ -1901,7 +1901,7 @@
|
||||
"label": "显示内存信息"
|
||||
},
|
||||
"gpu": {
|
||||
"label": ""
|
||||
"label": "显示 GPU 信息"
|
||||
},
|
||||
"showUptime": {
|
||||
"label": "显示运行时间"
|
||||
@@ -2200,15 +2200,15 @@
|
||||
"description": ""
|
||||
},
|
||||
"showPhotos": {
|
||||
"label": "",
|
||||
"label": "显示图片",
|
||||
"description": ""
|
||||
},
|
||||
"showVideos": {
|
||||
"label": "",
|
||||
"label": "显示视频",
|
||||
"description": ""
|
||||
},
|
||||
"showStorage": {
|
||||
"label": "",
|
||||
"label": "显示存储",
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
@@ -2229,7 +2229,7 @@
|
||||
"description": ""
|
||||
},
|
||||
"showPhotoInfo": {
|
||||
"label": "",
|
||||
"label": "显示图片信息",
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
@@ -2783,17 +2783,17 @@
|
||||
"recentResults": "",
|
||||
"recentResultsSpeed": "",
|
||||
"recentResultsPing": "",
|
||||
"download": "",
|
||||
"upload": "",
|
||||
"ping": "",
|
||||
"status": "",
|
||||
"avgDownload": "",
|
||||
"avgUpload": "",
|
||||
"avgPing": "",
|
||||
"download": "下载",
|
||||
"upload": "上传",
|
||||
"ping": "Ping",
|
||||
"status": "状态",
|
||||
"avgDownload": "平均下载",
|
||||
"avgUpload": "平均上传",
|
||||
"avgPing": "平均Ping",
|
||||
"healthy": "",
|
||||
"unhealthy": "",
|
||||
"ok": "",
|
||||
"fail": "",
|
||||
"fail": "失败",
|
||||
"noSectionsEnabled": "",
|
||||
"error": {
|
||||
"internalServerError": ""
|
||||
@@ -2812,7 +2812,7 @@
|
||||
"option": {
|
||||
"cpu": "CPU",
|
||||
"memory": "内存",
|
||||
"gpu": "",
|
||||
"gpu": "GPU",
|
||||
"network": "网络"
|
||||
}
|
||||
},
|
||||
@@ -2829,7 +2829,7 @@
|
||||
"card": {
|
||||
"cpu": "CPU",
|
||||
"memory": "内存",
|
||||
"gpu": "",
|
||||
"gpu": "GPU",
|
||||
"network": "网络",
|
||||
"up": "上传",
|
||||
"down": "下载"
|
||||
@@ -3790,6 +3790,10 @@
|
||||
"title": "翻译人员",
|
||||
"subtitle": "在许多语言中,有{count}人贡献了翻译"
|
||||
},
|
||||
"sponsors": {
|
||||
"title": "",
|
||||
"subtitle": ""
|
||||
},
|
||||
"libraries": {
|
||||
"title": "相关的库",
|
||||
"subtitle": "{count} 用于 Homarr 代码中"
|
||||
|
||||
@@ -3790,6 +3790,10 @@
|
||||
"title": "crwdns1884:0crwdne1884:0",
|
||||
"subtitle": "crwdns1886:0{count}crwdne1886:0"
|
||||
},
|
||||
"sponsors": {
|
||||
"title": "crwdns4100:0crwdne4100:0",
|
||||
"subtitle": "crwdns4102:0{count}crwdne4102:0"
|
||||
},
|
||||
"libraries": {
|
||||
"title": "crwdns1888:0crwdne1888:0",
|
||||
"subtitle": "crwdns1890:0{count}crwdne1890:0"
|
||||
|
||||
@@ -3790,6 +3790,10 @@
|
||||
"title": "Překladatelé",
|
||||
"subtitle": "{count} přispívá překladem do mnoha jazyků"
|
||||
},
|
||||
"sponsors": {
|
||||
"title": "",
|
||||
"subtitle": ""
|
||||
},
|
||||
"libraries": {
|
||||
"title": "",
|
||||
"subtitle": ""
|
||||
|
||||
@@ -3790,6 +3790,10 @@
|
||||
"title": "Oversættere",
|
||||
"subtitle": "{count} bidragende oversættere på mange sprog"
|
||||
},
|
||||
"sponsors": {
|
||||
"title": "",
|
||||
"subtitle": ""
|
||||
},
|
||||
"libraries": {
|
||||
"title": "Biblioteker",
|
||||
"subtitle": "{count} brugt i koden for Homarr"
|
||||
|
||||
@@ -3790,6 +3790,10 @@
|
||||
"title": "Übersetzer",
|
||||
"subtitle": "{count} tragen Übersetzungen in viele Sprachen bei"
|
||||
},
|
||||
"sponsors": {
|
||||
"title": "",
|
||||
"subtitle": ""
|
||||
},
|
||||
"libraries": {
|
||||
"title": "Bibliotheken",
|
||||
"subtitle": "{count} im Code von Homarr verwendet"
|
||||
|
||||
@@ -3790,6 +3790,10 @@
|
||||
"title": "Übersetzer",
|
||||
"subtitle": "{count} tragen Übersetzungen in viele Sprachen bei"
|
||||
},
|
||||
"sponsors": {
|
||||
"title": "",
|
||||
"subtitle": ""
|
||||
},
|
||||
"libraries": {
|
||||
"title": "Bibliotheken",
|
||||
"subtitle": "{count} im Code von Homarr verwendet"
|
||||
|
||||
@@ -3790,6 +3790,10 @@
|
||||
"title": "",
|
||||
"subtitle": ""
|
||||
},
|
||||
"sponsors": {
|
||||
"title": "",
|
||||
"subtitle": ""
|
||||
},
|
||||
"libraries": {
|
||||
"title": "",
|
||||
"subtitle": ""
|
||||
|
||||
@@ -1607,32 +1607,32 @@
|
||||
}
|
||||
},
|
||||
"anchorNote": {
|
||||
"name": "",
|
||||
"description": "",
|
||||
"name": "Anchor Note",
|
||||
"description": "Displays a selected note from Anchor",
|
||||
"option": {
|
||||
"noteId": {
|
||||
"label": ""
|
||||
"label": "Note"
|
||||
},
|
||||
"showTitle": {
|
||||
"label": ""
|
||||
"label": "Show title"
|
||||
},
|
||||
"showUpdatedAt": {
|
||||
"label": ""
|
||||
"label": "Show last updated"
|
||||
}
|
||||
},
|
||||
"empty": "",
|
||||
"emptyContent": "",
|
||||
"notFound": "",
|
||||
"noNotes": "",
|
||||
"integrationRequired": "",
|
||||
"updatedAt": "",
|
||||
"untitled": "",
|
||||
"edit": "",
|
||||
"save": "",
|
||||
"cancel": "",
|
||||
"readOnlyViewer": "",
|
||||
"saveForbidden": "",
|
||||
"saveFailed": ""
|
||||
"empty": "Select a note to display",
|
||||
"emptyContent": "This note has no content",
|
||||
"notFound": "Note not found",
|
||||
"noNotes": "No notes found",
|
||||
"integrationRequired": "Select an Anchor integration first",
|
||||
"updatedAt": "Updated {date}",
|
||||
"untitled": "Untitled",
|
||||
"edit": "Edit",
|
||||
"save": "Save",
|
||||
"cancel": "Cancel",
|
||||
"readOnlyViewer": "You have viewer access to this note. Editing is disabled.",
|
||||
"saveForbidden": "You do not have permission to edit this note.",
|
||||
"saveFailed": "Failed to save this note. Please try again."
|
||||
},
|
||||
"iframe": {
|
||||
"name": "iFrame",
|
||||
@@ -3790,6 +3790,10 @@
|
||||
"title": "",
|
||||
"subtitle": ""
|
||||
},
|
||||
"sponsors": {
|
||||
"title": "",
|
||||
"subtitle": ""
|
||||
},
|
||||
"libraries": {
|
||||
"title": "",
|
||||
"subtitle": ""
|
||||
|
||||
@@ -3792,6 +3792,10 @@
|
||||
"title": "Translators",
|
||||
"subtitle": "{count} contributing translations in many languages"
|
||||
},
|
||||
"sponsors": {
|
||||
"title": "Sponsors",
|
||||
"subtitle": "{count} supporting the project financially"
|
||||
},
|
||||
"libraries": {
|
||||
"title": "Libraries",
|
||||
"subtitle": "{count} used in the Code of Homarr"
|
||||
|
||||
@@ -3790,6 +3790,10 @@
|
||||
"title": "Traductores",
|
||||
"subtitle": "{count} contribuyendo con las traducciones en muchos idiomas"
|
||||
},
|
||||
"sponsors": {
|
||||
"title": "",
|
||||
"subtitle": ""
|
||||
},
|
||||
"libraries": {
|
||||
"title": "Librerías",
|
||||
"subtitle": "{count} usadas en el Código de Homarr"
|
||||
|
||||
@@ -3790,6 +3790,10 @@
|
||||
"title": "",
|
||||
"subtitle": ""
|
||||
},
|
||||
"sponsors": {
|
||||
"title": "",
|
||||
"subtitle": ""
|
||||
},
|
||||
"libraries": {
|
||||
"title": "",
|
||||
"subtitle": ""
|
||||
|
||||
@@ -3790,6 +3790,10 @@
|
||||
"title": "Traducteurs",
|
||||
"subtitle": "{count} ont contribué à des traductions dans de nombreuses langues"
|
||||
},
|
||||
"sponsors": {
|
||||
"title": "",
|
||||
"subtitle": ""
|
||||
},
|
||||
"libraries": {
|
||||
"title": "Bibliothèques",
|
||||
"subtitle": "{count} utilisés dans le Code d'Homarr"
|
||||
|
||||
@@ -3790,6 +3790,10 @@
|
||||
"title": "מתרגמים",
|
||||
"subtitle": "{count} תורמים בתרגום לשפות שונות"
|
||||
},
|
||||
"sponsors": {
|
||||
"title": "",
|
||||
"subtitle": ""
|
||||
},
|
||||
"libraries": {
|
||||
"title": "ספריות",
|
||||
"subtitle": "{count} בשימוש בקוד של Homarr"
|
||||
|
||||
@@ -3790,6 +3790,10 @@
|
||||
"title": "",
|
||||
"subtitle": ""
|
||||
},
|
||||
"sponsors": {
|
||||
"title": "",
|
||||
"subtitle": ""
|
||||
},
|
||||
"libraries": {
|
||||
"title": "",
|
||||
"subtitle": ""
|
||||
|
||||
@@ -3790,6 +3790,10 @@
|
||||
"title": "",
|
||||
"subtitle": ""
|
||||
},
|
||||
"sponsors": {
|
||||
"title": "",
|
||||
"subtitle": ""
|
||||
},
|
||||
"libraries": {
|
||||
"title": "",
|
||||
"subtitle": ""
|
||||
|
||||
@@ -3790,6 +3790,10 @@
|
||||
"title": "Traduttori",
|
||||
"subtitle": "{count} contribuendo alle traduzioni in molte lingue"
|
||||
},
|
||||
"sponsors": {
|
||||
"title": "Sponsor",
|
||||
"subtitle": "{count} sostengono il progetto finanziariamente"
|
||||
},
|
||||
"libraries": {
|
||||
"title": "Librerie",
|
||||
"subtitle": "{count} utilizzato nel Codice di Homarr"
|
||||
|
||||
@@ -3790,6 +3790,10 @@
|
||||
"title": "翻訳者",
|
||||
"subtitle": "{count} の多くの言語で翻訳を行っている"
|
||||
},
|
||||
"sponsors": {
|
||||
"title": "",
|
||||
"subtitle": ""
|
||||
},
|
||||
"libraries": {
|
||||
"title": "ライブラリ",
|
||||
"subtitle": "{count} 個が Homarr のコードで使用されています"
|
||||
|
||||
@@ -3790,6 +3790,10 @@
|
||||
"title": "",
|
||||
"subtitle": ""
|
||||
},
|
||||
"sponsors": {
|
||||
"title": "",
|
||||
"subtitle": ""
|
||||
},
|
||||
"libraries": {
|
||||
"title": "",
|
||||
"subtitle": ""
|
||||
|
||||
@@ -3790,6 +3790,10 @@
|
||||
"title": "",
|
||||
"subtitle": ""
|
||||
},
|
||||
"sponsors": {
|
||||
"title": "",
|
||||
"subtitle": ""
|
||||
},
|
||||
"libraries": {
|
||||
"title": "",
|
||||
"subtitle": ""
|
||||
|
||||
@@ -3790,6 +3790,10 @@
|
||||
"title": "",
|
||||
"subtitle": ""
|
||||
},
|
||||
"sponsors": {
|
||||
"title": "",
|
||||
"subtitle": ""
|
||||
},
|
||||
"libraries": {
|
||||
"title": "",
|
||||
"subtitle": ""
|
||||
|
||||
@@ -3790,6 +3790,10 @@
|
||||
"title": "Vertalers",
|
||||
"subtitle": "{count} bijdragen aan vertalingen in vele talen"
|
||||
},
|
||||
"sponsors": {
|
||||
"title": "",
|
||||
"subtitle": ""
|
||||
},
|
||||
"libraries": {
|
||||
"title": "Bibliotheken",
|
||||
"subtitle": "{count} gebruikt in de code van Homarr"
|
||||
|
||||
@@ -1526,13 +1526,13 @@
|
||||
}
|
||||
},
|
||||
"timetable": {
|
||||
"name": "",
|
||||
"description": "",
|
||||
"title": "",
|
||||
"noStation": "",
|
||||
"name": "Timeplan",
|
||||
"description": "Viser avgangstid for en stasjon",
|
||||
"title": "Avganger {station}",
|
||||
"noStation": "Ingen stasjon valgt",
|
||||
"option": {
|
||||
"station": {
|
||||
"label": ""
|
||||
"label": "Stasjon"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -1607,32 +1607,32 @@
|
||||
}
|
||||
},
|
||||
"anchorNote": {
|
||||
"name": "",
|
||||
"description": "",
|
||||
"name": "Ankermerknad",
|
||||
"description": "Viser et valgt notat fra anker",
|
||||
"option": {
|
||||
"noteId": {
|
||||
"label": ""
|
||||
"label": "Notat"
|
||||
},
|
||||
"showTitle": {
|
||||
"label": ""
|
||||
"label": "Vis tittel"
|
||||
},
|
||||
"showUpdatedAt": {
|
||||
"label": ""
|
||||
"label": "Vis sist oppdatert"
|
||||
}
|
||||
},
|
||||
"empty": "",
|
||||
"emptyContent": "",
|
||||
"notFound": "",
|
||||
"noNotes": "",
|
||||
"integrationRequired": "",
|
||||
"updatedAt": "",
|
||||
"untitled": "",
|
||||
"edit": "",
|
||||
"save": "",
|
||||
"cancel": "",
|
||||
"readOnlyViewer": "",
|
||||
"saveForbidden": "",
|
||||
"saveFailed": ""
|
||||
"empty": "Velg et notat som skal vises",
|
||||
"emptyContent": "Dette notatet har ikke innhold",
|
||||
"notFound": "Notat ikke funnet",
|
||||
"noNotes": "Fant ingen notater",
|
||||
"integrationRequired": "Velg en ankerintegrasjon først",
|
||||
"updatedAt": "Oppdatert {date}",
|
||||
"untitled": "Uten navn",
|
||||
"edit": "Rediger",
|
||||
"save": "Lagre",
|
||||
"cancel": "Avbryt",
|
||||
"readOnlyViewer": "Du har seertilgang til dette notatet. Redigering er deaktivert.",
|
||||
"saveForbidden": "Du har ikke tillatelse til å redigere dette notatet.",
|
||||
"saveFailed": "Kunne ikke lagre dette notatet. Prøv på nytt."
|
||||
},
|
||||
"iframe": {
|
||||
"name": "iFrame",
|
||||
@@ -1666,7 +1666,7 @@
|
||||
"label": "Tillat geolokalisering"
|
||||
},
|
||||
"allowModals": {
|
||||
"label": ""
|
||||
"label": "Tillat modaler"
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
@@ -1901,7 +1901,7 @@
|
||||
"label": "Vis minneinfo"
|
||||
},
|
||||
"gpu": {
|
||||
"label": ""
|
||||
"label": "Vis CPU-info"
|
||||
},
|
||||
"showUptime": {
|
||||
"label": "Vis oppetid"
|
||||
@@ -2042,58 +2042,58 @@
|
||||
}
|
||||
},
|
||||
"tracearr": {
|
||||
"name": "",
|
||||
"description": "",
|
||||
"name": "Tracearr",
|
||||
"description": "Forbedret medieserverovervåking med detaljert strøm- og transkodingsdata",
|
||||
"option": {
|
||||
"showStreams": {
|
||||
"label": ""
|
||||
"label": "Vis aktive strømmer"
|
||||
},
|
||||
"showStats": {
|
||||
"label": ""
|
||||
"label": "Vis statistikksammendrag"
|
||||
},
|
||||
"showRecentActivity": {
|
||||
"label": ""
|
||||
"label": "Vis nylig aktivitet"
|
||||
},
|
||||
"showViolations": {
|
||||
"label": ""
|
||||
"label": "Vis brudd"
|
||||
}
|
||||
},
|
||||
"stats": {
|
||||
"activeStreams": "",
|
||||
"totalUsers": "",
|
||||
"transcodes": "",
|
||||
"bandwidth": ""
|
||||
"activeStreams": "Aktive strømmninger",
|
||||
"totalUsers": "Totalt antall brukere",
|
||||
"transcodes": "Transkoder",
|
||||
"bandwidth": "Båndbredde"
|
||||
},
|
||||
"streams": {
|
||||
"empty": "",
|
||||
"user": "",
|
||||
"media": "",
|
||||
"state": "",
|
||||
"quality": "",
|
||||
"device": "",
|
||||
"progress": "",
|
||||
"empty": "Ingen aktive strømminger",
|
||||
"user": "Bruker",
|
||||
"media": "Media",
|
||||
"state": "Tilstand",
|
||||
"quality": "Kvalitet",
|
||||
"device": "Enhet",
|
||||
"progress": "Fremgang",
|
||||
"videoDecision": {
|
||||
"directPlay": "",
|
||||
"transcode": "",
|
||||
"directStream": ""
|
||||
"directPlay": "Direkte avspilling",
|
||||
"transcode": "Transkode",
|
||||
"directStream": "Direktestrøm"
|
||||
}
|
||||
},
|
||||
"recentActivity": {
|
||||
"title": "",
|
||||
"empty": "",
|
||||
"watched": "",
|
||||
"partial": ""
|
||||
"title": "Nylig aktivitet",
|
||||
"empty": "Ingen nylig aktivitet",
|
||||
"watched": "Sett",
|
||||
"partial": "Delvis"
|
||||
},
|
||||
"violations": {
|
||||
"title": "",
|
||||
"empty": "",
|
||||
"rule": "",
|
||||
"user": ""
|
||||
"title": "Brudd",
|
||||
"empty": "Ingen nylige brudd",
|
||||
"rule": "Regel",
|
||||
"user": "Bruker"
|
||||
},
|
||||
"noSectionsEnabled": "",
|
||||
"noSectionsEnabled": "Ingen seksjoner aktivert",
|
||||
"error": {
|
||||
"noIntegration": "",
|
||||
"internalServerError": ""
|
||||
"noIntegration": "Ingen Tracearr-integrasjon konfigurert",
|
||||
"internalServerError": "Kunne ikke hente Tracearr data"
|
||||
}
|
||||
},
|
||||
"common": {
|
||||
@@ -2546,7 +2546,7 @@
|
||||
"label": "Skjul beskrivelse"
|
||||
},
|
||||
"showPosterImage": {
|
||||
"label": ""
|
||||
"label": "Vis plakatbilde"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -2761,42 +2761,42 @@
|
||||
"option": {}
|
||||
},
|
||||
"speedtestTracker": {
|
||||
"name": "",
|
||||
"description": "",
|
||||
"name": "Speedtest tracker",
|
||||
"description": "Viser hastighetstesten i fra din Speedtest Tracker-instans.",
|
||||
"option": {
|
||||
"showLatestResult": {
|
||||
"label": ""
|
||||
"label": "Vis siste resultat"
|
||||
},
|
||||
"showStats": {
|
||||
"label": ""
|
||||
"label": "Vis gjennomsnitt"
|
||||
},
|
||||
"showRecentResults": {
|
||||
"label": ""
|
||||
"label": "Vis nylige resultater"
|
||||
},
|
||||
"showPingGraph": {
|
||||
"label": ""
|
||||
"label": "Vis ping-graf"
|
||||
}
|
||||
},
|
||||
"latestResult": "",
|
||||
"averages": "",
|
||||
"tests": "",
|
||||
"recentResults": "",
|
||||
"recentResultsSpeed": "",
|
||||
"recentResultsPing": "",
|
||||
"download": "",
|
||||
"upload": "",
|
||||
"ping": "",
|
||||
"status": "",
|
||||
"avgDownload": "",
|
||||
"avgUpload": "",
|
||||
"avgPing": "",
|
||||
"healthy": "",
|
||||
"unhealthy": "",
|
||||
"ok": "",
|
||||
"fail": "",
|
||||
"noSectionsEnabled": "",
|
||||
"latestResult": "Siste resultat",
|
||||
"averages": "Gjennomsnitt",
|
||||
"tests": "{count} tester",
|
||||
"recentResults": "Nylige resultater",
|
||||
"recentResultsSpeed": "Hastighet",
|
||||
"recentResultsPing": "Ping",
|
||||
"download": "Nedlasting",
|
||||
"upload": "Opplastning",
|
||||
"ping": "Ping",
|
||||
"status": "Status",
|
||||
"avgDownload": "Snitt nedlasting",
|
||||
"avgUpload": "Snitt opplasting",
|
||||
"avgPing": "Snitt ping",
|
||||
"healthy": "Frisk",
|
||||
"unhealthy": "Usunn",
|
||||
"ok": "OK",
|
||||
"fail": "Feil",
|
||||
"noSectionsEnabled": "Aktiver minst ett avsnitt i widgetalternativer.",
|
||||
"error": {
|
||||
"internalServerError": ""
|
||||
"internalServerError": "Kunne ikke hente hastighetsteste data"
|
||||
}
|
||||
},
|
||||
"systemResources": {
|
||||
@@ -2812,7 +2812,7 @@
|
||||
"option": {
|
||||
"cpu": "CPU",
|
||||
"memory": "Minne",
|
||||
"gpu": "",
|
||||
"gpu": "GPU",
|
||||
"network": "Nettverk"
|
||||
}
|
||||
},
|
||||
@@ -2829,7 +2829,7 @@
|
||||
"card": {
|
||||
"cpu": "CPU",
|
||||
"memory": "Minne",
|
||||
"gpu": "",
|
||||
"gpu": "GPU",
|
||||
"network": "NETT",
|
||||
"up": "OPP",
|
||||
"down": "NED"
|
||||
@@ -3595,7 +3595,7 @@
|
||||
"label": "Minecraft serverstatus"
|
||||
},
|
||||
"timetable": {
|
||||
"label": ""
|
||||
"label": "Timeplan"
|
||||
},
|
||||
"iconsUpdater": {
|
||||
"label": "Ikon oppdatering"
|
||||
@@ -3667,7 +3667,7 @@
|
||||
"label": "Vær"
|
||||
},
|
||||
"tracearr": {
|
||||
"label": ""
|
||||
"label": "Tracearr"
|
||||
}
|
||||
},
|
||||
"interval": {
|
||||
@@ -3790,6 +3790,10 @@
|
||||
"title": "Oversettere",
|
||||
"subtitle": "{count} bidrar med oversettelser på mange språk"
|
||||
},
|
||||
"sponsors": {
|
||||
"title": "Sponsorer",
|
||||
"subtitle": "{count} støtter prosjektet økonomisk"
|
||||
},
|
||||
"libraries": {
|
||||
"title": "Biblioteker",
|
||||
"subtitle": "{count} brukt i koden til Homarr"
|
||||
|
||||
@@ -3790,6 +3790,10 @@
|
||||
"title": "Tłumacze",
|
||||
"subtitle": "{count} współtworzy tłumaczenia w wielu językach"
|
||||
},
|
||||
"sponsors": {
|
||||
"title": "",
|
||||
"subtitle": ""
|
||||
},
|
||||
"libraries": {
|
||||
"title": "Biblioteki",
|
||||
"subtitle": "{count} używanych w kodzie Homarr"
|
||||
|
||||
@@ -164,7 +164,7 @@
|
||||
},
|
||||
"action": {
|
||||
"login": {
|
||||
"label": "“Login”",
|
||||
"label": "Login",
|
||||
"labelWith": "Iniciar sessão com {provider}",
|
||||
"notification": {
|
||||
"success": {
|
||||
@@ -3790,6 +3790,10 @@
|
||||
"title": "Tradutores",
|
||||
"subtitle": "{count} traduções contribuídas em vários idiomas"
|
||||
},
|
||||
"sponsors": {
|
||||
"title": "Apoiadores",
|
||||
"subtitle": "{count} apoiando o projeto financeiramente"
|
||||
},
|
||||
"libraries": {
|
||||
"title": "Bibliotecas",
|
||||
"subtitle": "{count} usadas no código do Homarr"
|
||||
|
||||
@@ -3790,6 +3790,10 @@
|
||||
"title": "",
|
||||
"subtitle": ""
|
||||
},
|
||||
"sponsors": {
|
||||
"title": "",
|
||||
"subtitle": ""
|
||||
},
|
||||
"libraries": {
|
||||
"title": "",
|
||||
"subtitle": ""
|
||||
|
||||
@@ -3790,6 +3790,10 @@
|
||||
"title": "Переводчики",
|
||||
"subtitle": "{count} помогают с переводами на разные языки"
|
||||
},
|
||||
"sponsors": {
|
||||
"title": "",
|
||||
"subtitle": ""
|
||||
},
|
||||
"libraries": {
|
||||
"title": "Библиотеки",
|
||||
"subtitle": "{count} используются в коде Homarr"
|
||||
|
||||
@@ -3790,6 +3790,10 @@
|
||||
"title": "Prekladatelia",
|
||||
"subtitle": "{count} prispievanie prekladmi do mnohých jazykov"
|
||||
},
|
||||
"sponsors": {
|
||||
"title": "",
|
||||
"subtitle": ""
|
||||
},
|
||||
"libraries": {
|
||||
"title": "Knižnice",
|
||||
"subtitle": "{count} používané v Homarrskom kódexe"
|
||||
|
||||
@@ -3790,6 +3790,10 @@
|
||||
"title": "",
|
||||
"subtitle": ""
|
||||
},
|
||||
"sponsors": {
|
||||
"title": "",
|
||||
"subtitle": ""
|
||||
},
|
||||
"libraries": {
|
||||
"title": "",
|
||||
"subtitle": ""
|
||||
|
||||
@@ -3790,6 +3790,10 @@
|
||||
"title": "Översättare",
|
||||
"subtitle": "{count} bidrar med översättningar på flera olika språk"
|
||||
},
|
||||
"sponsors": {
|
||||
"title": "",
|
||||
"subtitle": ""
|
||||
},
|
||||
"libraries": {
|
||||
"title": "",
|
||||
"subtitle": ""
|
||||
|
||||
@@ -3790,6 +3790,10 @@
|
||||
"title": "Çevirmenler",
|
||||
"subtitle": "Birçok kişi Homarr'ın {count} dile çevrilmesine katkı sağlıyor"
|
||||
},
|
||||
"sponsors": {
|
||||
"title": "",
|
||||
"subtitle": ""
|
||||
},
|
||||
"libraries": {
|
||||
"title": "Kütüphaneler",
|
||||
"subtitle": "Homarr kodlamasında {count} kütüphane kullanılıyor"
|
||||
|
||||
@@ -3790,6 +3790,10 @@
|
||||
"title": "Перекладачі",
|
||||
"subtitle": "{count} - стільки людей допомагають перекладати багатьма мовами"
|
||||
},
|
||||
"sponsors": {
|
||||
"title": "",
|
||||
"subtitle": ""
|
||||
},
|
||||
"libraries": {
|
||||
"title": "Бібліотеки",
|
||||
"subtitle": "{count} - стільки бібліотек використано в коді Homarr"
|
||||
|
||||
@@ -3790,6 +3790,10 @@
|
||||
"title": "",
|
||||
"subtitle": ""
|
||||
},
|
||||
"sponsors": {
|
||||
"title": "",
|
||||
"subtitle": ""
|
||||
},
|
||||
"libraries": {
|
||||
"title": "",
|
||||
"subtitle": ""
|
||||
|
||||
@@ -3790,6 +3790,10 @@
|
||||
"title": "翻譯人員",
|
||||
"subtitle": "{count} 翻譯了許多語言"
|
||||
},
|
||||
"sponsors": {
|
||||
"title": "",
|
||||
"subtitle": ""
|
||||
},
|
||||
"libraries": {
|
||||
"title": "資料庫",
|
||||
"subtitle": "{count} 於 Homarr 待碼中使用"
|
||||
|
||||
@@ -138,14 +138,10 @@ export const userChangeColorSchemeSchema = z.object({
|
||||
|
||||
export const userFirstDayOfWeekSchema = z.object({
|
||||
firstDayOfWeek: z
|
||||
.custom<DayOfWeek>((value) => z.number().min(0).max(6).safeParse(value).success)
|
||||
.meta({
|
||||
override: {
|
||||
type: "integer",
|
||||
minimum: 0,
|
||||
maximum: 6,
|
||||
},
|
||||
}),
|
||||
.number()
|
||||
.min(0)
|
||||
.max(6)
|
||||
.transform((value) => value as DayOfWeek),
|
||||
});
|
||||
|
||||
export const userPingIconsEnabledSchema = z.object({
|
||||
|
||||
Reference in new issue
Block a user