diff --git a/packages/backend/migration/1682777547198-LibreTranslate.js b/packages/backend/migration/1682777547198-LibreTranslate.js
new file mode 100644
index 000000000..dbaf483e6
--- /dev/null
+++ b/packages/backend/migration/1682777547198-LibreTranslate.js
@@ -0,0 +1,23 @@
+export class LibreTranslate1682777547198 {
+	name = "LibreTranslate1682777547198";
+
+	async up(queryRunner) {
+		await queryRunner.query(`
+				ALTER TABLE "meta"
+				ADD "libreTranslateApiUrl" character varying(512)
+		`);
+		await queryRunner.query(`
+				ALTER TABLE "meta"
+				ADD "libreTranslateApiKey" character varying(128)
+		`);
+	}
+
+	async down(queryRunner) {
+		await queryRunner.query(`
+				ALTER TABLE "meta" DROP COLUMN "libreTranslateApiKey"
+		`);
+		await queryRunner.query(`
+				ALTER TABLE "meta" DROP COLUMN "libreTranslateApiUrl"
+		`);
+	}
+}
diff --git a/packages/backend/src/config/types.ts b/packages/backend/src/config/types.ts
index 4f367debe..0cd8c02ad 100644
--- a/packages/backend/src/config/types.ts
+++ b/packages/backend/src/config/types.ts
@@ -89,6 +89,11 @@ export type Source = {
 		authKey?: string;
 		isPro?: boolean;
 	};
+	libreTranslate: {
+		managed?: boolean;
+		apiUrl?: string;
+		apiKey?: string;
+	};
 	email: {
 		managed?: boolean;
 		address?: string;
diff --git a/packages/backend/src/models/entities/meta.ts b/packages/backend/src/models/entities/meta.ts
index 26a7c9c19..2f77796c4 100644
--- a/packages/backend/src/models/entities/meta.ts
+++ b/packages/backend/src/models/entities/meta.ts
@@ -386,6 +386,18 @@ export class Meta {
 	})
 	public deeplIsPro: boolean;
 
+	@Column('varchar', {
+		length: 512,
+		nullable: true,
+	})
+	public libreTranslateApiUrl: string | null;
+
+	@Column('varchar', {
+		length: 128,
+		nullable: true,
+	})
+	public libreTranslateApiKey: string | null;
+
 	@Column('varchar', {
 		length: 512,
 		nullable: true,
diff --git a/packages/backend/src/server/api/endpoints/admin/accounts/hosted.ts b/packages/backend/src/server/api/endpoints/admin/accounts/hosted.ts
index 15ad1f9a1..a7b6e95c2 100644
--- a/packages/backend/src/server/api/endpoints/admin/accounts/hosted.ts
+++ b/packages/backend/src/server/api/endpoints/admin/accounts/hosted.ts
@@ -30,6 +30,17 @@ export default define(meta, paramDef, async (ps, me) => {
 				set.deeplIsPro = config.deepl.isPro;
 			}
 		}
+		if (
+			config.libreTranslate.managed != null &&
+			config.libreTranslate.managed === true
+		) {
+			if (typeof config.libreTranslate.apiUrl === "string") {
+				set.libreTranslateApiUrl = config.libreTranslate.apiUrl;
+			}
+			if (typeof config.libreTranslate.apiKey === "string") {
+				set.libreTranslateApiKey = config.libreTranslate.apiKey;
+			}
+		}
 		if (config.email.managed != null && config.email.managed === true) {
 			set.enableEmail = true;
 			if (typeof config.email.address === "string") {
diff --git a/packages/backend/src/server/api/endpoints/admin/meta.ts b/packages/backend/src/server/api/endpoints/admin/meta.ts
index c8c639f50..f0ac57892 100644
--- a/packages/backend/src/server/api/endpoints/admin/meta.ts
+++ b/packages/backend/src/server/api/endpoints/admin/meta.ts
@@ -512,7 +512,8 @@ export default define(meta, paramDef, async (ps, me) => {
 		enableGithubIntegration: instance.enableGithubIntegration,
 		enableDiscordIntegration: instance.enableDiscordIntegration,
 		enableServiceWorker: instance.enableServiceWorker,
-		translatorAvailable: instance.deeplAuthKey != null,
+		translatorAvailable:
+			instance.deeplAuthKey != null || instance.libreTranslateApiUrl != null,
 		pinnedPages: instance.pinnedPages,
 		pinnedClipId: instance.pinnedClipId,
 		cacheRemoteFiles: instance.cacheRemoteFiles,
@@ -564,6 +565,8 @@ export default define(meta, paramDef, async (ps, me) => {
 		objectStorageS3ForcePathStyle: instance.objectStorageS3ForcePathStyle,
 		deeplAuthKey: instance.deeplAuthKey,
 		deeplIsPro: instance.deeplIsPro,
+		libreTranslateApiUrl: instance.libreTranslateApiUrl,
+		libreTranslateApiKey: instance.libreTranslateApiKey,
 		enableIpLogging: instance.enableIpLogging,
 		enableActiveEmailValidation: instance.enableActiveEmailValidation,
 	};
diff --git a/packages/backend/src/server/api/endpoints/admin/update-meta.ts b/packages/backend/src/server/api/endpoints/admin/update-meta.ts
index f7e79b64b..a23000732 100644
--- a/packages/backend/src/server/api/endpoints/admin/update-meta.ts
+++ b/packages/backend/src/server/api/endpoints/admin/update-meta.ts
@@ -124,6 +124,8 @@ export const paramDef = {
 		summalyProxy: { type: "string", nullable: true },
 		deeplAuthKey: { type: "string", nullable: true },
 		deeplIsPro: { type: "boolean" },
+		libreTranslateApiUrl: { type: "string", nullable: true },
+		libreTranslateApiKey: { type: "string", nullable: true },
 		enableTwitterIntegration: { type: "boolean" },
 		twitterConsumerKey: { type: "string", nullable: true },
 		twitterConsumerSecret: { type: "string", nullable: true },
@@ -515,6 +517,22 @@ export default define(meta, paramDef, async (ps, me) => {
 		set.deeplIsPro = ps.deeplIsPro;
 	}
 
+	if (ps.libreTranslateApiUrl !== undefined) {
+		if (ps.libreTranslateApiUrl === "") {
+			set.libreTranslateApiUrl = null;
+		} else {
+			set.libreTranslateApiUrl = ps.libreTranslateApiUrl;
+		}
+	}
+
+	if (ps.libreTranslateApiKey !== undefined) {
+		if (ps.libreTranslateApiKey === "") {
+			set.libreTranslateApiKey = null;
+		} else {
+			set.libreTranslateApiKey = ps.libreTranslateApiKey;
+		}
+	}
+
 	if (ps.enableIpLogging !== undefined) {
 		set.enableIpLogging = ps.enableIpLogging;
 	}
diff --git a/packages/backend/src/server/api/endpoints/meta.ts b/packages/backend/src/server/api/endpoints/meta.ts
index 4dc1c941e..23989750f 100644
--- a/packages/backend/src/server/api/endpoints/meta.ts
+++ b/packages/backend/src/server/api/endpoints/meta.ts
@@ -482,7 +482,8 @@ export default define(meta, paramDef, async (ps, me) => {
 
 		enableServiceWorker: instance.enableServiceWorker,
 
-		translatorAvailable: instance.deeplAuthKey != null,
+		translatorAvailable:
+			instance.deeplAuthKey != null || instance.libreTranslateApiUrl != null,
 		defaultReaction: instance.defaultReaction,
 
 		...(ps.detail
diff --git a/packages/backend/src/server/api/endpoints/notes/translate.ts b/packages/backend/src/server/api/endpoints/notes/translate.ts
index c6415ceef..d86fc12a2 100644
--- a/packages/backend/src/server/api/endpoints/notes/translate.ts
+++ b/packages/backend/src/server/api/endpoints/notes/translate.ts
@@ -51,15 +51,54 @@ export default define(meta, paramDef, async (ps, user) => {
 
 	const instance = await fetchMeta();
 
-	if (instance.deeplAuthKey == null) {
+	if (instance.deeplAuthKey == null && instance.libreTranslateApiUrl == null) {
 		return 204; // TODO: 良い感じのエラー返す
 	}
 
 	let targetLang = ps.targetLang;
 	if (targetLang.includes("-")) targetLang = targetLang.split("-")[0];
 
+	if (instance.libreTranslateApiUrl != null) {
+		const jsonBody = {
+			q: note.text,
+			source: "auto",
+			target: targetLang,
+			format: "text",
+			api_key: instance.libreTranslateApiKey ?? "",
+		};
+
+		const url = new URL(instance.libreTranslateApiUrl);
+		if (url.pathname.endsWith("/")) {
+			url.pathname = url.pathname.slice(0, -1);
+		}
+		if (!url.pathname.endsWith("/translate")) {
+			url.pathname += "/translate";
+		}
+		const res = await fetch(url.toString(), {
+			method: "POST",
+			headers: {
+				"Content-Type": "application/json",
+			},
+			body: JSON.stringify(jsonBody),
+			agent: getAgentByUrl,
+		});
+
+		const json = (await res.json()) as {
+			detectedLanguage?: {
+				confidence: number;
+				language: string;
+			};
+			translatedText: string;
+		};
+
+		return {
+			sourceLang: json.detectedLanguage?.language,
+			text: json.translatedText,
+		};
+	}
+
 	const params = new URLSearchParams();
-	params.append("auth_key", instance.deeplAuthKey);
+	params.append("auth_key", instance.deeplAuthKey ?? "");
 	params.append("text", note.text);
 	params.append("target_lang", targetLang);
 
diff --git a/packages/calckey-js/.eslintignore b/packages/calckey-js/.eslintignore
deleted file mode 100644
index f22128f04..000000000
--- a/packages/calckey-js/.eslintignore
+++ /dev/null
@@ -1,7 +0,0 @@
-node_modules
-/built
-/coverage
-/.eslintrc.js
-/jest.config.ts
-/test
-/test-d
diff --git a/packages/calckey-js/.eslintrc.js b/packages/calckey-js/.eslintrc.js
deleted file mode 100644
index 164cf1fbe..000000000
--- a/packages/calckey-js/.eslintrc.js
+++ /dev/null
@@ -1,65 +0,0 @@
-module.exports = {
-	root: true,
-	parser: "@typescript-eslint/parser",
-	parserOptions: {
-		tsconfigRootDir: __dirname,
-		project: ["./tsconfig.json"],
-	},
-	plugins: ["@typescript-eslint"],
-	extends: ["eslint:recommended", "plugin:@typescript-eslint/recommended"],
-	rules: {
-		indent: [
-			"error",
-			"tab",
-			{
-				SwitchCase: 1,
-				MemberExpression: "off",
-				flatTernaryExpressions: true,
-				ArrayExpression: "first",
-				ObjectExpression: "first",
-			},
-		],
-		"eol-last": ["error", "always"],
-		semi: ["error", "always"],
-		quotes: ["error", "single"],
-		"comma-dangle": ["error", "always-multiline"],
-		"keyword-spacing": [
-			"error",
-			{
-				before: true,
-				after: true,
-			},
-		],
-		"key-spacing": [
-			"error",
-			{
-				beforeColon: false,
-				afterColon: true,
-			},
-		],
-		"space-infix-ops": ["error"],
-		"space-before-blocks": ["error", "always"],
-		"object-curly-spacing": ["error", "always"],
-		"nonblock-statement-body-position": ["error", "beside"],
-		eqeqeq: ["error", "always", { null: "ignore" }],
-		"no-multiple-empty-lines": ["error", { max: 1 }],
-		"no-multi-spaces": ["error"],
-		"no-var": ["error"],
-		"prefer-arrow-callback": ["error"],
-		"no-throw-literal": ["error"],
-		"no-param-reassign": ["warn"],
-		"no-constant-condition": ["warn"],
-		"no-empty-pattern": ["warn"],
-		"@typescript-eslint/no-unnecessary-condition": ["error"],
-		"@typescript-eslint/no-inferrable-types": ["warn"],
-		"@typescript-eslint/no-non-null-assertion": ["warn"],
-		"@typescript-eslint/explicit-function-return-type": ["warn"],
-		"@typescript-eslint/no-misused-promises": [
-			"error",
-			{
-				checksVoidReturn: false,
-			},
-		],
-		"@typescript-eslint/consistent-type-imports": "error",
-	},
-};
diff --git a/packages/calckey-js/package.json b/packages/calckey-js/package.json
index d68f24175..598dd1cdb 100644
--- a/packages/calckey-js/package.json
+++ b/packages/calckey-js/package.json
@@ -9,9 +9,8 @@
 		"tsd": "tsd",
 		"api": "pnpm api-extractor run --local --verbose",
 		"api-prod": "pnpm api-extractor run --verbose",
-		"eslint": "eslint . --ext .js,.jsx,.ts,.tsx",
 		"typecheck": "tsc --noEmit",
-		"lint": "pnpm typecheck && pnpm eslint",
+		"lint": "pnpm typecheck && pnpm rome check \"src/*.ts\"",
 		"jest": "jest --coverage --detectOpenHandles",
 		"test": "pnpm jest && pnpm tsd"
 	},
diff --git a/packages/client/src/pages/admin/settings.vue b/packages/client/src/pages/admin/settings.vue
index 5349df805..feedaff6d 100644
--- a/packages/client/src/pages/admin/settings.vue
+++ b/packages/client/src/pages/admin/settings.vue
@@ -371,6 +371,34 @@
 								<template #label>Pro account</template>
 							</FormSwitch>
 						</FormSection>
+
+						<FormSection>
+							<template #label>Libre Translate</template>
+
+							<FormInput
+								v-model="libreTranslateApiUrl"
+								class="_formBlock"
+							>
+								<template #prefix
+									><i class="ph-link ph-bold ph-lg"></i
+								></template>
+								<template #label
+									>Libre Translate API URL</template
+								>
+							</FormInput>
+
+							<FormInput
+								v-model="libreTranslateApiKey"
+								class="_formBlock"
+							>
+								<template #prefix
+									><i class="ph-key ph-bold ph-lg"></i
+								></template>
+								<template #label
+									>Libre Translate API Key</template
+								>
+							</FormInput>
+						</FormSection>
 					</div>
 				</FormSuspense>
 			</MkSpacer>
@@ -422,6 +450,8 @@ let swPublicKey: any = $ref(null);
 let swPrivateKey: any = $ref(null);
 let deeplAuthKey: string = $ref("");
 let deeplIsPro: boolean = $ref(false);
+let libreTranslateApiUrl: string = $ref("");
+let libreTranslateApiKey: string = $ref("");
 let defaultReaction: string = $ref("");
 let defaultReactionCustom: string = $ref("");
 
@@ -456,6 +486,8 @@ async function init() {
 	swPrivateKey = meta.swPrivateKey;
 	deeplAuthKey = meta.deeplAuthKey;
 	deeplIsPro = meta.deeplIsPro;
+	libreTranslateApiUrl = meta.libreTranslateApiUrl;
+	libreTranslateApiKey = meta.libreTranslateApiKey;
 	defaultReaction = ["⭐", "👍", "❤️"].includes(meta.defaultReaction)
 		? meta.defaultReaction
 		: "custom";
@@ -498,6 +530,8 @@ function save() {
 		swPrivateKey,
 		deeplAuthKey,
 		deeplIsPro,
+		libreTranslateApiUrl,
+		libreTranslateApiKey,
 		defaultReaction,
 	}).then(() => {
 		fetchInstance();