tweak client
This commit is contained in:
		
							parent
							
								
									a6fff86099
								
							
						
					
					
						commit
						f880d0631c
					
				
					 8 changed files with 175 additions and 167 deletions
				
			
		|  | @ -848,6 +848,8 @@ rateLimitExceeded: "レート制限を超えました" | |||
| cropImage: "画像のクロップ" | ||||
| cropImageAsk: "画像をクロップしますか?" | ||||
| file: "ファイル" | ||||
| recentNHours: "直近{n}時間" | ||||
| recentNDays: "直近{n}日" | ||||
| 
 | ||||
| _emailUnavailable: | ||||
|   used: "既に使用されています" | ||||
|  |  | |||
|  | @ -1,11 +1,13 @@ | |||
| import { db } from '@/db/postgre.js'; | ||||
| import { Instance } from '@/models/entities/instance.js'; | ||||
| import { Packed } from '@/misc/schema.js'; | ||||
| import { fetchMeta } from '@/misc/fetch-meta.js'; | ||||
| 
 | ||||
| export const InstanceRepository = db.getRepository(Instance).extend({ | ||||
| 	async pack( | ||||
| 		instance: Instance, | ||||
| 	): Promise<Packed<'FederationInstance'>> { | ||||
| 		const meta = await fetchMeta(); | ||||
| 		return { | ||||
| 			id: instance.id, | ||||
| 			caughtAt: instance.caughtAt.toISOString(), | ||||
|  | @ -18,6 +20,7 @@ export const InstanceRepository = db.getRepository(Instance).extend({ | |||
| 			lastCommunicatedAt: instance.lastCommunicatedAt.toISOString(), | ||||
| 			isNotResponding: instance.isNotResponding, | ||||
| 			isSuspended: instance.isSuspended, | ||||
| 			isBlocked: meta.blockedHosts.includes(instance.host), | ||||
| 			softwareName: instance.softwareName, | ||||
| 			softwareVersion: instance.softwareVersion, | ||||
| 			openRegistrations: instance.openRegistrations, | ||||
|  |  | |||
|  | @ -52,6 +52,10 @@ export const packedFederationInstanceSchema = { | |||
| 			type: 'boolean', | ||||
| 			optional: false, nullable: false, | ||||
| 		}, | ||||
| 		isBlocked: { | ||||
| 			type: 'boolean', | ||||
| 			optional: false, nullable: false, | ||||
| 		}, | ||||
| 		softwareName: { | ||||
| 			type: 'string', | ||||
| 			optional: false, nullable: true, | ||||
|  |  | |||
|  | @ -79,7 +79,8 @@ function resolve() { | |||
| 			align-items: center; | ||||
| 			padding: 14px; | ||||
| 			border-radius: 8px; | ||||
| 			background-image: linear-gradient(45deg, rgb(255 196 0 / 15%) 16.67%, transparent 16.67%, transparent 50%, rgb(255 196 0 / 15%) 50%, rgb(255 196 0 / 15%) 66.67%, transparent 66.67%, transparent 100%); | ||||
| 			--c: rgb(255 196 0 / 15%); | ||||
| 			background-image: linear-gradient(45deg, var(--c) 16.67%, transparent 16.67%, transparent 50%, var(--c) 50%, var(--c) 66.67%, transparent 66.67%, transparent 100%); | ||||
| 			background-size: 16px 16px; | ||||
| 
 | ||||
| 			> .avatar { | ||||
|  |  | |||
							
								
								
									
										96
									
								
								packages/client/src/components/instance-info.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										96
									
								
								packages/client/src/components/instance-info.vue
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,96 @@ | |||
| <template> | ||||
| <div :class="[$style.root, { yellow: instance.isNotResponding, red: instance.isBlocked, gray: instance.isSuspended }]"> | ||||
| 	<img v-if="instance.iconUrl" class="icon" :src="instance.iconUrl" alt=""/> | ||||
| 	<div class="body"> | ||||
| 		<span class="host">{{ instance.host }}</span> | ||||
| 		<span class="sub">{{ instance.softwareName || '?' }} {{ instance.softwareVersion }}</span> | ||||
| 	</div> | ||||
| 	<MkMiniChart v-if="chart" class="chart" :src="chart.requests.received"/> | ||||
| </div> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts" setup> | ||||
| import * as misskey from 'misskey-js'; | ||||
| import MkMiniChart from '@/components/mini-chart.vue'; | ||||
| import * as os from '@/os'; | ||||
| 
 | ||||
| const props = defineProps<{ | ||||
| 	instance: misskey.entities.Instance; | ||||
| }>(); | ||||
| 
 | ||||
| const chart = $ref(null); | ||||
| 
 | ||||
| os.api('charts/instance', { host: props.instance.host, limit: 16, span: 'hour' }).then(res => { | ||||
| 	chart = res; | ||||
| }); | ||||
| </script> | ||||
| 
 | ||||
| <style lang="scss" module> | ||||
| .root { | ||||
| 	$bodyTitleHieght: 18px; | ||||
| 	$bodyInfoHieght: 16px; | ||||
| 
 | ||||
| 	display: flex; | ||||
| 	align-items: center; | ||||
| 	padding: 16px; | ||||
| 	background: var(--panel); | ||||
| 	border-radius: 8px; | ||||
| 
 | ||||
| 	> :global(.icon) { | ||||
| 		display: block; | ||||
| 		width: ($bodyTitleHieght + $bodyInfoHieght); | ||||
| 		height: ($bodyTitleHieght + $bodyInfoHieght); | ||||
| 		object-fit: cover; | ||||
| 		border-radius: 4px; | ||||
| 		margin-right: 8px; | ||||
| 	} | ||||
| 
 | ||||
| 	> :global(.body) { | ||||
| 		flex: 1; | ||||
| 		overflow: hidden; | ||||
| 		font-size: 0.9em; | ||||
| 		color: var(--fg); | ||||
| 		padding-right: 8px; | ||||
| 
 | ||||
| 		> :global(.host) { | ||||
| 			display: block; | ||||
| 			width: 100%; | ||||
| 			white-space: nowrap; | ||||
| 			overflow: hidden; | ||||
| 			text-overflow: ellipsis; | ||||
| 			line-height: $bodyTitleHieght; | ||||
| 		} | ||||
| 
 | ||||
| 		> :global(.sub) { | ||||
| 			font-size: 75%; | ||||
| 			opacity: 0.7; | ||||
| 			line-height: $bodyInfoHieght; | ||||
| 			white-space: nowrap; | ||||
| 			overflow: hidden; | ||||
| 			text-overflow: ellipsis; | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	> :global(.chart) { | ||||
| 		height: 30px; | ||||
| 	} | ||||
| 
 | ||||
| 	&:global(.yellow) { | ||||
| 		--c: rgb(255 196 0 / 15%); | ||||
| 		background-image: linear-gradient(45deg, var(--c) 16.67%, transparent 16.67%, transparent 50%, var(--c) 50%, var(--c) 66.67%, transparent 66.67%, transparent 100%); | ||||
| 		background-size: 16px 16px; | ||||
| 	} | ||||
| 
 | ||||
| 	&:global(.red) { | ||||
| 		--c: rgb(255 0 0 / 15%); | ||||
| 		background-image: linear-gradient(45deg, var(--c) 16.67%, transparent 16.67%, transparent 50%, var(--c) 50%, var(--c) 66.67%, transparent 66.67%, transparent 100%); | ||||
| 		background-size: 16px 16px; | ||||
| 	} | ||||
| 
 | ||||
| 	&:global(.gray) { | ||||
| 		--c: var(--bg); | ||||
| 		background-image: linear-gradient(45deg, var(--c) 16.67%, transparent 16.67%, transparent 50%, var(--c) 50%, var(--c) 66.67%, transparent 66.67%, transparent 100%); | ||||
| 		background-size: 16px 16px; | ||||
| 	} | ||||
| } | ||||
| </style> | ||||
|  | @ -41,51 +41,8 @@ | |||
| 
 | ||||
| 			<MkPagination v-slot="{items}" ref="instances" :key="host + state" :pagination="pagination"> | ||||
| 				<div class="dqokceoi"> | ||||
| 					<MkA v-for="instance in items" :key="instance.id" class="instance" :to="`/instance-info/${instance.host}`"> | ||||
| 						<div class="host"><img :src="instance.faviconUrl">{{ instance.host }}</div> | ||||
| 						<div class="table"> | ||||
| 							<div class="cell"> | ||||
| 								<div class="key">{{ $ts.registeredAt }}</div> | ||||
| 								<div class="value"><MkTime :time="instance.caughtAt"/></div> | ||||
| 							</div> | ||||
| 							<div class="cell"> | ||||
| 								<div class="key">{{ $ts.software }}</div> | ||||
| 								<div class="value">{{ instance.softwareName || `(${$ts.unknown})` }}</div> | ||||
| 							</div> | ||||
| 							<div class="cell"> | ||||
| 								<div class="key">{{ $ts.version }}</div> | ||||
| 								<div class="value">{{ instance.softwareVersion || `(${$ts.unknown})` }}</div> | ||||
| 							</div> | ||||
| 							<div class="cell"> | ||||
| 								<div class="key">{{ $ts.users }}</div> | ||||
| 								<div class="value">{{ instance.usersCount }}</div> | ||||
| 							</div> | ||||
| 							<div class="cell"> | ||||
| 								<div class="key">{{ $ts.notes }}</div> | ||||
| 								<div class="value">{{ instance.notesCount }}</div> | ||||
| 							</div> | ||||
| 							<div class="cell"> | ||||
| 								<div class="key">{{ $ts.sent }}</div> | ||||
| 								<div class="value"><MkTime v-if="instance.latestRequestSentAt" :time="instance.latestRequestSentAt"/><span v-else>N/A</span></div> | ||||
| 							</div> | ||||
| 							<div class="cell"> | ||||
| 								<div class="key">{{ $ts.received }}</div> | ||||
| 								<div class="value"><MkTime v-if="instance.latestRequestReceivedAt" :time="instance.latestRequestReceivedAt"/><span v-else>N/A</span></div> | ||||
| 							</div> | ||||
| 						</div> | ||||
| 						<div class="footer"> | ||||
| 							<span class="status" :class="getStatus(instance)">{{ getStatus(instance) }}</span> | ||||
| 							<span class="pubSub"> | ||||
| 								<span v-if="instance.followersCount > 0" class="sub"><i class="fas fa-caret-down icon"></i>Sub</span> | ||||
| 								<span v-else class="sub"><i class="fas fa-caret-down icon"></i>-</span> | ||||
| 								<span v-if="instance.followingCount > 0" class="pub"><i class="fas fa-caret-up icon"></i>Pub</span> | ||||
| 								<span v-else class="pub"><i class="fas fa-caret-up icon"></i>-</span> | ||||
| 							</span> | ||||
| 							<span class="right"> | ||||
| 								<span class="latestStatus">{{ instance.latestStatus || '-' }}</span> | ||||
| 								<span class="lastCommunicatedAt"><MkTime :time="instance.lastCommunicatedAt"/></span> | ||||
| 							</span> | ||||
| 						</div> | ||||
| 					<MkA v-for="instance in items" :key="instance.id" v-tooltip.mfm="`Last communicated: ${new Date(instance.lastCommunicatedAt).toLocaleString()}\nStatus: ${getStatus(instance)}`" class="instance" :to="`/instance-info/${instance.host}`" :behavior="'window'"> | ||||
| 						<MkInstanceInfo :instance="instance"/> | ||||
| 					</MkA> | ||||
| 				</div> | ||||
| 			</MkPagination> | ||||
|  | @ -100,6 +57,7 @@ import MkButton from '@/components/ui/button.vue'; | |||
| import MkInput from '@/components/form/input.vue'; | ||||
| import MkSelect from '@/components/form/select.vue'; | ||||
| import MkPagination from '@/components/ui/pagination.vue'; | ||||
| import MkInstanceInfo from '@/components/instance-info.vue'; | ||||
| import FormSplit from '@/components/form/split.vue'; | ||||
| import * as os from '@/os'; | ||||
| import { i18n } from '@/i18n'; | ||||
|  | @ -127,9 +85,10 @@ const pagination = { | |||
| }; | ||||
| 
 | ||||
| function getStatus(instance) { | ||||
| 	if (instance.isSuspended) return 'suspended'; | ||||
| 	if (instance.isNotResponding) return 'error'; | ||||
| 	return 'alive'; | ||||
| 	if (instance.isSuspended) return 'Suspended'; | ||||
| 	if (instance.isBlocked) return 'Blocked'; | ||||
| 	if (instance.isNotResponding) return 'Error'; | ||||
| 	return 'Alive'; | ||||
| } | ||||
| 
 | ||||
| const headerActions = $computed(() => []); | ||||
|  | @ -156,86 +115,8 @@ definePageMetadata({ | |||
| 	grid-template-columns: repeat(auto-fill, minmax(270px, 1fr)); | ||||
| 	grid-gap: 12px; | ||||
| 
 | ||||
| 	> .instance { | ||||
| 		padding: 16px; | ||||
| 		background: var(--panel); | ||||
| 		border-radius: 8px; | ||||
| 
 | ||||
| 		&:hover { | ||||
| 			text-decoration: none; | ||||
| 		} | ||||
| 
 | ||||
| 		> .host { | ||||
| 			font-weight: bold; | ||||
| 			white-space: nowrap; | ||||
| 			overflow: hidden; | ||||
| 			text-overflow: ellipsis; | ||||
| 
 | ||||
| 			> img { | ||||
| 				width: 18px; | ||||
| 				height: 18px; | ||||
| 				margin-right: 6px; | ||||
| 				vertical-align: middle; | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		> .table { | ||||
| 			display: grid; | ||||
| 			grid-template-columns: repeat(auto-fill, minmax(60px, 1fr)); | ||||
| 			grid-gap: 6px; | ||||
| 			margin: 6px 0; | ||||
| 			font-size: 70%; | ||||
| 
 | ||||
| 			> .cell { | ||||
| 				> .key, > .value { | ||||
| 					white-space: nowrap; | ||||
| 					overflow: hidden; | ||||
| 					text-overflow: ellipsis; | ||||
| 				} | ||||
| 
 | ||||
| 				> .key { | ||||
| 					opacity: 0.7; | ||||
| 				} | ||||
| 
 | ||||
| 				> .value { | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		> .footer { | ||||
| 			display: flex; | ||||
| 			align-items: center; | ||||
| 			font-size: 0.9em; | ||||
| 
 | ||||
| 			> .status { | ||||
| 				&.suspended { | ||||
| 					opacity: 0.5; | ||||
| 				} | ||||
| 
 | ||||
| 				&.error { | ||||
| 					color: var(--error); | ||||
| 				} | ||||
| 
 | ||||
| 				&.alive { | ||||
| 					color: var(--success); | ||||
| 				} | ||||
| 			} | ||||
| 
 | ||||
| 			> .pubSub { | ||||
| 				margin-left: 8px; | ||||
| 			} | ||||
| 
 | ||||
| 			> .right { | ||||
| 				margin-left: auto; | ||||
| 
 | ||||
| 				> .latestStatus { | ||||
| 					border: solid 1px var(--divider); | ||||
| 					border-radius: 4px; | ||||
| 					margin: 0 8px; | ||||
| 					padding: 0 4px; | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 	> .instance:hover { | ||||
| 		text-decoration: none; | ||||
| 	} | ||||
| } | ||||
| </style> | ||||
|  |  | |||
|  | @ -1,8 +1,8 @@ | |||
| <template> | ||||
| <MkStickyContainer> | ||||
| 	<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template> | ||||
| 	<MkSpacer :content-max="600" :margin-min="16" :margin-max="32"> | ||||
| 		<div v-if="instance" class="_formRoot"> | ||||
| 	<MkSpacer v-if="instance" :content-max="600" :margin-min="16" :margin-max="32"> | ||||
| 		<div v-if="tab === 'overview'" class="_formRoot"> | ||||
| 			<div class="fnfelxur"> | ||||
| 				<img :src="instance.iconUrl || instance.faviconUrl" alt="" class="icon"/> | ||||
| 			</div> | ||||
|  | @ -64,37 +64,6 @@ | |||
| 				</MkKeyValue> | ||||
| 			</FormSection> | ||||
| 
 | ||||
| 			<FormSection> | ||||
| 				<template #label>{{ $ts.statistics }}</template> | ||||
| 				<div class="cmhjzshl"> | ||||
| 					<div class="selects"> | ||||
| 						<MkSelect v-model="chartSrc" style="margin: 0 10px 0 0; flex: 1;"> | ||||
| 							<option value="instance-requests">{{ $ts._instanceCharts.requests }}</option> | ||||
| 							<option value="instance-users">{{ $ts._instanceCharts.users }}</option> | ||||
| 							<option value="instance-users-total">{{ $ts._instanceCharts.usersTotal }}</option> | ||||
| 							<option value="instance-notes">{{ $ts._instanceCharts.notes }}</option> | ||||
| 							<option value="instance-notes-total">{{ $ts._instanceCharts.notesTotal }}</option> | ||||
| 							<option value="instance-ff">{{ $ts._instanceCharts.ff }}</option> | ||||
| 							<option value="instance-ff-total">{{ $ts._instanceCharts.ffTotal }}</option> | ||||
| 							<option value="instance-drive-usage">{{ $ts._instanceCharts.cacheSize }}</option> | ||||
| 							<option value="instance-drive-usage-total">{{ $ts._instanceCharts.cacheSizeTotal }}</option> | ||||
| 							<option value="instance-drive-files">{{ $ts._instanceCharts.files }}</option> | ||||
| 							<option value="instance-drive-files-total">{{ $ts._instanceCharts.filesTotal }}</option> | ||||
| 						</MkSelect> | ||||
| 						<MkSelect v-model="chartSpan" style="margin: 0;"> | ||||
| 							<option value="hour">{{ $ts.perHour }}</option> | ||||
| 							<option value="day">{{ $ts.perDay }}</option> | ||||
| 						</MkSelect> | ||||
| 					</div> | ||||
| 					<div class="chart"> | ||||
| 						<MkChart :src="chartSrc" :span="chartSpan" :limit="90" :args="{ host: host }" :detailed="true"></MkChart> | ||||
| 					</div> | ||||
| 				</div> | ||||
| 			</FormSection> | ||||
| 
 | ||||
| 			<MkObjectView tall :value="instance"> | ||||
| 			</MkObjectView> | ||||
| 
 | ||||
| 			<FormSection> | ||||
| 				<template #label>Well-known resources</template> | ||||
| 				<FormLink :to="`https://${host}/.well-known/host-meta`" external style="margin-bottom: 8px;">host-meta</FormLink> | ||||
|  | @ -104,6 +73,35 @@ | |||
| 				<FormLink :to="`https://${host}/manifest.json`" external style="margin-bottom: 8px;">manifest.json</FormLink> | ||||
| 			</FormSection> | ||||
| 		</div> | ||||
| 		<div v-if="tab === 'chart'" class="_formRoot"> | ||||
| 			<div class="cmhjzshl"> | ||||
| 				<div class="selects"> | ||||
| 					<MkSelect v-model="chartSrc" style="margin: 0 10px 0 0; flex: 1;"> | ||||
| 						<option value="instance-requests">{{ $ts._instanceCharts.requests }}</option> | ||||
| 						<option value="instance-users">{{ $ts._instanceCharts.users }}</option> | ||||
| 						<option value="instance-users-total">{{ $ts._instanceCharts.usersTotal }}</option> | ||||
| 						<option value="instance-notes">{{ $ts._instanceCharts.notes }}</option> | ||||
| 						<option value="instance-notes-total">{{ $ts._instanceCharts.notesTotal }}</option> | ||||
| 						<option value="instance-ff">{{ $ts._instanceCharts.ff }}</option> | ||||
| 						<option value="instance-ff-total">{{ $ts._instanceCharts.ffTotal }}</option> | ||||
| 						<option value="instance-drive-usage">{{ $ts._instanceCharts.cacheSize }}</option> | ||||
| 						<option value="instance-drive-usage-total">{{ $ts._instanceCharts.cacheSizeTotal }}</option> | ||||
| 						<option value="instance-drive-files">{{ $ts._instanceCharts.files }}</option> | ||||
| 						<option value="instance-drive-files-total">{{ $ts._instanceCharts.filesTotal }}</option> | ||||
| 					</MkSelect> | ||||
| 				</div> | ||||
| 				<div class="charts"> | ||||
| 					<div class="label">{{ i18n.t('recentNHours', { n: 90 }) }}</div> | ||||
| 					<MkChart class="chart" :src="chartSrc" span="hour" :limit="90" :args="{ host: host }" :detailed="true"></MkChart> | ||||
| 					<div class="label">{{ i18n.t('recentNDays', { n: 90 }) }}</div> | ||||
| 					<MkChart class="chart" :src="chartSrc" span="day" :limit="90" :args="{ host: host }" :detailed="true"></MkChart> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 		<div v-if="tab === 'raw'" class="_formRoot"> | ||||
| 			<MkObjectView tall :value="instance"> | ||||
| 			</MkObjectView> | ||||
| 		</div> | ||||
| 	</MkSpacer> | ||||
| </MkStickyContainer> | ||||
| </template> | ||||
|  | @ -125,17 +123,18 @@ import number from '@/filters/number'; | |||
| import bytes from '@/filters/bytes'; | ||||
| import { iAmModerator } from '@/account'; | ||||
| import { definePageMetadata } from '@/scripts/page-metadata'; | ||||
| import { i18n } from '@/i18n'; | ||||
| 
 | ||||
| const props = defineProps<{ | ||||
| 	host: string; | ||||
| }>(); | ||||
| 
 | ||||
| let tab = $ref('overview'); | ||||
| let meta = $ref<misskey.entities.DetailedInstanceMetadata | null>(null); | ||||
| let instance = $ref<misskey.entities.Instance | null>(null); | ||||
| let suspended = $ref(false); | ||||
| let isBlocked = $ref(false); | ||||
| let chartSrc = $ref('instance-requests'); | ||||
| let chartSpan = $ref('hour'); | ||||
| 
 | ||||
| async function fetch() { | ||||
| 	if (iAmModerator) { | ||||
|  | @ -184,11 +183,26 @@ const headerActions = $computed(() => [{ | |||
| 	}, | ||||
| }]); | ||||
| 
 | ||||
| const headerTabs = $computed(() => []); | ||||
| const headerTabs = $computed(() => [{ | ||||
| 	active: tab === 'overview', | ||||
| 	title: i18n.ts.overview, | ||||
| 	icon: 'fas fa-info-circle', | ||||
| 	onClick: () => { tab = 'overview'; }, | ||||
| }, { | ||||
| 	active: tab === 'chart', | ||||
| 	title: i18n.ts.charts, | ||||
| 	icon: 'fas fa-chart-simple', | ||||
| 	onClick: () => { tab = 'chart'; }, | ||||
| }, { | ||||
| 	active: tab === 'raw', | ||||
| 	title: 'Raw data', | ||||
| 	icon: 'fas fa-code', | ||||
| 	onClick: () => { tab = 'raw'; }, | ||||
| }]); | ||||
| 
 | ||||
| definePageMetadata({ | ||||
| 	title: props.host, | ||||
| 	icon: 'fas fa-info-circle', | ||||
| 	icon: 'fas fa-server', | ||||
| 	bg: 'var(--bg)', | ||||
| }); | ||||
| </script> | ||||
|  | @ -208,5 +222,12 @@ definePageMetadata({ | |||
| 		display: flex; | ||||
| 		margin: 0 0 16px 0; | ||||
| 	} | ||||
| 
 | ||||
| 	> .charts { | ||||
| 		> .label { | ||||
| 			margin-bottom: 12px; | ||||
| 			font-weight: bold; | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| </style> | ||||
|  |  | |||
|  | @ -11,13 +11,13 @@ export class I18n<T extends Record<string, any>> { | |||
| 
 | ||||
| 	// string にしているのは、ドット区切りでのパス指定を許可するため
 | ||||
| 	// なるべくこのメソッド使うよりもlocale直接参照の方がvueのキャッシュ効いてパフォーマンスが良いかも
 | ||||
| 	public t(key: string, args?: Record<string, string>): string { | ||||
| 	public t(key: string, args?: Record<string, string | number>): string { | ||||
| 		try { | ||||
| 			let str = key.split('.').reduce((o, i) => o[i], this.ts) as unknown as string; | ||||
| 
 | ||||
| 			if (args) { | ||||
| 				for (const [k, v] of Object.entries(args)) { | ||||
| 					str = str.replace(`{${k}}`, v); | ||||
| 					str = str.replace(`{${k}}`, v.toString()); | ||||
| 				} | ||||
| 			} | ||||
| 			return str; | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue