diff --git a/packages/client/src/pages/admin/ads.vue b/packages/client/src/pages/admin/ads.vue
index 8f164caa9..b18e08db9 100644
--- a/packages/client/src/pages/admin/ads.vue
+++ b/packages/client/src/pages/admin/ads.vue
@@ -7,7 +7,7 @@
 				<template #label>URL</template>
 			</MkInput>
 			<MkInput v-model="ad.imageUrl" class="_formBlock">
-				<template #label>{{ $ts.imageUrl }}</template>
+				<template #label>{{ i18n.ts.imageUrl }}</template>
 			</MkInput>
 			<FormRadios v-model="ad.place" class="_formBlock">
 				<template #label>Form</template>
@@ -17,34 +17,34 @@
 			</FormRadios>
 			<!--
 			<div style="margin: 32px 0;">
-				{{ $ts.priority }}
-				<MkRadio v-model="ad.priority" value="high">{{ $ts.high }}</MkRadio>
-				<MkRadio v-model="ad.priority" value="middle">{{ $ts.middle }}</MkRadio>
-				<MkRadio v-model="ad.priority" value="low">{{ $ts.low }}</MkRadio>
+				{{ i18n.ts.priority }}
+				<MkRadio v-model="ad.priority" value="high">{{ i18n.ts.high }}</MkRadio>
+				<MkRadio v-model="ad.priority" value="middle">{{ i18n.ts.middle }}</MkRadio>
+				<MkRadio v-model="ad.priority" value="low">{{ i18n.ts.low }}</MkRadio>
 			</div>
 			-->
 			<FormSplit>
 				<MkInput v-model="ad.ratio" type="number">
-					<template #label>{{ $ts.ratio }}</template>
+					<template #label>{{ i18n.ts.ratio }}</template>
 				</MkInput>
 				<MkInput v-model="ad.expiresAt" type="date">
-					<template #label>{{ $ts.expiration }}</template>
+					<template #label>{{ i18n.ts.expiration }}</template>
 				</MkInput>
 			</FormSplit>
 			<MkTextarea v-model="ad.memo" class="_formBlock">
-				<template #label>{{ $ts.memo }}</template>
+				<template #label>{{ i18n.ts.memo }}</template>
 			</MkTextarea>
 			<div class="buttons _formBlock">
-				<MkButton class="button" inline primary style="margin-right: 12px;" @click="save(ad)"><i class="fas fa-save"></i> {{ $ts.save }}</MkButton>
-				<MkButton class="button" inline danger @click="remove(ad)"><i class="fas fa-trash-alt"></i> {{ $ts.remove }}</MkButton>
+				<MkButton class="button" inline primary style="margin-right: 12px;" @click="save(ad)"><i class="fas fa-save"></i> {{ i18n.ts.save }}</MkButton>
+				<MkButton class="button" inline danger @click="remove(ad)"><i class="fas fa-trash-alt"></i> {{ i18n.ts.remove }}</MkButton>
 			</div>
 		</div>
 	</div>
 </MkSpacer>
 </template>
 
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { } from 'vue';
 import MkButton from '@/components/ui/button.vue';
 import MkInput from '@/components/form/input.vue';
 import MkTextarea from '@/components/form/textarea.vue';
@@ -52,81 +52,65 @@ import FormRadios from '@/components/form/radios.vue';
 import FormSplit from '@/components/form/split.vue';
 import * as os from '@/os';
 import * as symbols from '@/symbols';
+import { i18n } from '@/i18n';
 
-export default defineComponent({
-	components: {
-		MkButton,
-		MkInput,
-		MkTextarea,
-		FormRadios,
-		FormSplit,
-	},
+let ads: any[] = $ref([]);
 
-	emits: ['info'],
+os.api('admin/ad/list').then(adsResponse => {
+	ads = adsResponse;
+});
 
-	data() {
-		return {
-			[symbols.PAGE_INFO]: {
-				title: this.$ts.ads,
-				icon: 'fas fa-audio-description',
-				bg: 'var(--bg)',
-				actions: [{
-					asFullButton: true,
-					icon: 'fas fa-plus',
-					text: this.$ts.add,
-					handler: this.add,
-				}],
-			},
-			ads: [],
-		}
-	},
+function add() {
+	ads.unshift({
+		id: null,
+		memo: '',
+		place: 'square',
+		priority: 'middle',
+		ratio: 1,
+		url: '',
+		imageUrl: null,
+		expiresAt: null,
+	});
+}
 
-	created() {
-		os.api('admin/ad/list').then(ads => {
-			this.ads = ads;
+function remove(ad) {
+	os.confirm({
+		type: 'warning',
+		text: i18n.t('removeAreYouSure', { x: ad.url }),
+	}).then(({ canceled }) => {
+		if (canceled) return;
+		ads = ads.filter(x => x !== ad);
+		os.apiWithDialog('admin/ad/delete', {
+			id: ad.id
 		});
-	},
+	});
+}
 
-	methods: {
-		add() {
-			this.ads.unshift({
-				id: null,
-				memo: '',
-				place: 'square',
-				priority: 'middle',
-				ratio: 1,
-				url: '',
-				imageUrl: null,
-				expiresAt: null,
-			});
-		},
+function save(ad) {
+	if (ad.id == null) {
+		os.apiWithDialog('admin/ad/create', {
+			...ad,
+			expiresAt: new Date(ad.expiresAt).getTime()
+		});
+	} else {
+		os.apiWithDialog('admin/ad/update', {
+			...ad,
+			expiresAt: new Date(ad.expiresAt).getTime()
+		});
+	}
+}
 
-		remove(ad) {
-			os.confirm({
-				type: 'warning',
-				text: this.$t('removeAreYouSure', { x: ad.url }),
-			}).then(({ canceled }) => {
-				if (canceled) return;
-				this.ads = this.ads.filter(x => x != ad);
-				os.apiWithDialog('admin/ad/delete', {
-					id: ad.id
-				});
-			});
-		},
-
-		save(ad) {
-			if (ad.id == null) {
-				os.apiWithDialog('admin/ad/create', {
-					...ad,
-					expiresAt: new Date(ad.expiresAt).getTime()
-				});
-			} else {
-				os.apiWithDialog('admin/ad/update', {
-					...ad,
-					expiresAt: new Date(ad.expiresAt).getTime()
-				});
-			}
-		}
+defineExpose({
+	[symbols.PAGE_INFO]: {
+		title: i18n.ts.ads,
+		icon: 'fas fa-audio-description',
+		bg: 'var(--bg)',
+		actions: [{
+			asFullButton: true,
+			icon: 'fas fa-plus',
+			text: i18n.ts.add,
+			handler: add,
+		}],
 	}
 });
 </script>