refactor: deprecate i18n.t (#13039)
* refactor: deprecate i18n.t * revert: deprecate i18n.t This reverts commit 7dbf873a2f745040ee723df5db659acacff84e12. * chore: reimpl
This commit is contained in:
		
							parent
							
								
									a637b4e282
								
							
						
					
					
						commit
						7881f06be0
					
				
					 84 changed files with 7311 additions and 222 deletions
				
			
		| 
						 | 
				
			
			@ -16,32 +16,40 @@ function createMemberType(item) {
 | 
			
		|||
		item.matchAll(parameterRegExp),
 | 
			
		||||
		([, parameter]) => parameter,
 | 
			
		||||
	);
 | 
			
		||||
	if (!parameters.length) {
 | 
			
		||||
		return ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword);
 | 
			
		||||
	}
 | 
			
		||||
	return ts.factory.createTypeReferenceNode(
 | 
			
		||||
	return parameters.length
 | 
			
		||||
		? ts.factory.createTypeReferenceNode(
 | 
			
		||||
				ts.factory.createIdentifier('ParameterizedString'),
 | 
			
		||||
				[
 | 
			
		||||
					ts.factory.createUnionTypeNode(
 | 
			
		||||
						parameters.map((parameter) =>
 | 
			
		||||
					ts.factory.createLiteralTypeNode(
 | 
			
		||||
							ts.factory.createStringLiteral(parameter),
 | 
			
		||||
						),
 | 
			
		||||
					),
 | 
			
		||||
			),
 | 
			
		||||
				],
 | 
			
		||||
	);
 | 
			
		||||
			)
 | 
			
		||||
		: ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function createMembers(record) {
 | 
			
		||||
	return Object.entries(record).map(([k, v]) =>
 | 
			
		||||
		ts.factory.createPropertySignature(
 | 
			
		||||
	return Object.entries(record).map(([k, v]) => {
 | 
			
		||||
		const node = ts.factory.createPropertySignature(
 | 
			
		||||
			undefined,
 | 
			
		||||
			ts.factory.createStringLiteral(k),
 | 
			
		||||
			undefined,
 | 
			
		||||
			createMemberType(v),
 | 
			
		||||
		),
 | 
			
		||||
		);
 | 
			
		||||
		if (typeof v === 'string') {
 | 
			
		||||
			ts.addSyntheticLeadingComment(
 | 
			
		||||
				node,
 | 
			
		||||
				ts.SyntaxKind.MultiLineCommentTrivia,
 | 
			
		||||
				`*
 | 
			
		||||
 * ${v.replace(/\n/g, '\n * ')}
 | 
			
		||||
 `,
 | 
			
		||||
				true,
 | 
			
		||||
			);
 | 
			
		||||
		}
 | 
			
		||||
		return node;
 | 
			
		||||
	});
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default function generateDTS() {
 | 
			
		||||
| 
						 | 
				
			
			@ -72,10 +80,8 @@ export default function generateDTS() {
 | 
			
		|||
				ts.factory.createTypeParameterDeclaration(
 | 
			
		||||
					undefined,
 | 
			
		||||
					ts.factory.createIdentifier('T'),
 | 
			
		||||
					ts.factory.createTypeReferenceNode(
 | 
			
		||||
						ts.factory.createIdentifier('string'),
 | 
			
		||||
						undefined,
 | 
			
		||||
					),
 | 
			
		||||
					ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword),
 | 
			
		||||
					ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword),
 | 
			
		||||
				),
 | 
			
		||||
			],
 | 
			
		||||
			undefined,
 | 
			
		||||
| 
						 | 
				
			
			@ -115,7 +121,6 @@ export default function generateDTS() {
 | 
			
		|||
						ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword),
 | 
			
		||||
						ts.factory.createTypeReferenceNode(
 | 
			
		||||
							ts.factory.createIdentifier('ParameterizedString'),
 | 
			
		||||
							[ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword)],
 | 
			
		||||
						),
 | 
			
		||||
						ts.factory.createTypeReferenceNode(
 | 
			
		||||
							ts.factory.createIdentifier('ILocale'),
 | 
			
		||||
| 
						 | 
				
			
			@ -187,6 +192,24 @@ export default function generateDTS() {
 | 
			
		|||
		),
 | 
			
		||||
		ts.factory.createExportDefault(ts.factory.createIdentifier('locales')),
 | 
			
		||||
	];
 | 
			
		||||
	ts.addSyntheticLeadingComment(
 | 
			
		||||
		elements[0],
 | 
			
		||||
		ts.SyntaxKind.MultiLineCommentTrivia,
 | 
			
		||||
		' eslint-disable ',
 | 
			
		||||
		true,
 | 
			
		||||
	);
 | 
			
		||||
	ts.addSyntheticLeadingComment(
 | 
			
		||||
		elements[0],
 | 
			
		||||
		ts.SyntaxKind.SingleLineCommentTrivia,
 | 
			
		||||
		' This file is generated by locales/generateDTS.js',
 | 
			
		||||
		true,
 | 
			
		||||
	);
 | 
			
		||||
	ts.addSyntheticLeadingComment(
 | 
			
		||||
		elements[0],
 | 
			
		||||
		ts.SyntaxKind.SingleLineCommentTrivia,
 | 
			
		||||
		' Do not edit this file directly.',
 | 
			
		||||
		true,
 | 
			
		||||
	);
 | 
			
		||||
	const printed = ts
 | 
			
		||||
		.createPrinter({
 | 
			
		||||
			newLine: ts.NewLineKind.LineFeed,
 | 
			
		||||
| 
						 | 
				
			
			@ -203,12 +226,5 @@ export default function generateDTS() {
 | 
			
		|||
			),
 | 
			
		||||
		);
 | 
			
		||||
 | 
			
		||||
	fs.writeFileSync(
 | 
			
		||||
		`${__dirname}/index.d.ts`,
 | 
			
		||||
		`/* eslint-disable */
 | 
			
		||||
// This file is generated by locales/generateDTS.js
 | 
			
		||||
// Do not edit this file directly.
 | 
			
		||||
${printed}`,
 | 
			
		||||
		'utf-8',
 | 
			
		||||
	);
 | 
			
		||||
	fs.writeFileSync(`${__dirname}/index.d.ts`, printed, 'utf-8');
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										6878
									
								
								locales/index.d.ts
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										6878
									
								
								locales/index.d.ts
									
										
									
									
										vendored
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load diff
											
										
									
								
							| 
						 | 
				
			
			@ -205,7 +205,7 @@ export async function mainBoot() {
 | 
			
		|||
			const lastUsedDate = parseInt(lastUsed, 10);
 | 
			
		||||
			// 二時間以上前なら
 | 
			
		||||
			if (Date.now() - lastUsedDate > 1000 * 60 * 60 * 2) {
 | 
			
		||||
				toast(i18n.t('welcomeBackWithName', {
 | 
			
		||||
				toast(i18n.tsx.welcomeBackWithName({
 | 
			
		||||
					name: $i.name || $i.username,
 | 
			
		||||
				}));
 | 
			
		||||
			}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -44,7 +44,7 @@ async function ok() {
 | 
			
		|||
		const confirm = await os.confirm({
 | 
			
		||||
			type: 'question',
 | 
			
		||||
			title: i18n.ts._announcement.readConfirmTitle,
 | 
			
		||||
			text: i18n.t('_announcement.readConfirmText', { title: props.announcement.title }),
 | 
			
		||||
			text: i18n.tsx._announcement.readConfirmText({ title: props.announcement.title }),
 | 
			
		||||
		});
 | 
			
		||||
		if (confirm.canceled) return;
 | 
			
		||||
	}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -41,9 +41,9 @@ const emit = defineEmits<{
 | 
			
		|||
 | 
			
		||||
const label = computed(() => {
 | 
			
		||||
	return concat([
 | 
			
		||||
		props.text ? [i18n.t('_cw.chars', { count: props.text.length })] : [],
 | 
			
		||||
		props.text ? [i18n.tsx._cw.chars({ count: props.text.length })] : [],
 | 
			
		||||
		props.renote ? [i18n.ts.quote] : [],
 | 
			
		||||
		props.files.length !== 0 ? [i18n.t('_cw.files', { count: props.files.length })] : [],
 | 
			
		||||
		props.files.length !== 0 ? [i18n.tsx._cw.files({ count: props.files.length })] : [],
 | 
			
		||||
		props.poll != null ? [i18n.ts.poll] : [],
 | 
			
		||||
	] as string[][]).join(' / ');
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -46,7 +46,7 @@ export default defineComponent({
 | 
			
		|||
		function getDateText(time: string) {
 | 
			
		||||
			const date = new Date(time).getDate();
 | 
			
		||||
			const month = new Date(time).getMonth() + 1;
 | 
			
		||||
			return i18n.t('monthAndDay', {
 | 
			
		||||
			return i18n.tsx.monthAndDay({
 | 
			
		||||
				month: month.toString(),
 | 
			
		||||
				day: date.toString(),
 | 
			
		||||
			});
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -30,8 +30,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		|||
		<MkInput v-if="input" v-model="inputValue" autofocus :type="input.type || 'text'" :placeholder="input.placeholder || undefined" :autocomplete="input.autocomplete" @keydown="onInputKeydown">
 | 
			
		||||
			<template v-if="input.type === 'password'" #prefix><i class="ti ti-lock"></i></template>
 | 
			
		||||
			<template #caption>
 | 
			
		||||
				<span v-if="okButtonDisabledReason === 'charactersExceeded'" v-text="i18n.t('_dialog.charactersExceeded', { current: (inputValue as string).length, max: input.maxLength ?? 'NaN' })"/>
 | 
			
		||||
				<span v-else-if="okButtonDisabledReason === 'charactersBelow'" v-text="i18n.t('_dialog.charactersBelow', { current: (inputValue as string).length, min: input.minLength ?? 'NaN' })"/>
 | 
			
		||||
				<span v-if="okButtonDisabledReason === 'charactersExceeded'" v-text="i18n.tsx._dialog.charactersExceeded({ current: (inputValue as string).length, max: input.maxLength ?? 'NaN' })"/>
 | 
			
		||||
				<span v-else-if="okButtonDisabledReason === 'charactersBelow'" v-text="i18n.tsx._dialog.charactersBelow({ current: (inputValue as string).length, min: input.minLength ?? 'NaN' })"/>
 | 
			
		||||
			</template>
 | 
			
		||||
		</MkInput>
 | 
			
		||||
		<MkSelect v-if="select" v-model="selectedValue" autofocus>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -82,8 +82,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		|||
				<MkButton v-show="moreFiles" ref="loadMoreFiles" @click="fetchMoreFiles">{{ i18n.ts.loadMore }}</MkButton>
 | 
			
		||||
			</div>
 | 
			
		||||
			<div v-if="files.length == 0 && folders.length == 0 && !fetching" :class="$style.empty">
 | 
			
		||||
				<div v-if="draghover">{{ i18n.t('empty-draghover') }}</div>
 | 
			
		||||
				<div v-if="!draghover && folder == null"><strong>{{ i18n.ts.emptyDrive }}</strong><br/>{{ i18n.t('empty-drive-description') }}</div>
 | 
			
		||||
				<div v-if="draghover">{{ i18n.ts['empty-draghover'] }}</div>
 | 
			
		||||
				<div v-if="!draghover && folder == null"><strong>{{ i18n.ts.emptyDrive }}</strong><br/>{{ i18n.ts['empty-drive-description'] }}</div>
 | 
			
		||||
				<div v-if="!draghover && folder != null">{{ i18n.ts.emptyFolder }}</div>
 | 
			
		||||
			</div>
 | 
			
		||||
		</div>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -84,7 +84,7 @@ async function onClick() {
 | 
			
		|||
		if (isFollowing.value) {
 | 
			
		||||
			const { canceled } = await os.confirm({
 | 
			
		||||
				type: 'warning',
 | 
			
		||||
				text: i18n.t('unfollowConfirm', { name: props.user.name || props.user.username }),
 | 
			
		||||
				text: i18n.tsx.unfollowConfirm({ name: props.user.name || props.user.username }),
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			if (canceled) return;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -73,7 +73,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		|||
						<div v-if="translating || translation" :class="$style.translation">
 | 
			
		||||
							<MkLoading v-if="translating" mini/>
 | 
			
		||||
							<div v-else>
 | 
			
		||||
								<b>{{ i18n.t('translatedFrom', { x: translation.sourceLang }) }}: </b>
 | 
			
		||||
								<b>{{ i18n.tsx.translatedFrom({ x: translation.sourceLang }) }}: </b>
 | 
			
		||||
								<Mfm :text="translation.text" :author="appearNote.user" :nyaize="'respect'" :emojiUrls="appearNote.emojis"/>
 | 
			
		||||
							</div>
 | 
			
		||||
						</div>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -87,7 +87,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		|||
				<div v-if="translating || translation" :class="$style.translation">
 | 
			
		||||
					<MkLoading v-if="translating" mini/>
 | 
			
		||||
					<div v-else>
 | 
			
		||||
						<b>{{ i18n.t('translatedFrom', { x: translation.sourceLang }) }}: </b>
 | 
			
		||||
						<b>{{ i18n.tsx.translatedFrom({ x: translation.sourceLang }) }}: </b>
 | 
			
		||||
						<Mfm :text="translation.text" :author="appearNote.user" :nyaize="'respect'" :emojiUrls="appearNote.emojis"/>
 | 
			
		||||
					</div>
 | 
			
		||||
				</div>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -56,8 +56,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		|||
			<span v-else-if="notification.type === 'achievementEarned'">{{ i18n.ts._notification.achievementEarned }}</span>
 | 
			
		||||
			<span v-else-if="notification.type === 'test'">{{ i18n.ts._notification.testNotification }}</span>
 | 
			
		||||
			<MkA v-else-if="notification.user" v-user-preview="notification.user.id" :class="$style.headerName" :to="userPage(notification.user)"><MkUserName :user="notification.user"/></MkA>
 | 
			
		||||
			<span v-else-if="notification.type === 'reaction:grouped'">{{ i18n.t('_notification.reactedBySomeUsers', { n: notification.reactions.length }) }}</span>
 | 
			
		||||
			<span v-else-if="notification.type === 'renote:grouped'">{{ i18n.t('_notification.renotedBySomeUsers', { n: notification.users.length }) }}</span>
 | 
			
		||||
			<span v-else-if="notification.type === 'reaction:grouped'">{{ i18n.tsx._notification.reactedBySomeUsers({ n: notification.reactions.length }) }}</span>
 | 
			
		||||
			<span v-else-if="notification.type === 'renote:grouped'">{{ i18n.tsx._notification.renotedBySomeUsers({ n: notification.users.length }) }}</span>
 | 
			
		||||
			<span v-else>{{ notification.header }}</span>
 | 
			
		||||
			<MkTime v-if="withTime" :time="notification.createdAt" :class="$style.headerTime"/>
 | 
			
		||||
		</header>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -23,7 +23,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		|||
				<MkButton inline @click="disableAll">{{ i18n.ts.disableAll }}</MkButton>
 | 
			
		||||
				<MkButton inline @click="enableAll">{{ i18n.ts.enableAll }}</MkButton>
 | 
			
		||||
			</div>
 | 
			
		||||
			<MkSwitch v-for="ntype in notificationTypes" :key="ntype" v-model="typesMap[ntype].value">{{ i18n.t(`_notification._types.${ntype}`) }}</MkSwitch>
 | 
			
		||||
			<MkSwitch v-for="ntype in notificationTypes" :key="ntype" v-model="typesMap[ntype].value">{{ i18n.ts._notification._types[ntype] }}</MkSwitch>
 | 
			
		||||
		</div>
 | 
			
		||||
	</MkSpacer>
 | 
			
		||||
</MkModalWindow>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -11,12 +11,12 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		|||
			<span :class="$style.fg">
 | 
			
		||||
				<template v-if="choice.isVoted"><i class="ti ti-check" style="margin-right: 4px; color: var(--accent);"></i></template>
 | 
			
		||||
				<Mfm :text="choice.text" :plain="true"/>
 | 
			
		||||
				<span v-if="showResult" style="margin-left: 4px; opacity: 0.7;">({{ i18n.t('_poll.votesCount', { n: choice.votes }) }})</span>
 | 
			
		||||
				<span v-if="showResult" style="margin-left: 4px; opacity: 0.7;">({{ i18n.tsx._poll.votesCount({ n: choice.votes }) }})</span>
 | 
			
		||||
			</span>
 | 
			
		||||
		</li>
 | 
			
		||||
	</ul>
 | 
			
		||||
	<p v-if="!readOnly" :class="$style.info">
 | 
			
		||||
		<span>{{ i18n.t('_poll.totalVotes', { n: total }) }}</span>
 | 
			
		||||
		<span>{{ i18n.tsx._poll.totalVotes({ n: total }) }}</span>
 | 
			
		||||
		<span> · </span>
 | 
			
		||||
		<a v-if="!closed && !isVoted" style="color: inherit;" @click="showResult = !showResult">{{ showResult ? i18n.ts._poll.vote : i18n.ts._poll.showResult }}</a>
 | 
			
		||||
		<span v-if="isVoted">{{ i18n.ts._poll.voted }}</span>
 | 
			
		||||
| 
						 | 
				
			
			@ -47,10 +47,11 @@ const remaining = ref(-1);
 | 
			
		|||
const total = computed(() => sum(props.note.poll.choices.map(x => x.votes)));
 | 
			
		||||
const closed = computed(() => remaining.value === 0);
 | 
			
		||||
const isVoted = computed(() => !props.note.poll.multiple && props.note.poll.choices.some(c => c.isVoted));
 | 
			
		||||
const timer = computed(() => i18n.t(
 | 
			
		||||
	remaining.value >= 86400 ? '_poll.remainingDays' :
 | 
			
		||||
	remaining.value >= 3600 ? '_poll.remainingHours' :
 | 
			
		||||
	remaining.value >= 60 ? '_poll.remainingMinutes' : '_poll.remainingSeconds', {
 | 
			
		||||
const timer = computed(() => i18n.tsx._poll[
 | 
			
		||||
		remaining.value >= 86400 ? 'remainingDays' :
 | 
			
		||||
		remaining.value >= 3600 ? 'remainingHours' :
 | 
			
		||||
		remaining.value >= 60 ? 'remainingMinutes' : 'remainingSeconds'
 | 
			
		||||
	]({
 | 
			
		||||
		s: Math.floor(remaining.value % 60),
 | 
			
		||||
		m: Math.floor(remaining.value / 60) % 60,
 | 
			
		||||
		h: Math.floor(remaining.value / 3600) % 24,
 | 
			
		||||
| 
						 | 
				
			
			@ -81,7 +82,7 @@ const vote = async (id) => {
 | 
			
		|||
 | 
			
		||||
	const { canceled } = await os.confirm({
 | 
			
		||||
		type: 'question',
 | 
			
		||||
		text: i18n.t('voteConfirm', { choice: props.note.poll.choices[id].text }),
 | 
			
		||||
		text: i18n.tsx.voteConfirm({ choice: props.note.poll.choices[id].text }),
 | 
			
		||||
	});
 | 
			
		||||
	if (canceled) return;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		|||
	</p>
 | 
			
		||||
	<ul>
 | 
			
		||||
		<li v-for="(choice, i) in choices" :key="i">
 | 
			
		||||
			<MkInput class="input" small :modelValue="choice" :placeholder="i18n.t('_poll.choiceN', { n: i + 1 })" @update:modelValue="onInput(i, $event)">
 | 
			
		||||
			<MkInput class="input" small :modelValue="choice" :placeholder="i18n.tsx._poll.choiceN({ n: i + 1 })" @update:modelValue="onInput(i, $event)">
 | 
			
		||||
			</MkInput>
 | 
			
		||||
			<button class="_button" @click="remove(i)">
 | 
			
		||||
				<i class="ti ti-x"></i>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -263,7 +263,7 @@ async function onSubmit(): Promise<void> {
 | 
			
		|||
			os.alert({
 | 
			
		||||
				type: 'success',
 | 
			
		||||
				title: i18n.ts._signup.almostThere,
 | 
			
		||||
				text: i18n.t('_signup.emailSent', { email: email.value }),
 | 
			
		||||
				text: i18n.tsx._signup.emailSent({ email: email.value }),
 | 
			
		||||
			});
 | 
			
		||||
			emit('signupEmailPending');
 | 
			
		||||
		} else {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -105,7 +105,7 @@ async function updateAgreeServerRules(v: boolean) {
 | 
			
		|||
		const confirm = await os.confirm({
 | 
			
		||||
			type: 'question',
 | 
			
		||||
			title: i18n.ts.doYouAgree,
 | 
			
		||||
			text: i18n.t('iHaveReadXCarefullyAndAgree', { x: i18n.ts.serverRules }),
 | 
			
		||||
			text: i18n.tsx.iHaveReadXCarefullyAndAgree({ x: i18n.ts.serverRules }),
 | 
			
		||||
		});
 | 
			
		||||
		if (confirm.canceled) return;
 | 
			
		||||
		agreeServerRules.value = true;
 | 
			
		||||
| 
						 | 
				
			
			@ -119,7 +119,7 @@ async function updateAgreeTosAndPrivacyPolicy(v: boolean) {
 | 
			
		|||
		const confirm = await os.confirm({
 | 
			
		||||
			type: 'question',
 | 
			
		||||
			title: i18n.ts.doYouAgree,
 | 
			
		||||
			text: i18n.t('iHaveReadXCarefullyAndAgree', {
 | 
			
		||||
			text: i18n.tsx.iHaveReadXCarefullyAndAgree({
 | 
			
		||||
				x: tosPrivacyPolicyLabel.value,
 | 
			
		||||
			}),
 | 
			
		||||
		});
 | 
			
		||||
| 
						 | 
				
			
			@ -135,7 +135,7 @@ async function updateAgreeNote(v: boolean) {
 | 
			
		|||
		const confirm = await os.confirm({
 | 
			
		||||
			type: 'question',
 | 
			
		||||
			title: i18n.ts.doYouAgree,
 | 
			
		||||
			text: i18n.t('iHaveReadXCarefullyAndAgree', { x: i18n.ts.basicNotesBeforeCreateAccount }),
 | 
			
		||||
			text: i18n.tsx.iHaveReadXCarefullyAndAgree({ x: i18n.ts.basicNotesBeforeCreateAccount }),
 | 
			
		||||
		});
 | 
			
		||||
		if (confirm.canceled) return;
 | 
			
		||||
		agreeNote.value = true;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -13,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		|||
		<MkA v-if="note.renoteId" :class="$style.rp" :to="`/notes/${note.renoteId}`">RN: ...</MkA>
 | 
			
		||||
	</div>
 | 
			
		||||
	<details v-if="note.files.length > 0">
 | 
			
		||||
		<summary>({{ i18n.t('withNFiles', { n: note.files.length }) }})</summary>
 | 
			
		||||
		<summary>({{ i18n.tsx.withNFiles({ n: note.files.length }) }})</summary>
 | 
			
		||||
		<MkMediaList :mediaList="note.files"/>
 | 
			
		||||
	</details>
 | 
			
		||||
	<details v-if="note.poll">
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -33,12 +33,12 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		|||
				<MkButton inline @click="enableAll">{{ i18n.ts.enableAll }}</MkButton>
 | 
			
		||||
			</div>
 | 
			
		||||
			<div class="_gaps_s">
 | 
			
		||||
				<MkSwitch v-for="kind in Object.keys(permissionSwitches)" :key="kind" v-model="permissionSwitches[kind]">{{ i18n.t(`_permissions.${kind}`) }}</MkSwitch>
 | 
			
		||||
				<MkSwitch v-for="kind in Object.keys(permissionSwitches)" :key="kind" v-model="permissionSwitches[kind]">{{ i18n.ts._permissions[kind] }}</MkSwitch>
 | 
			
		||||
			</div>
 | 
			
		||||
			<div v-if="iAmAdmin" :class="$style.adminPermissions">
 | 
			
		||||
				<div :class="$style.adminPermissionsHeader"><b>{{ i18n.ts.adminPermission }}</b></div>
 | 
			
		||||
				<div class="_gaps_s">
 | 
			
		||||
					<MkSwitch v-for="kind in Object.keys(permissionSwitchesForAdmin)" :key="kind" v-model="permissionSwitchesForAdmin[kind]">{{ i18n.t(`_permissions.${kind}`) }}</MkSwitch>
 | 
			
		||||
					<MkSwitch v-for="kind in Object.keys(permissionSwitchesForAdmin)" :key="kind" v-model="permissionSwitchesForAdmin[kind]">{{ i18n.ts._permissions[kind] }}</MkSwitch>
 | 
			
		||||
				</div>
 | 
			
		||||
			</div>
 | 
			
		||||
		</div>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -133,7 +133,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		|||
									<a href="https://misskey-hub.net/docs/for-users/" target="_blank" class="_link">{{ i18n.ts.help }}</a>
 | 
			
		||||
								</template>
 | 
			
		||||
							</I18n>
 | 
			
		||||
							<div>{{ i18n.t('_initialAccountSetting.haveFun', { name: instance.name ?? host }) }}</div>
 | 
			
		||||
							<div>{{ i18n.tsx._initialAccountSetting.haveFun({ name: instance.name ?? host }) }}</div>
 | 
			
		||||
							<div class="_buttonsCenter" style="margin-top: 16px;">
 | 
			
		||||
								<MkButton v-if="initialPage !== 4" rounded @click="page--"><i class="ti ti-arrow-left"></i> {{ i18n.ts.goBack }}</MkButton>
 | 
			
		||||
								<MkButton rounded primary gradate @click="close(false)">{{ i18n.ts.close }}</MkButton>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -118,7 +118,7 @@ async function done() {
 | 
			
		|||
async function del() {
 | 
			
		||||
	const { canceled } = await os.confirm({
 | 
			
		||||
		type: 'warning',
 | 
			
		||||
		text: i18n.t('removeAreYouSure', { x: title.value }),
 | 
			
		||||
		text: i18n.tsx.removeAreYouSure({ x: title.value }),
 | 
			
		||||
	});
 | 
			
		||||
	if (canceled) return;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -68,7 +68,7 @@ function setAvatar(ev) {
 | 
			
		|||
 | 
			
		||||
		const { canceled } = await os.confirm({
 | 
			
		||||
			type: 'question',
 | 
			
		||||
			text: i18n.t('cropImageAsk'),
 | 
			
		||||
			text: i18n.ts.cropImageAsk,
 | 
			
		||||
			okText: i18n.ts.cropYes,
 | 
			
		||||
			cancelText: i18n.ts.cropNo,
 | 
			
		||||
		});
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -93,7 +93,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		|||
						<div class="_gaps" style="text-align: center;">
 | 
			
		||||
							<i class="ti ti-bell-ringing-2" style="display: block; margin: auto; font-size: 3em; color: var(--accent);"></i>
 | 
			
		||||
							<div style="font-size: 120%;">{{ i18n.ts.pushNotification }}</div>
 | 
			
		||||
							<div style="padding: 0 16px;">{{ i18n.t('_initialAccountSetting.pushNotificationDescription', { name: instance.name ?? host }) }}</div>
 | 
			
		||||
							<div style="padding: 0 16px;">{{ i18n.tsx._initialAccountSetting.pushNotificationDescription({ name: instance.name ?? host }) }}</div>
 | 
			
		||||
							<MkPushNotificationAllowButton primary showOnlyToRegister style="margin: 0 auto;"/>
 | 
			
		||||
							<div class="_buttonsCenter" style="margin-top: 16px;">
 | 
			
		||||
								<MkButton rounded data-cy-user-setup-back @click="page--"><i class="ti ti-arrow-left"></i> {{ i18n.ts.goBack }}</MkButton>
 | 
			
		||||
| 
						 | 
				
			
			@ -110,7 +110,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		|||
						<div class="_gaps" style="text-align: center;">
 | 
			
		||||
							<i class="ti ti-check" style="display: block; margin: auto; font-size: 3em; color: var(--accent);"></i>
 | 
			
		||||
							<div style="font-size: 120%;">{{ i18n.ts._initialAccountSetting.initialAccountSettingCompleted }}</div>
 | 
			
		||||
							<div>{{ i18n.t('_initialAccountSetting.youCanContinueTutorial', { name: instance.name ?? host }) }}</div>
 | 
			
		||||
							<div>{{ i18n.tsx._initialAccountSetting.youCanContinueTutorial({ name: instance.name ?? host }) }}</div>
 | 
			
		||||
							<div class="_buttonsCenter" style="margin-top: 16px;">
 | 
			
		||||
								<MkButton rounded primary gradate data-cy-user-setup-continue @click="launchTutorial()">{{ i18n.ts._initialAccountSetting.startTutorial }} <i class="ti ti-arrow-right"></i></MkButton>
 | 
			
		||||
							</div>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		|||
		<header :class="$style.editHeader">
 | 
			
		||||
			<MkSelect v-model="widgetAdderSelected" style="margin-bottom: var(--margin)" data-cy-widget-select>
 | 
			
		||||
				<template #label>{{ i18n.ts.selectWidget }}</template>
 | 
			
		||||
				<option v-for="widget in widgetDefs" :key="widget" :value="widget">{{ i18n.t(`_widgets.${widget}`) }}</option>
 | 
			
		||||
				<option v-for="widget in widgetDefs" :key="widget" :value="widget">{{ i18n.ts._widgets[widget] }}</option>
 | 
			
		||||
			</MkSelect>
 | 
			
		||||
			<MkButton inline primary data-cy-widget-add @click="addWidget"><i class="ti ti-plus"></i> {{ i18n.ts.add }}</MkButton>
 | 
			
		||||
			<MkButton inline @click="$emit('exit')">{{ i18n.ts.close }}</MkButton>
 | 
			
		||||
| 
						 | 
				
			
			@ -109,7 +109,7 @@ function onContextmenu(widget: Widget, ev: MouseEvent) {
 | 
			
		|||
 | 
			
		||||
	os.contextMenu([{
 | 
			
		||||
		type: 'label',
 | 
			
		||||
		text: i18n.t(`_widgets.${widget.name}`),
 | 
			
		||||
		text: i18n.ts._widgets[widget.name],
 | 
			
		||||
	}, {
 | 
			
		||||
		icon: 'ti ti-settings',
 | 
			
		||||
		text: i18n.ts.settings,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										46
									
								
								packages/frontend/src/components/global/I18n.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								packages/frontend/src/components/global/I18n.vue
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,46 @@
 | 
			
		|||
<template>
 | 
			
		||||
<render/>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script setup lang="ts" generic="T extends string | ParameterizedString">
 | 
			
		||||
import { computed, h } from 'vue';
 | 
			
		||||
import type { ParameterizedString } from '../../../../../locales/index.js';
 | 
			
		||||
 | 
			
		||||
const props = withDefaults(defineProps<{
 | 
			
		||||
	src: T;
 | 
			
		||||
	tag?: string;
 | 
			
		||||
	// eslint-disable-next-line vue/require-default-prop
 | 
			
		||||
	textTag?: string;
 | 
			
		||||
}>(), {
 | 
			
		||||
	tag: 'span',
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const slots = defineSlots<T extends ParameterizedString<infer R> ? { [K in R]: () => unknown } : NonNullable<unknown>>();
 | 
			
		||||
 | 
			
		||||
const parsed = computed(() => {
 | 
			
		||||
	let str = props.src as string;
 | 
			
		||||
	const value: (string | { arg: string; })[] = [];
 | 
			
		||||
	for (;;) {
 | 
			
		||||
		const nextBracketOpen = str.indexOf('{');
 | 
			
		||||
		const nextBracketClose = str.indexOf('}');
 | 
			
		||||
 | 
			
		||||
		if (nextBracketOpen === -1) {
 | 
			
		||||
			value.push(str);
 | 
			
		||||
			break;
 | 
			
		||||
		} else {
 | 
			
		||||
			if (nextBracketOpen > 0) value.push(str.substring(0, nextBracketOpen));
 | 
			
		||||
			value.push({
 | 
			
		||||
				arg: str.substring(nextBracketOpen + 1, nextBracketClose),
 | 
			
		||||
			});
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		str = str.substring(nextBracketClose + 1);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return value;
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const render = () => {
 | 
			
		||||
	return h(props.tag, parsed.value.map(x => typeof x === 'string' ? (props.textTag ? h(props.textTag, x) : x) : slots[x.arg]()));
 | 
			
		||||
};
 | 
			
		||||
</script>
 | 
			
		||||
| 
						 | 
				
			
			@ -123,7 +123,7 @@ export const DetailNow = {
 | 
			
		|||
export const RelativeOneHourAgo = {
 | 
			
		||||
	...Empty,
 | 
			
		||||
	async play({ canvasElement }) {
 | 
			
		||||
		await expect(canvasElement).toHaveTextContent(i18n.t('_ago.hoursAgo', { n: 1 }));
 | 
			
		||||
		await expect(canvasElement).toHaveTextContent(i18n.tsx._ago.hoursAgo({ n: 1 }));
 | 
			
		||||
	},
 | 
			
		||||
	args: {
 | 
			
		||||
		...Empty.args,
 | 
			
		||||
| 
						 | 
				
			
			@ -162,7 +162,7 @@ export const DetailOneHourAgo = {
 | 
			
		|||
export const RelativeOneDayAgo = {
 | 
			
		||||
	...Empty,
 | 
			
		||||
	async play({ canvasElement }) {
 | 
			
		||||
		await expect(canvasElement).toHaveTextContent(i18n.t('_ago.daysAgo', { n: 1 }));
 | 
			
		||||
		await expect(canvasElement).toHaveTextContent(i18n.tsx._ago.daysAgo({ n: 1 }));
 | 
			
		||||
	},
 | 
			
		||||
	args: {
 | 
			
		||||
		...Empty.args,
 | 
			
		||||
| 
						 | 
				
			
			@ -201,7 +201,7 @@ export const DetailOneDayAgo = {
 | 
			
		|||
export const RelativeOneWeekAgo = {
 | 
			
		||||
	...Empty,
 | 
			
		||||
	async play({ canvasElement }) {
 | 
			
		||||
		await expect(canvasElement).toHaveTextContent(i18n.t('_ago.weeksAgo', { n: 1 }));
 | 
			
		||||
		await expect(canvasElement).toHaveTextContent(i18n.tsx._ago.weeksAgo({ n: 1 }));
 | 
			
		||||
	},
 | 
			
		||||
	args: {
 | 
			
		||||
		...Empty.args,
 | 
			
		||||
| 
						 | 
				
			
			@ -240,7 +240,7 @@ export const DetailOneWeekAgo = {
 | 
			
		|||
export const RelativeOneMonthAgo = {
 | 
			
		||||
	...Empty,
 | 
			
		||||
	async play({ canvasElement }) {
 | 
			
		||||
		await expect(canvasElement).toHaveTextContent(i18n.t('_ago.monthsAgo', { n: 1 }));
 | 
			
		||||
		await expect(canvasElement).toHaveTextContent(i18n.tsx._ago.monthsAgo({ n: 1 }));
 | 
			
		||||
	},
 | 
			
		||||
	args: {
 | 
			
		||||
		...Empty.args,
 | 
			
		||||
| 
						 | 
				
			
			@ -279,7 +279,7 @@ export const DetailOneMonthAgo = {
 | 
			
		|||
export const RelativeOneYearAgo = {
 | 
			
		||||
	...Empty,
 | 
			
		||||
	async play({ canvasElement }) {
 | 
			
		||||
		await expect(canvasElement).toHaveTextContent(i18n.t('_ago.yearsAgo', { n: 1 }));
 | 
			
		||||
		await expect(canvasElement).toHaveTextContent(i18n.tsx._ago.yearsAgo({ n: 1 }));
 | 
			
		||||
	},
 | 
			
		||||
	args: {
 | 
			
		||||
		...Empty.args,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -55,21 +55,21 @@ const relative = computed<string>(() => {
 | 
			
		|||
	if (invalid) return i18n.ts._ago.invalid;
 | 
			
		||||
 | 
			
		||||
	return (
 | 
			
		||||
		ago.value >= 31536000 ? i18n.t('_ago.yearsAgo', { n: Math.round(ago.value / 31536000).toString() }) :
 | 
			
		||||
		ago.value >= 2592000 ? i18n.t('_ago.monthsAgo', { n: Math.round(ago.value / 2592000).toString() }) :
 | 
			
		||||
		ago.value >= 604800 ? i18n.t('_ago.weeksAgo', { n: Math.round(ago.value / 604800).toString() }) :
 | 
			
		||||
		ago.value >= 86400 ? i18n.t('_ago.daysAgo', { n: Math.round(ago.value / 86400).toString() }) :
 | 
			
		||||
		ago.value >= 3600 ? i18n.t('_ago.hoursAgo', { n: Math.round(ago.value / 3600).toString() }) :
 | 
			
		||||
		ago.value >= 60 ? i18n.t('_ago.minutesAgo', { n: (~~(ago.value / 60)).toString() }) :
 | 
			
		||||
		ago.value >= 10 ? i18n.t('_ago.secondsAgo', { n: (~~(ago.value % 60)).toString() }) :
 | 
			
		||||
		ago.value >= 31536000 ? i18n.tsx._ago.yearsAgo({ n: Math.round(ago.value / 31536000).toString() }) :
 | 
			
		||||
		ago.value >= 2592000 ? i18n.tsx._ago.monthsAgo({ n: Math.round(ago.value / 2592000).toString() }) :
 | 
			
		||||
		ago.value >= 604800 ? i18n.tsx._ago.weeksAgo({ n: Math.round(ago.value / 604800).toString() }) :
 | 
			
		||||
		ago.value >= 86400 ? i18n.tsx._ago.daysAgo({ n: Math.round(ago.value / 86400).toString() }) :
 | 
			
		||||
		ago.value >= 3600 ? i18n.tsx._ago.hoursAgo({ n: Math.round(ago.value / 3600).toString() }) :
 | 
			
		||||
		ago.value >= 60 ? i18n.tsx._ago.minutesAgo({ n: (~~(ago.value / 60)).toString() }) :
 | 
			
		||||
		ago.value >= 10 ? i18n.tsx._ago.secondsAgo({ n: (~~(ago.value % 60)).toString() }) :
 | 
			
		||||
		ago.value >= -3 ? i18n.ts._ago.justNow :
 | 
			
		||||
		ago.value < -31536000 ? i18n.t('_timeIn.years', { n: Math.round(-ago.value / 31536000).toString() }) :
 | 
			
		||||
		ago.value < -2592000 ? i18n.t('_timeIn.months', { n: Math.round(-ago.value / 2592000).toString() }) :
 | 
			
		||||
		ago.value < -604800 ? i18n.t('_timeIn.weeks', { n: Math.round(-ago.value / 604800).toString() }) :
 | 
			
		||||
		ago.value < -86400 ? i18n.t('_timeIn.days', { n: Math.round(-ago.value / 86400).toString() }) :
 | 
			
		||||
		ago.value < -3600 ? i18n.t('_timeIn.hours', { n: Math.round(-ago.value / 3600).toString() }) :
 | 
			
		||||
		ago.value < -60 ? i18n.t('_timeIn.minutes', { n: (~~(-ago.value / 60)).toString() }) :
 | 
			
		||||
		i18n.t('_timeIn.seconds', { n: (~~(-ago.value % 60)).toString() })
 | 
			
		||||
		ago.value < -31536000 ? i18n.tsx._timeIn.years({ n: Math.round(-ago.value / 31536000).toString() }) :
 | 
			
		||||
		ago.value < -2592000 ? i18n.tsx._timeIn.months({ n: Math.round(-ago.value / 2592000).toString() }) :
 | 
			
		||||
		ago.value < -604800 ? i18n.tsx._timeIn.weeks({ n: Math.round(-ago.value / 604800).toString() }) :
 | 
			
		||||
		ago.value < -86400 ? i18n.tsx._timeIn.days({ n: Math.round(-ago.value / 86400).toString() }) :
 | 
			
		||||
		ago.value < -3600 ? i18n.tsx._timeIn.hours({ n: Math.round(-ago.value / 3600).toString() }) :
 | 
			
		||||
		ago.value < -60 ? i18n.tsx._timeIn.minutes({ n: (~~(-ago.value / 60)).toString() }) :
 | 
			
		||||
		i18n.tsx._timeIn.seconds({ n: (~~(-ago.value % 60)).toString() })
 | 
			
		||||
	);
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,29 +0,0 @@
 | 
			
		|||
/*
 | 
			
		||||
 * SPDX-FileCopyrightText: syuilo and other misskey contributors
 | 
			
		||||
 * SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
import { h } from 'vue';
 | 
			
		||||
 | 
			
		||||
export default function(props: { src: string; tag?: string; textTag?: string; }, { slots }) {
 | 
			
		||||
	let str = props.src;
 | 
			
		||||
	const parsed = [] as (string | { arg: string; })[];
 | 
			
		||||
	while (true) {
 | 
			
		||||
		const nextBracketOpen = str.indexOf('{');
 | 
			
		||||
		const nextBracketClose = str.indexOf('}');
 | 
			
		||||
 | 
			
		||||
		if (nextBracketOpen === -1) {
 | 
			
		||||
			parsed.push(str);
 | 
			
		||||
			break;
 | 
			
		||||
		} else {
 | 
			
		||||
			if (nextBracketOpen > 0) parsed.push(str.substring(0, nextBracketOpen));
 | 
			
		||||
			parsed.push({
 | 
			
		||||
				arg: str.substring(nextBracketOpen + 1, nextBracketClose),
 | 
			
		||||
			});
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		str = str.substring(nextBracketClose + 1);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return h(props.tag ?? 'span', parsed.map(x => typeof x === 'string' ? (props.textTag ? h(props.textTag, x) : x) : slots[x.arg]()));
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -16,7 +16,7 @@ import MkUserName from './global/MkUserName.vue';
 | 
			
		|||
import MkEllipsis from './global/MkEllipsis.vue';
 | 
			
		||||
import MkTime from './global/MkTime.vue';
 | 
			
		||||
import MkUrl from './global/MkUrl.vue';
 | 
			
		||||
import I18n from './global/i18n.js';
 | 
			
		||||
import I18n from './global/I18n.vue';
 | 
			
		||||
import RouterView from './global/RouterView.vue';
 | 
			
		||||
import MkLoading from './global/MkLoading.vue';
 | 
			
		||||
import MkError from './global/MkError.vue';
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -29,7 +29,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		|||
							<template #key>Misskey</template>
 | 
			
		||||
							<template #value>{{ version }}</template>
 | 
			
		||||
						</MkKeyValue>
 | 
			
		||||
						<div v-html="i18n.t('poweredByMisskeyDescription', { name: instance.name ?? host })">
 | 
			
		||||
						<div v-html="i18n.tsx.poweredByMisskeyDescription({ name: instance.name ?? host })">
 | 
			
		||||
						</div>
 | 
			
		||||
						<FormLink to="/about-misskey">{{ i18n.ts.aboutMisskey }}</FormLink>
 | 
			
		||||
					</div>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -104,7 +104,7 @@ fetch();
 | 
			
		|||
async function del() {
 | 
			
		||||
	const { canceled } = await os.confirm({
 | 
			
		||||
		type: 'warning',
 | 
			
		||||
		text: i18n.t('removeAreYouSure', { x: file.value.name }),
 | 
			
		||||
		text: i18n.tsx.removeAreYouSure({ x: file.value.name }),
 | 
			
		||||
	});
 | 
			
		||||
	if (canceled) return;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -182,9 +182,9 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		|||
						</MkSelect>
 | 
			
		||||
					</div>
 | 
			
		||||
					<div class="charts">
 | 
			
		||||
						<div class="label">{{ i18n.t('recentNHours', { n: 90 }) }}</div>
 | 
			
		||||
						<div class="label">{{ i18n.tsx.recentNHours({ n: 90 }) }}</div>
 | 
			
		||||
						<MkChart class="chart" :src="chartSrc" span="hour" :limit="90" :args="{ user, withoutAll: true }" :detailed="true"></MkChart>
 | 
			
		||||
						<div class="label">{{ i18n.t('recentNDays', { n: 90 }) }}</div>
 | 
			
		||||
						<div class="label">{{ i18n.tsx.recentNDays({ n: 90 }) }}</div>
 | 
			
		||||
						<MkChart class="chart" :src="chartSrc" span="day" :limit="90" :args="{ user, withoutAll: true }" :detailed="true"></MkChart>
 | 
			
		||||
					</div>
 | 
			
		||||
				</div>
 | 
			
		||||
| 
						 | 
				
			
			@ -307,7 +307,7 @@ async function resetPassword() {
 | 
			
		|||
		});
 | 
			
		||||
		os.alert({
 | 
			
		||||
			type: 'success',
 | 
			
		||||
			text: i18n.t('newPasswordIs', { password }),
 | 
			
		||||
			text: i18n.tsx.newPasswordIs({ password }),
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -390,7 +390,7 @@ async function deleteAccount() {
 | 
			
		|||
	if (confirm.canceled) return;
 | 
			
		||||
 | 
			
		||||
	const typed = await os.inputText({
 | 
			
		||||
		text: i18n.t('typeToConfirm', { x: user.value?.username }),
 | 
			
		||||
		text: i18n.tsx.typeToConfirm({ x: user.value?.username }),
 | 
			
		||||
	});
 | 
			
		||||
	if (typed.canceled) return;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -160,7 +160,7 @@ function add() {
 | 
			
		|||
function remove(ad) {
 | 
			
		||||
	os.confirm({
 | 
			
		||||
		type: 'warning',
 | 
			
		||||
		text: i18n.t('removeAreYouSure', { x: ad.url }),
 | 
			
		||||
		text: i18n.tsx.removeAreYouSure({ x: ad.url }),
 | 
			
		||||
	}).then(({ canceled }) => {
 | 
			
		||||
		if (canceled) return;
 | 
			
		||||
		ads.value = ads.value.filter(x => x !== ad);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -54,7 +54,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		|||
					<MkSwitch v-model="announcement.needConfirmationToRead" :helpText="i18n.ts._announcement.needConfirmationToReadDescription">
 | 
			
		||||
						{{ i18n.ts._announcement.needConfirmationToRead }}
 | 
			
		||||
					</MkSwitch>
 | 
			
		||||
					<p v-if="announcement.reads">{{ i18n.t('nUsersRead', { n: announcement.reads }) }}</p>
 | 
			
		||||
					<p v-if="announcement.reads">{{ i18n.tsx.nUsersRead({ n: announcement.reads }) }}</p>
 | 
			
		||||
					<div class="buttons _buttons">
 | 
			
		||||
						<MkButton class="button" inline primary @click="save(announcement)"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton>
 | 
			
		||||
						<MkButton v-if="announcement.id != null" class="button" inline @click="archive(announcement)"><i class="ti ti-check"></i> {{ i18n.ts._announcement.end }} ({{ i18n.ts.archive }})</MkButton>
 | 
			
		||||
| 
						 | 
				
			
			@ -109,7 +109,7 @@ function add() {
 | 
			
		|||
function del(announcement) {
 | 
			
		||||
	os.confirm({
 | 
			
		||||
		type: 'warning',
 | 
			
		||||
		text: i18n.t('deleteAreYouSure', { x: announcement.title }),
 | 
			
		||||
		text: i18n.tsx.deleteAreYouSure({ x: announcement.title }),
 | 
			
		||||
	}).then(({ canceled }) => {
 | 
			
		||||
		if (canceled) return;
 | 
			
		||||
		announcements.value = announcements.value.filter(x => x !== announcement);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -19,10 +19,10 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		|||
						<template #prefix><i class="ti ti-link"></i></template>
 | 
			
		||||
						<template #label>{{ i18n.ts._serverSettings.iconUrl }} (App/192px)</template>
 | 
			
		||||
						<template #caption>
 | 
			
		||||
							<div>{{ i18n.t('_serverSettings.appIconDescription', { host: instance.name ?? host }) }}</div>
 | 
			
		||||
							<div>{{ i18n.tsx._serverSettings.appIconDescription({ host: instance.name ?? host }) }}</div>
 | 
			
		||||
							<div>({{ i18n.ts._serverSettings.appIconUsageExample }})</div>
 | 
			
		||||
							<div>{{ i18n.ts._serverSettings.appIconStyleRecommendation }}</div>
 | 
			
		||||
							<div><strong>{{ i18n.t('_serverSettings.appIconResolutionMustBe', { resolution: '192x192px' }) }}</strong></div>
 | 
			
		||||
							<div><strong>{{ i18n.tsx._serverSettings.appIconResolutionMustBe({ resolution: '192x192px' }) }}</strong></div>
 | 
			
		||||
						</template>
 | 
			
		||||
					</MkInput>
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -30,10 +30,10 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		|||
						<template #prefix><i class="ti ti-link"></i></template>
 | 
			
		||||
						<template #label>{{ i18n.ts._serverSettings.iconUrl }} (App/512px)</template>
 | 
			
		||||
						<template #caption>
 | 
			
		||||
							<div>{{ i18n.t('_serverSettings.appIconDescription', { host: instance.name ?? host }) }}</div>
 | 
			
		||||
							<div>{{ i18n.tsx._serverSettings.appIconDescription({ host: instance.name ?? host }) }}</div>
 | 
			
		||||
							<div>({{ i18n.ts._serverSettings.appIconUsageExample }})</div>
 | 
			
		||||
							<div>{{ i18n.ts._serverSettings.appIconStyleRecommendation }}</div>
 | 
			
		||||
							<div><strong>{{ i18n.t('_serverSettings.appIconResolutionMustBe', { resolution: '512x512px' }) }}</strong></div>
 | 
			
		||||
							<div><strong>{{ i18n.tsx._serverSettings.appIconResolutionMustBe({ resolution: '512x512px' }) }}</strong></div>
 | 
			
		||||
						</template>
 | 
			
		||||
					</MkInput>
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -14,7 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		|||
					<i v-if="relay.status === 'accepted'" class="ti ti-check" :class="$style.icon" style="color: var(--success);"></i>
 | 
			
		||||
					<i v-else-if="relay.status === 'rejected'" class="ti ti-ban" :class="$style.icon" style="color: var(--error);"></i>
 | 
			
		||||
					<i v-else class="ti ti-clock" :class="$style.icon"></i>
 | 
			
		||||
					<span>{{ i18n.t(`_relayStatus.${relay.status}`) }}</span>
 | 
			
		||||
					<span>{{ i18n.ts._relayStatus[relay.status] }}</span>
 | 
			
		||||
				</div>
 | 
			
		||||
				<MkButton class="button" inline danger @click="remove(relay.inbox)"><i class="ti ti-trash"></i> {{ i18n.ts.remove }}</MkButton>
 | 
			
		||||
			</div>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -104,7 +104,7 @@ function edit() {
 | 
			
		|||
async function del() {
 | 
			
		||||
	const { canceled } = await os.confirm({
 | 
			
		||||
		type: 'warning',
 | 
			
		||||
		text: i18n.t('deleteAreYouSure', { x: role.name }),
 | 
			
		||||
		text: i18n.tsx.deleteAreYouSure({ x: role.name }),
 | 
			
		||||
	});
 | 
			
		||||
	if (canceled) return;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -78,7 +78,7 @@ async function read(announcement) {
 | 
			
		|||
		const confirm = await os.confirm({
 | 
			
		||||
			type: 'question',
 | 
			
		||||
			title: i18n.ts._announcement.readConfirmTitle,
 | 
			
		||||
			text: i18n.t('_announcement.readConfirmText', { title: announcement.title }),
 | 
			
		||||
			text: i18n.tsx._announcement.readConfirmText({ title: announcement.title }),
 | 
			
		||||
		});
 | 
			
		||||
		if (confirm.canceled) return;
 | 
			
		||||
	}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -6,12 +6,12 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		|||
<template>
 | 
			
		||||
<section>
 | 
			
		||||
	<div v-if="app.permission.length > 0">
 | 
			
		||||
		<p>{{ i18n.t('_auth.permission', { name }) }}</p>
 | 
			
		||||
		<p>{{ i18n.tsx._auth.permission({ name }) }}</p>
 | 
			
		||||
		<ul>
 | 
			
		||||
			<li v-for="p in app.permission" :key="p">{{ i18n.t(`_permissions.${p}`) }}</li>
 | 
			
		||||
			<li v-for="p in app.permission" :key="p">{{ i18n.ts._permissions[p] }}</li>
 | 
			
		||||
		</ul>
 | 
			
		||||
	</div>
 | 
			
		||||
	<div>{{ i18n.t('_auth.shareAccess', { name: `${name} (${app.id})` }) }}</div>
 | 
			
		||||
	<div>{{ i18n.tsx._auth.shareAccess({ name: `${name} (${app.id})` }) }}</div>
 | 
			
		||||
	<div :class="$style.buttons">
 | 
			
		||||
		<MkButton inline @click="cancel">{{ i18n.ts.cancel }}</MkButton>
 | 
			
		||||
		<MkButton inline primary @click="accept">{{ i18n.ts.accept }}</MkButton>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -25,7 +25,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		|||
				<h1>{{ i18n.ts._auth.denied }}</h1>
 | 
			
		||||
			</div>
 | 
			
		||||
			<div v-if="state == 'accepted' && session">
 | 
			
		||||
				<h1>{{ session.app.isAuthorized ? i18n.t('already-authorized') : i18n.ts.allowed }}</h1>
 | 
			
		||||
				<h1>{{ session.app.isAuthorized ? i18n.ts['already-authorized'] : i18n.ts.allowed }}</h1>
 | 
			
		||||
				<p v-if="session.app.callbackUrl">
 | 
			
		||||
					{{ i18n.ts._auth.callback }}
 | 
			
		||||
					<MkEllipsis/>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -60,7 +60,7 @@ function add() {
 | 
			
		|||
function del(avatarDecoration) {
 | 
			
		||||
	os.confirm({
 | 
			
		||||
		type: 'warning',
 | 
			
		||||
		text: i18n.t('deleteAreYouSure', { x: avatarDecoration.name }),
 | 
			
		||||
		text: i18n.tsx.deleteAreYouSure({ x: avatarDecoration.name }),
 | 
			
		||||
	}).then(({ canceled }) => {
 | 
			
		||||
		if (canceled) return;
 | 
			
		||||
		avatarDecorations.value = avatarDecorations.value.filter(x => x !== avatarDecoration);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -174,7 +174,7 @@ function save() {
 | 
			
		|||
async function archive() {
 | 
			
		||||
	const { canceled } = await os.confirm({
 | 
			
		||||
		type: 'warning',
 | 
			
		||||
		title: i18n.t('channelArchiveConfirmTitle', { name: name.value }),
 | 
			
		||||
		title: i18n.tsx.channelArchiveConfirmTitle({ name: name.value }),
 | 
			
		||||
		text: i18n.ts.channelArchiveConfirmDescription,
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -145,7 +145,7 @@ const headerActions = computed(() => clip.value && isOwned.value ? [{
 | 
			
		|||
	handler: async (): Promise<void> => {
 | 
			
		||||
		const { canceled } = await os.confirm({
 | 
			
		||||
			type: 'warning',
 | 
			
		||||
			text: i18n.t('deleteAreYouSure', { x: clip.value.name }),
 | 
			
		||||
			text: i18n.tsx.deleteAreYouSure({ x: clip.value.name }),
 | 
			
		||||
		});
 | 
			
		||||
		if (canceled) return;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -180,7 +180,7 @@ async function deleteFile() {
 | 
			
		|||
 | 
			
		||||
	const { canceled } = await os.confirm({
 | 
			
		||||
		type: 'warning',
 | 
			
		||||
		text: i18n.t('driveFileDeleteConfirm', { name: file.value.name }),
 | 
			
		||||
		text: i18n.tsx.driveFileDeleteConfirm({ name: file.value.name }),
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	if (canceled) return;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -45,7 +45,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		|||
				<div :class="$style.frame">
 | 
			
		||||
					<div :class="$style.frameInner">
 | 
			
		||||
						<div class="_gaps_s" style="padding: 16px;">
 | 
			
		||||
							<div><b>{{ i18n.t('lastNDays', { n: 7 }) }} {{ i18n.ts.ranking }}</b> ({{ gameMode }})</div>
 | 
			
		||||
							<div><b>{{ i18n.tsx.lastNDays({ n: 7 }) }} {{ i18n.ts.ranking }}</b> ({{ gameMode }})</div>
 | 
			
		||||
							<div v-if="ranking" class="_gaps_s">
 | 
			
		||||
								<div v-for="r in ranking" :key="r.id" :class="$style.rankingRecord">
 | 
			
		||||
									<MkAvatar :link="true" style="width: 24px; height: 24px; margin-right: 4px;" :user="r.user"/>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -185,7 +185,7 @@ async function done() {
 | 
			
		|||
async function del() {
 | 
			
		||||
	const { canceled } = await os.confirm({
 | 
			
		||||
		type: 'warning',
 | 
			
		||||
		text: i18n.t('removeAreYouSure', { x: name.value }),
 | 
			
		||||
		text: i18n.tsx.removeAreYouSure({ x: name.value }),
 | 
			
		||||
	});
 | 
			
		||||
	if (canceled) return;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -438,7 +438,7 @@ function show() {
 | 
			
		|||
async function del() {
 | 
			
		||||
	const { canceled } = await os.confirm({
 | 
			
		||||
		type: 'warning',
 | 
			
		||||
		text: i18n.t('deleteAreYouSure', { x: flash.value.title }),
 | 
			
		||||
		text: i18n.tsx.deleteAreYouSure({ x: flash.value.title }),
 | 
			
		||||
	});
 | 
			
		||||
	if (canceled) return;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -20,7 +20,7 @@ import { mainRouter } from '@/global/router/main.js';
 | 
			
		|||
async function follow(user): Promise<void> {
 | 
			
		||||
	const { canceled } = await os.confirm({
 | 
			
		||||
		type: 'question',
 | 
			
		||||
		text: i18n.t('followConfirm', { name: user.name || user.username }),
 | 
			
		||||
		text: i18n.tsx.followConfirm({ name: user.name || user.username }),
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	if (canceled) {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -95,9 +95,9 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		|||
						</MkSelect>
 | 
			
		||||
					</div>
 | 
			
		||||
					<div class="charts">
 | 
			
		||||
						<div class="label">{{ i18n.t('recentNHours', { n: 90 }) }}</div>
 | 
			
		||||
						<div class="label">{{ i18n.tsx.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>
 | 
			
		||||
						<div class="label">{{ i18n.tsx.recentNDays({ n: 90 }) }}</div>
 | 
			
		||||
						<MkChart class="chart" :src="chartSrc" span="day" :limit="90" :args="{ host: host }" :detailed="true"></MkChart>
 | 
			
		||||
					</div>
 | 
			
		||||
				</div>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -19,9 +19,9 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		|||
	</MKSpacer>
 | 
			
		||||
	<MkSpacer v-else :contentMax="800">
 | 
			
		||||
		<div class="_gaps_m" style="text-align: center;">
 | 
			
		||||
			<div v-if="resetCycle && inviteLimit">{{ i18n.t('inviteLimitResetCycle', { time: resetCycle, limit: inviteLimit }) }}</div>
 | 
			
		||||
			<div v-if="resetCycle && inviteLimit">{{ i18n.tsx.inviteLimitResetCycle({ time: resetCycle, limit: inviteLimit }) }}</div>
 | 
			
		||||
			<MkButton inline primary rounded :disabled="currentInviteLimit !== null && currentInviteLimit <= 0" @click="create"><i class="ti ti-user-plus"></i> {{ i18n.ts.createInviteCode }}</MkButton>
 | 
			
		||||
			<div v-if="currentInviteLimit !== null">{{ i18n.t('createLimitRemaining', { limit: currentInviteLimit }) }}</div>
 | 
			
		||||
			<div v-if="currentInviteLimit !== null">{{ i18n.tsx.createLimitRemaining({ limit: currentInviteLimit }) }}</div>
 | 
			
		||||
 | 
			
		||||
			<MkPagination ref="pagingComponent" :pagination="pagination">
 | 
			
		||||
				<template #default="{ items }">
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -20,13 +20,13 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		|||
			</div>
 | 
			
		||||
			<div v-else>
 | 
			
		||||
				<div v-if="_permissions.length > 0">
 | 
			
		||||
					<p v-if="name">{{ i18n.t('_auth.permission', { name }) }}</p>
 | 
			
		||||
					<p v-if="name">{{ i18n.tsx._auth.permission({ name }) }}</p>
 | 
			
		||||
					<p v-else>{{ i18n.ts._auth.permissionAsk }}</p>
 | 
			
		||||
					<ul>
 | 
			
		||||
						<li v-for="p in _permissions" :key="p">{{ i18n.t(`_permissions.${p}`) }}</li>
 | 
			
		||||
						<li v-for="p in _permissions" :key="p">{{ i18n.ts._permissions[p] }}</li>
 | 
			
		||||
					</ul>
 | 
			
		||||
				</div>
 | 
			
		||||
				<div v-if="name">{{ i18n.t('_auth.shareAccess', { name }) }}</div>
 | 
			
		||||
				<div v-if="name">{{ i18n.tsx._auth.shareAccess({ name }) }}</div>
 | 
			
		||||
				<div v-else>{{ i18n.ts._auth.shareAccessAsk }}</div>
 | 
			
		||||
				<div :class="$style.buttons">
 | 
			
		||||
					<MkButton inline @click="deny">{{ i18n.ts.cancel }}</MkButton>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -116,7 +116,7 @@ async function saveAntenna() {
 | 
			
		|||
async function deleteAntenna() {
 | 
			
		||||
	const { canceled } = await os.confirm({
 | 
			
		||||
		type: 'warning',
 | 
			
		||||
		text: i18n.t('removeAreYouSure', { x: props.antenna.name }),
 | 
			
		||||
		text: i18n.tsx.removeAreYouSure({ x: props.antenna.name }),
 | 
			
		||||
	});
 | 
			
		||||
	if (canceled) return;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -19,7 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		|||
 | 
			
		||||
			<div v-if="items.length > 0" class="_gaps">
 | 
			
		||||
				<MkA v-for="list in items" :key="list.id" class="_panel" :class="$style.list" :to="`/my/lists/${ list.id }`">
 | 
			
		||||
					<div style="margin-bottom: 4px;">{{ list.name }} <span :class="$style.nUsers">({{ i18n.t('nUsers', { n: `${list.userIds.length}/${$i.policies['userEachUserListsLimit']}` }) }})</span></div>
 | 
			
		||||
					<div style="margin-bottom: 4px;">{{ list.name }} <span :class="$style.nUsers">({{ i18n.tsx.nUsers({ n: `${list.userIds.length}/${$i.policies['userEachUserListsLimit']}` }) }})</span></div>
 | 
			
		||||
					<MkAvatars :userIds="list.userIds" :limit="10"/>
 | 
			
		||||
				</MkA>
 | 
			
		||||
			</div>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -25,7 +25,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		|||
 | 
			
		||||
			<MkFolder defaultOpen>
 | 
			
		||||
				<template #label>{{ i18n.ts.members }}</template>
 | 
			
		||||
				<template #caption>{{ i18n.t('nUsers', { n: `${list.userIds.length}/${$i.policies['userEachUserListsLimit']}` }) }}</template>
 | 
			
		||||
				<template #caption>{{ i18n.tsx.nUsers({ n: `${list.userIds.length}/${$i.policies['userEachUserListsLimit']}` }) }}</template>
 | 
			
		||||
 | 
			
		||||
				<div class="_gaps_s">
 | 
			
		||||
					<MkButton rounded primary style="margin: 0 auto;" @click="addUser()">{{ i18n.ts.addUser }}</MkButton>
 | 
			
		||||
| 
						 | 
				
			
			@ -155,7 +155,7 @@ async function deleteList() {
 | 
			
		|||
	if (!list.value) return;
 | 
			
		||||
	const { canceled } = await os.confirm({
 | 
			
		||||
		type: 'warning',
 | 
			
		||||
		text: i18n.t('removeAreYouSure', { x: list.value.name }),
 | 
			
		||||
		text: i18n.tsx.removeAreYouSure({ x: list.value.name }),
 | 
			
		||||
	});
 | 
			
		||||
	if (canceled) return;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -147,7 +147,7 @@ definePageMetadata(computed(() => note.value ? {
 | 
			
		|||
	avatar: note.value.user,
 | 
			
		||||
	path: `/notes/${note.value.id}`,
 | 
			
		||||
	share: {
 | 
			
		||||
		title: i18n.t('noteOf', { user: note.value.user.name }),
 | 
			
		||||
		title: i18n.tsx.noteOf({ user: note.value.user.name }),
 | 
			
		||||
		text: note.value.text,
 | 
			
		||||
	},
 | 
			
		||||
} : null));
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -51,7 +51,7 @@ const directNotesPagination = {
 | 
			
		|||
 | 
			
		||||
function setFilter(ev) {
 | 
			
		||||
	const typeItems = notificationTypes.map(t => ({
 | 
			
		||||
		text: i18n.t(`_notification._types.${t}`),
 | 
			
		||||
		text: i18n.ts._notification._types[t],
 | 
			
		||||
		active: includeTypes.value && includeTypes.value.includes(t),
 | 
			
		||||
		action: () => {
 | 
			
		||||
			includeTypes.value = [t];
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -9,13 +9,13 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		|||
	<MkSpacer :contentMax="800">
 | 
			
		||||
		<div v-if="$i">
 | 
			
		||||
			<div v-if="permissions.length > 0">
 | 
			
		||||
				<p v-if="name">{{ i18n.t('_auth.permission', { name }) }}</p>
 | 
			
		||||
				<p v-if="name">{{ i18n.tsx._auth.permission({ name }) }}</p>
 | 
			
		||||
				<p v-else>{{ i18n.ts._auth.permissionAsk }}</p>
 | 
			
		||||
				<ul>
 | 
			
		||||
					<li v-for="p in permissions" :key="p">{{ i18n.t(`_permissions.${p}`) }}</li>
 | 
			
		||||
					<li v-for="p in permissions" :key="p">{{ i18n.ts._permissions[p] }}</li>
 | 
			
		||||
				</ul>
 | 
			
		||||
			</div>
 | 
			
		||||
			<div v-if="name">{{ i18n.t('_auth.shareAccess', { name }) }}</div>
 | 
			
		||||
			<div v-if="name">{{ i18n.tsx._auth.shareAccess({ name }) }}</div>
 | 
			
		||||
			<div v-else>{{ i18n.ts._auth.shareAccessAsk }}</div>
 | 
			
		||||
			<form :class="$style.buttons" action="/oauth/decision" accept-charset="utf-8" method="post">
 | 
			
		||||
				<input name="login_token" type="hidden" :value="$i.token"/>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -175,7 +175,7 @@ function save() {
 | 
			
		|||
function del() {
 | 
			
		||||
	os.confirm({
 | 
			
		||||
		type: 'warning',
 | 
			
		||||
		text: i18n.t('removeAreYouSure', { x: title.value.trim() }),
 | 
			
		||||
		text: i18n.tsx.removeAreYouSure({ x: title.value.trim() }),
 | 
			
		||||
	}).then(({ canceled }) => {
 | 
			
		||||
		if (canceled) return;
 | 
			
		||||
		misskeyApi('pages/delete', {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -10,17 +10,17 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		|||
 | 
			
		||||
		<div style="overflow: clip; line-height: 28px;">
 | 
			
		||||
			<div v-if="!iAmPlayer && !game.isEnded && turnUser" class="turn">
 | 
			
		||||
				<Mfm :key="'turn:' + turnUser.id" :text="i18n.t('_reversi.turnOf', { name: turnUser.name ?? turnUser.username })" :plain="true" :customEmojis="turnUser.emojis"/>
 | 
			
		||||
				<Mfm :key="'turn:' + turnUser.id" :text="i18n.tsx._reversi.turnOf({ name: turnUser.name ?? turnUser.username })" :plain="true" :customEmojis="turnUser.emojis"/>
 | 
			
		||||
				<MkEllipsis/>
 | 
			
		||||
			</div>
 | 
			
		||||
			<div v-if="(logPos !== logs.length) && turnUser" class="turn">
 | 
			
		||||
				<Mfm :key="'past-turn-of:' + turnUser.id" :text="i18n.t('_reversi.pastTurnOf', { name: turnUser.name ?? turnUser.username })" :plain="true" :customEmojis="turnUser.emojis"/>
 | 
			
		||||
				<Mfm :key="'past-turn-of:' + turnUser.id" :text="i18n.tsx._reversi.pastTurnOf({ name: turnUser.name ?? turnUser.username })" :plain="true" :customEmojis="turnUser.emojis"/>
 | 
			
		||||
			</div>
 | 
			
		||||
			<div v-if="iAmPlayer && !game.isEnded && !isMyTurn" class="turn1">{{ i18n.ts._reversi.opponentTurn }}<MkEllipsis/></div>
 | 
			
		||||
			<div v-if="iAmPlayer && !game.isEnded && isMyTurn" class="turn2" style="animation: tada 1s linear infinite both;">{{ i18n.ts._reversi.myTurn }}</div>
 | 
			
		||||
			<div v-if="game.isEnded && logPos == logs.length" class="result">
 | 
			
		||||
				<template v-if="game.winner">
 | 
			
		||||
					<Mfm :key="'won'" :text="i18n.t('_reversi.won', { name: game.winner.name ?? game.winner.username })" :plain="true" :customEmojis="game.winner.emojis"/>
 | 
			
		||||
					<Mfm :key="'won'" :text="i18n.tsx._reversi.won({ name: game.winner.name ?? game.winner.username })" :plain="true" :customEmojis="game.winner.emojis"/>
 | 
			
		||||
					<span v-if="game.surrendered != null"> ({{ i18n.ts._reversi.surrendered }})</span>
 | 
			
		||||
				</template>
 | 
			
		||||
				<template v-else>{{ i18n.ts._reversi.drawn }}</template>
 | 
			
		||||
| 
						 | 
				
			
			@ -62,7 +62,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		|||
			</div>
 | 
			
		||||
		</div>
 | 
			
		||||
 | 
			
		||||
		<div class="status"><b>{{ i18n.t('_reversi.turnCount', { count: logPos }) }}</b> {{ i18n.ts._reversi.black }}:{{ engine.blackCount }} {{ i18n.ts._reversi.white }}:{{ engine.whiteCount }} {{ i18n.ts._reversi.total }}:{{ engine.blackCount + engine.whiteCount }}</div>
 | 
			
		||||
		<div class="status"><b>{{ i18n.tsx._reversi.turnCount({ count: logPos }) }}</b> {{ i18n.ts._reversi.black }}:{{ engine.blackCount }} {{ i18n.ts._reversi.white }}:{{ engine.whiteCount }} {{ i18n.ts._reversi.total }}:{{ engine.blackCount + engine.whiteCount }}</div>
 | 
			
		||||
 | 
			
		||||
		<div v-if="!game.isEnded && iAmPlayer" class="_buttonsCenter">
 | 
			
		||||
			<MkButton danger @click="surrender">{{ i18n.ts._reversi.surrender }}</MkButton>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -141,7 +141,7 @@ async function unregisterKey(key) {
 | 
			
		|||
	const confirm = await os.confirm({
 | 
			
		||||
		type: 'question',
 | 
			
		||||
		title: i18n.ts._2fa.removeKey,
 | 
			
		||||
		text: i18n.t('_2fa.removeKeyConfirm', { name: key.name }),
 | 
			
		||||
		text: i18n.tsx._2fa.removeKeyConfirm({ name: key.name }),
 | 
			
		||||
	});
 | 
			
		||||
	if (confirm.canceled) return;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -30,7 +30,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		|||
						<details>
 | 
			
		||||
							<summary>{{ i18n.ts.details }}</summary>
 | 
			
		||||
							<ul>
 | 
			
		||||
								<li v-for="p in token.permission" :key="p">{{ i18n.t(`_permissions.${p}`) }}</li>
 | 
			
		||||
								<li v-for="p in token.permission" :key="p">{{ i18n.ts._permissions[p] }}</li>
 | 
			
		||||
							</ul>
 | 
			
		||||
						</details>
 | 
			
		||||
						<div>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		|||
<template>
 | 
			
		||||
<div>
 | 
			
		||||
	<div v-if="!loading" class="_gaps">
 | 
			
		||||
		<MkInfo>{{ i18n.t('_profile.avatarDecorationMax', { max: $i.policies.avatarDecorationLimit }) }} ({{ i18n.t('remainingN', { n: $i.policies.avatarDecorationLimit - $i.avatarDecorations.length }) }})</MkInfo>
 | 
			
		||||
		<MkInfo>{{ i18n.tsx._profile.avatarDecorationMax({ max: $i.policies.avatarDecorationLimit }) }} ({{ i18n.tsx.remainingN({ n: $i.policies.avatarDecorationLimit - $i.avatarDecorations.length }) }})</MkInfo>
 | 
			
		||||
 | 
			
		||||
		<MkAvatar :class="$style.avatar" :user="$i" forceShowDecoration/>
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -77,9 +77,9 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		|||
			<MkRadios v-model="mediaListWithOneImageAppearance">
 | 
			
		||||
				<template #label>{{ i18n.ts.mediaListWithOneImageAppearance }}</template>
 | 
			
		||||
				<option value="expand">{{ i18n.ts.default }}</option>
 | 
			
		||||
				<option value="16_9">{{ i18n.t('limitTo', { x: '16:9' }) }}</option>
 | 
			
		||||
				<option value="1_1">{{ i18n.t('limitTo', { x: '1:1' }) }}</option>
 | 
			
		||||
				<option value="2_3">{{ i18n.t('limitTo', { x: '2:3' }) }}</option>
 | 
			
		||||
				<option value="16_9">{{ i18n.tsx.limitTo({ x: '16:9' }) }}</option>
 | 
			
		||||
				<option value="1_1">{{ i18n.tsx.limitTo({ x: '1:1' }) }}</option>
 | 
			
		||||
				<option value="2_3">{{ i18n.tsx.limitTo({ x: '2:3' }) }}</option>
 | 
			
		||||
			</MkRadios>
 | 
			
		||||
		</div>
 | 
			
		||||
	</FormSection>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -21,7 +21,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		|||
			<div class="_gaps">
 | 
			
		||||
				<MkInput v-for="(_, i) in accountAliases" v-model="accountAliases[i]">
 | 
			
		||||
					<template #prefix><i class="ti ti-plane-arrival"></i></template>
 | 
			
		||||
					<template #label>{{ i18n.t('_accountMigration.moveFromLabel', { n: i + 1 }) }}</template>
 | 
			
		||||
					<template #label>{{ i18n.tsx._accountMigration.moveFromLabel({ n: i + 1 }) }}</template>
 | 
			
		||||
				</MkInput>
 | 
			
		||||
			</div>
 | 
			
		||||
		</div>
 | 
			
		||||
| 
						 | 
				
			
			@ -97,7 +97,7 @@ async function move(): Promise<void> {
 | 
			
		|||
	const account = moveToAccount.value;
 | 
			
		||||
	const confirm = await os.confirm({
 | 
			
		||||
		type: 'warning',
 | 
			
		||||
		text: i18n.t('_accountMigration.migrationConfirm', { account }),
 | 
			
		||||
		text: i18n.tsx._accountMigration.migrationConfirm({ account }),
 | 
			
		||||
	});
 | 
			
		||||
	if (confirm.canceled) return;
 | 
			
		||||
	await os.apiWithDialog('i/move', {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -64,7 +64,7 @@ async function save() {
 | 
			
		|||
					os.alert({
 | 
			
		||||
						type: 'error',
 | 
			
		||||
						title: i18n.ts.regexpError,
 | 
			
		||||
						text: i18n.t('regexpErrorDescription', { tab: 'word mute', line: i + 1 }) + '\n' + err.toString(),
 | 
			
		||||
						text: i18n.tsx.regexpErrorDescription({ tab: 'word mute', line: i + 1 }) + '\n' + err.toString(),
 | 
			
		||||
					});
 | 
			
		||||
					// re-throw error so these invalid settings are not saved
 | 
			
		||||
					throw err;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		|||
		<template #label>{{ i18n.ts.notificationRecieveConfig }}</template>
 | 
			
		||||
		<div class="_gaps_s">
 | 
			
		||||
			<MkFolder v-for="type in notificationTypes.filter(x => !nonConfigurableNotificationTypes.includes(x))" :key="type">
 | 
			
		||||
				<template #label>{{ i18n.t('_notification._types.' + type) }}</template>
 | 
			
		||||
				<template #label>{{ i18n.ts._notification._types[type] }}</template>
 | 
			
		||||
				<template #suffix>
 | 
			
		||||
					{{
 | 
			
		||||
						$i.notificationRecieveConfig[type]?.type === 'never' ? i18n.ts.none :
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -206,7 +206,7 @@ function changeAvatar(ev) {
 | 
			
		|||
 | 
			
		||||
		const { canceled } = await os.confirm({
 | 
			
		||||
			type: 'question',
 | 
			
		||||
			text: i18n.t('cropImageAsk'),
 | 
			
		||||
			text: i18n.ts.cropImageAsk,
 | 
			
		||||
			okText: i18n.ts.cropYes,
 | 
			
		||||
			cancelText: i18n.ts.cropNo,
 | 
			
		||||
		});
 | 
			
		||||
| 
						 | 
				
			
			@ -232,7 +232,7 @@ function changeBanner(ev) {
 | 
			
		|||
 | 
			
		||||
		const { canceled } = await os.confirm({
 | 
			
		||||
			type: 'question',
 | 
			
		||||
			text: i18n.t('cropImageAsk'),
 | 
			
		||||
			text: i18n.ts.cropImageAsk,
 | 
			
		||||
			okText: i18n.ts.cropYes,
 | 
			
		||||
			cancelText: i18n.ts.cropNo,
 | 
			
		||||
		});
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -19,7 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		|||
		<template #label>{{ i18n.ts.sounds }}</template>
 | 
			
		||||
		<div class="_gaps_s">
 | 
			
		||||
			<MkFolder v-for="type in operationTypes" :key="type">
 | 
			
		||||
				<template #label>{{ i18n.t('_sfx.' + type) }}</template>
 | 
			
		||||
				<template #label>{{ i18n.ts._sfx[type] }}</template>
 | 
			
		||||
				<template #suffix>{{ getSoundTypeName(sounds[type].type) }}</template>
 | 
			
		||||
 | 
			
		||||
				<XSound :type="sounds[type].type" :volume="sounds[type].volume" :fileId="sounds[type].fileId" :fileUrl="sounds[type].fileUrl" @update="(res) => updated(type, res)"/>
 | 
			
		||||
| 
						 | 
				
			
			@ -33,9 +33,9 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		|||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { Ref, computed, ref } from 'vue';
 | 
			
		||||
import XSound from './sounds.sound.vue';
 | 
			
		||||
import type { SoundType, OperationType } from '@/scripts/sound.js';
 | 
			
		||||
import type { SoundStore } from '@/store.js';
 | 
			
		||||
import XSound from './sounds.sound.vue';
 | 
			
		||||
import MkRange from '@/components/MkRange.vue';
 | 
			
		||||
import MkButton from '@/components/MkButton.vue';
 | 
			
		||||
import FormSection from '@/components/form/section.vue';
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -33,7 +33,7 @@ async function install(code: string): Promise<void> {
 | 
			
		|||
		await installTheme(code);
 | 
			
		||||
		os.alert({
 | 
			
		||||
			type: 'success',
 | 
			
		||||
			text: i18n.t('_theme.installed', { name: theme.name }),
 | 
			
		||||
			text: i18n.tsx._theme.installed({ name: theme.name }),
 | 
			
		||||
		});
 | 
			
		||||
	} catch (err) {
 | 
			
		||||
		switch (err.message.toLowerCase()) {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -99,7 +99,7 @@ async function save(): Promise<void> {
 | 
			
		|||
async function del(): Promise<void> {
 | 
			
		||||
	const { canceled } = await os.confirm({
 | 
			
		||||
		type: 'warning',
 | 
			
		||||
		text: i18n.t('deleteAreYouSure', { x: webhook.name }),
 | 
			
		||||
		text: i18n.tsx.deleteAreYouSure({ x: webhook.name }),
 | 
			
		||||
	});
 | 
			
		||||
	if (canceled) return;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		|||
				<i class="ti ti-user-check"></i>
 | 
			
		||||
			</div>
 | 
			
		||||
			<div class="_gaps_m" style="padding: 32px;">
 | 
			
		||||
				<div>{{ i18n.t('clickToFinishEmailVerification', { ok: i18n.ts.gotIt }) }}</div>
 | 
			
		||||
				<div>{{ i18n.tsx.clickToFinishEmailVerification({ ok: i18n.ts.gotIt }) }}</div>
 | 
			
		||||
				<div>
 | 
			
		||||
					<MkButton gradate large rounded type="submit" :disabled="submitting" data-cy-admin-ok style="margin: 0 auto;">
 | 
			
		||||
						{{ submitting ? i18n.ts.processing : i18n.ts.gotIt }}<MkEllipsis v-if="submitting"/>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -208,7 +208,7 @@ async function saveAs() {
 | 
			
		|||
	changed.value = false;
 | 
			
		||||
	os.alert({
 | 
			
		||||
		type: 'success',
 | 
			
		||||
		text: i18n.t('_theme.installed', { name: theme.value.name }),
 | 
			
		||||
		text: i18n.tsx._theme.installed({ name: theme.value.name }),
 | 
			
		||||
	});
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -87,7 +87,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		|||
						</dl>
 | 
			
		||||
						<dl v-if="user.birthday" class="field">
 | 
			
		||||
							<dt class="name"><i class="ti ti-cake ti-fw"></i> {{ i18n.ts.birthday }}</dt>
 | 
			
		||||
							<dd class="value">{{ user.birthday.replace('-', '/').replace('-', '/') }} ({{ i18n.t('yearsOld', { age }) }})</dd>
 | 
			
		||||
							<dd class="value">{{ user.birthday.replace('-', '/').replace('-', '/') }} ({{ i18n.tsx.yearsOld({ age }) }})</dd>
 | 
			
		||||
						</dl>
 | 
			
		||||
						<dl class="field">
 | 
			
		||||
							<dt class="name"><i class="ti ti-calendar ti-fw"></i> {{ i18n.ts.registeredDate }}</dt>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -66,7 +66,7 @@ function addApp() {
 | 
			
		|||
async function deleteFile(file: Misskey.entities.DriveFile) {
 | 
			
		||||
	const { canceled } = await os.confirm({
 | 
			
		||||
		type: 'warning',
 | 
			
		||||
		text: i18n.t('driveFileDeleteConfirm', { name: file.name }),
 | 
			
		||||
		text: i18n.tsx.driveFileDeleteConfirm({ name: file.name }),
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	if (canceled) return;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -47,7 +47,7 @@ export async function getNoteClipMenu(props: {
 | 
			
		|||
					if (err.id === '734806c4-542c-463a-9311-15c512803965') {
 | 
			
		||||
						const confirm = await os.confirm({
 | 
			
		||||
							type: 'warning',
 | 
			
		||||
							text: i18n.t('confirmToUnclipAlreadyClippedNote', { name: clip.name }),
 | 
			
		||||
							text: i18n.tsx.confirmToUnclipAlreadyClippedNote({ name: clip.name }),
 | 
			
		||||
						});
 | 
			
		||||
						if (!confirm.canceled) {
 | 
			
		||||
							os.apiWithDialog('clips/remove-note', { clipId: clip.id, noteId: appearNote.id });
 | 
			
		||||
| 
						 | 
				
			
			@ -231,7 +231,7 @@ export function getNoteMenu(props: {
 | 
			
		|||
 | 
			
		||||
	function share(): void {
 | 
			
		||||
		navigator.share({
 | 
			
		||||
			title: i18n.t('noteOf', { user: appearNote.user.name }),
 | 
			
		||||
			title: i18n.tsx.noteOf({ user: appearNote.user.name }),
 | 
			
		||||
			text: appearNote.text,
 | 
			
		||||
			url: `${url}/notes/${appearNote.id}`,
 | 
			
		||||
		});
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -30,7 +30,7 @@ export const getNoteSummary = (note: Misskey.entities.Note): string => {
 | 
			
		|||
 | 
			
		||||
	// ファイルが添付されているとき
 | 
			
		||||
	if ((note.files || []).length !== 0) {
 | 
			
		||||
		summary += ` (${i18n.t('withNFiles', { n: note.files.length })})`;
 | 
			
		||||
		summary += ` (${i18n.tsx.withNFiles({ n: note.files.length })})`;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 投票が添付されているとき
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -14,37 +14,39 @@ type FlattenKeys<T extends ILocale, TPrediction> = keyof {
 | 
			
		|||
			: never]: T[K];
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
type ParametersOf<T extends ILocale, TKey extends FlattenKeys<T, ParameterizedString<string>>> = T extends ILocale
 | 
			
		||||
	? TKey extends `${infer K}.${infer C}`
 | 
			
		||||
		// @ts-expect-error -- C は明らかに FlattenKeys<T[K], ParameterizedString<string>> になるが、型システムはここでは TKey がドット区切りであることのコンテキストを持たないので、型システムに合法にて示すことはできない。
 | 
			
		||||
type ParametersOf<T extends ILocale, TKey extends FlattenKeys<T, ParameterizedString>> = TKey extends `${infer K}.${infer C}`
 | 
			
		||||
	// @ts-expect-error -- C は明らかに FlattenKeys<T[K], ParameterizedString> になるが、型システムはここでは TKey がドット区切りであることのコンテキストを持たないので、型システムに合法にて示すことはできない。
 | 
			
		||||
	? ParametersOf<T[K], C>
 | 
			
		||||
	: TKey extends keyof T
 | 
			
		||||
		? T[TKey] extends ParameterizedString<infer P>
 | 
			
		||||
			? P
 | 
			
		||||
			: never
 | 
			
		||||
			: never
 | 
			
		||||
		: never;
 | 
			
		||||
 | 
			
		||||
type Ts<T extends ILocale> = {
 | 
			
		||||
	readonly [K in keyof T as T[K] extends ParameterizedString<string> ? never : K]: T[K] extends ILocale ? Ts<T[K]> : string;
 | 
			
		||||
type Tsx<T extends ILocale> = {
 | 
			
		||||
	readonly [K in keyof T as T[K] extends string ? never : K]: T[K] extends ParameterizedString<infer P>
 | 
			
		||||
		? (arg: { readonly [_ in P]: string | number }) => string
 | 
			
		||||
		// @ts-expect-error -- 証明省略
 | 
			
		||||
		: Tsx<T[K]>;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export class I18n<T extends ILocale> {
 | 
			
		||||
	constructor(private locale: T) {
 | 
			
		||||
	private tsxCache?: Tsx<T>;
 | 
			
		||||
 | 
			
		||||
	constructor(public locale: T) {
 | 
			
		||||
		//#region BIND
 | 
			
		||||
		this.t = this.t.bind(this);
 | 
			
		||||
		//#endregion
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	public get ts(): Ts<T> {
 | 
			
		||||
	public get ts(): T {
 | 
			
		||||
		if (_DEV_) {
 | 
			
		||||
			class Handler<TTarget extends object> implements ProxyHandler<TTarget> {
 | 
			
		||||
			class Handler<TTarget extends ILocale> implements ProxyHandler<TTarget> {
 | 
			
		||||
				get(target: TTarget, p: string | symbol): unknown {
 | 
			
		||||
					const value = target[p as keyof TTarget];
 | 
			
		||||
 | 
			
		||||
					if (typeof value === 'object') {
 | 
			
		||||
						// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- 実際には null がくることはないので。
 | 
			
		||||
						return new Proxy(value!, new Handler<TTarget[keyof TTarget] & object>());
 | 
			
		||||
						return new Proxy(value, new Handler<TTarget[keyof TTarget] & ILocale>());
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
					if (typeof value === 'string') {
 | 
			
		||||
| 
						 | 
				
			
			@ -63,19 +65,148 @@ export class I18n<T extends ILocale> {
 | 
			
		|||
				}
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			return new Proxy(this.locale, new Handler()) as Ts<T>;
 | 
			
		||||
			return new Proxy(this.locale, new Handler());
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		return this.locale as Ts<T>;
 | 
			
		||||
		return this.locale;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	public get tsx(): Tsx<T> {
 | 
			
		||||
		if (_DEV_) {
 | 
			
		||||
			if (this.tsxCache) {
 | 
			
		||||
				return this.tsxCache;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			class Handler<TTarget extends ILocale> implements ProxyHandler<TTarget> {
 | 
			
		||||
				get(target: TTarget, p: string | symbol): unknown {
 | 
			
		||||
					const value = target[p as keyof TTarget];
 | 
			
		||||
 | 
			
		||||
					if (typeof value === 'object') {
 | 
			
		||||
						return new Proxy(value, new Handler<TTarget[keyof TTarget] & ILocale>());
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
					if (typeof value === 'string') {
 | 
			
		||||
						const quasis: string[] = [];
 | 
			
		||||
						const expressions: string[] = [];
 | 
			
		||||
						let cursor = 0;
 | 
			
		||||
 | 
			
		||||
						while (~cursor) {
 | 
			
		||||
							const start = value.indexOf('{', cursor);
 | 
			
		||||
 | 
			
		||||
							if (!~start) {
 | 
			
		||||
								quasis.push(value.slice(cursor));
 | 
			
		||||
								break;
 | 
			
		||||
							}
 | 
			
		||||
 | 
			
		||||
							quasis.push(value.slice(cursor, start));
 | 
			
		||||
 | 
			
		||||
							const end = value.indexOf('}', start);
 | 
			
		||||
 | 
			
		||||
							expressions.push(value.slice(start + 1, end));
 | 
			
		||||
 | 
			
		||||
							cursor = end + 1;
 | 
			
		||||
						}
 | 
			
		||||
 | 
			
		||||
						if (!expressions.length) {
 | 
			
		||||
							console.error(`Unexpected locale key: ${String(p)}`);
 | 
			
		||||
 | 
			
		||||
							return () => value;
 | 
			
		||||
						}
 | 
			
		||||
 | 
			
		||||
						return (arg) => {
 | 
			
		||||
							let str = quasis[0];
 | 
			
		||||
 | 
			
		||||
							for (let i = 0; i < expressions.length; i++) {
 | 
			
		||||
								if (!Object.hasOwn(arg, expressions[i])) {
 | 
			
		||||
									console.error(`Missing locale parameters: ${expressions[i]} at ${String(p)}`);
 | 
			
		||||
								}
 | 
			
		||||
 | 
			
		||||
								str += arg[expressions[i]] + quasis[i + 1];
 | 
			
		||||
							}
 | 
			
		||||
 | 
			
		||||
							return str;
 | 
			
		||||
						};
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
					console.error(`Unexpected locale key: ${String(p)}`);
 | 
			
		||||
 | 
			
		||||
					return p;
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			return this.tsxCache = new Proxy(this.locale, new Handler()) as unknown as Tsx<T>;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
 | 
			
		||||
		if (this.tsxCache) {
 | 
			
		||||
			return this.tsxCache;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		function build(target: ILocale): Tsx<T> {
 | 
			
		||||
			const result = {} as Tsx<T>;
 | 
			
		||||
 | 
			
		||||
			for (const k in target) {
 | 
			
		||||
				if (!Object.hasOwn(target, k)) {
 | 
			
		||||
					continue;
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				const value = target[k as keyof typeof target];
 | 
			
		||||
 | 
			
		||||
				if (typeof value === 'object') {
 | 
			
		||||
					result[k] = build(value as ILocale);
 | 
			
		||||
				} else if (typeof value === 'string') {
 | 
			
		||||
					const quasis: string[] = [];
 | 
			
		||||
					const expressions: string[] = [];
 | 
			
		||||
					let cursor = 0;
 | 
			
		||||
 | 
			
		||||
					while (~cursor) {
 | 
			
		||||
						const start = value.indexOf('{', cursor);
 | 
			
		||||
 | 
			
		||||
						if (!~start) {
 | 
			
		||||
							quasis.push(value.slice(cursor));
 | 
			
		||||
							break;
 | 
			
		||||
						}
 | 
			
		||||
 | 
			
		||||
						quasis.push(value.slice(cursor, start));
 | 
			
		||||
 | 
			
		||||
						const end = value.indexOf('}', start);
 | 
			
		||||
 | 
			
		||||
						expressions.push(value.slice(start + 1, end));
 | 
			
		||||
 | 
			
		||||
						cursor = end + 1;
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
					if (!expressions.length) {
 | 
			
		||||
						continue;
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
					result[k] = (arg) => {
 | 
			
		||||
						let str = quasis[0];
 | 
			
		||||
 | 
			
		||||
						for (let i = 0; i < expressions.length; i++) {
 | 
			
		||||
							str += arg[expressions[i]] + quasis[i + 1];
 | 
			
		||||
						}
 | 
			
		||||
 | 
			
		||||
						return str;
 | 
			
		||||
					};
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
			return result;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		return this.tsxCache = build(this.locale);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * @deprecated なるべくこのメソッド使うよりも locale 直接参照の方が vue のキャッシュ効いてパフォーマンスが良いかも
 | 
			
		||||
	 * @deprecated なるべくこのメソッド使うよりも ts 直接参照の方が vue のキャッシュ効いてパフォーマンスが良いかも
 | 
			
		||||
	 */
 | 
			
		||||
	public t<TKey extends FlattenKeys<T, string>>(key: TKey): string;
 | 
			
		||||
	public t<TKey extends FlattenKeys<T, ParameterizedString<string>>>(key: TKey, args: { readonly [_ in ParametersOf<T, TKey>]: string | number }): string;
 | 
			
		||||
	/**
 | 
			
		||||
	 * @deprecated なるべくこのメソッド使うよりも tsx 直接参照の方が vue のキャッシュ効いてパフォーマンスが良いかも
 | 
			
		||||
	 */
 | 
			
		||||
	public t<TKey extends FlattenKeys<T, ParameterizedString>>(key: TKey, args: { readonly [_ in ParametersOf<T, TKey>]: string | number }): string;
 | 
			
		||||
	public t(key: string, args?: { readonly [_: string]: string | number }) {
 | 
			
		||||
		let str: string | ParameterizedString<string> | ILocale = this.locale;
 | 
			
		||||
		let str: string | ParameterizedString | ILocale = this.locale;
 | 
			
		||||
 | 
			
		||||
		for (const k of key.split('.')) {
 | 
			
		||||
			str = str[k];
 | 
			
		||||
| 
						 | 
				
			
			@ -113,3 +244,51 @@ export class I18n<T extends ILocale> {
 | 
			
		|||
		return str;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
if (import.meta.vitest) {
 | 
			
		||||
	const { describe, expect, it } = import.meta.vitest;
 | 
			
		||||
 | 
			
		||||
	describe('i18n', () => {
 | 
			
		||||
		it('t', () => {
 | 
			
		||||
			const i18n = new I18n({
 | 
			
		||||
				foo: 'foo',
 | 
			
		||||
				bar: {
 | 
			
		||||
					baz: 'baz',
 | 
			
		||||
					qux: 'qux {0}' as unknown as ParameterizedString<'0'>,
 | 
			
		||||
					quux: 'quux {0} {1}' as unknown as ParameterizedString<'0' | '1'>,
 | 
			
		||||
				},
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			expect(i18n.t('foo')).toBe('foo');
 | 
			
		||||
			expect(i18n.t('bar.baz')).toBe('baz');
 | 
			
		||||
			expect(i18n.tsx.bar.qux({ 0: 'hoge' })).toBe('qux hoge');
 | 
			
		||||
			expect(i18n.tsx.bar.quux({ 0: 'hoge', 1: 'fuga' })).toBe('quux hoge fuga');
 | 
			
		||||
		});
 | 
			
		||||
		it('ts', () => {
 | 
			
		||||
			const i18n = new I18n({
 | 
			
		||||
				foo: 'foo',
 | 
			
		||||
				bar: {
 | 
			
		||||
					baz: 'baz',
 | 
			
		||||
					qux: 'qux {0}' as unknown as ParameterizedString<'0'>,
 | 
			
		||||
					quux: 'quux {0} {1}' as unknown as ParameterizedString<'0' | '1'>,
 | 
			
		||||
				},
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			expect(i18n.ts.foo).toBe('foo');
 | 
			
		||||
			expect(i18n.ts.bar.baz).toBe('baz');
 | 
			
		||||
		});
 | 
			
		||||
		it('tsx', () => {
 | 
			
		||||
			const i18n = new I18n({
 | 
			
		||||
				foo: 'foo',
 | 
			
		||||
				bar: {
 | 
			
		||||
					baz: 'baz',
 | 
			
		||||
					qux: 'qux {0}' as unknown as ParameterizedString<'0'>,
 | 
			
		||||
					quux: 'quux {0} {1}' as unknown as ParameterizedString<'0' | '1'>,
 | 
			
		||||
				},
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			expect(i18n.tsx.bar.qux({ 0: 'hoge' })).toBe('qux hoge');
 | 
			
		||||
			expect(i18n.tsx.bar.quux({ 0: 'hoge', 1: 'fuga' })).toBe('quux hoge fuga');
 | 
			
		||||
		});
 | 
			
		||||
	});
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -189,7 +189,7 @@ const addColumn = async (ev) => {
 | 
			
		|||
	const { canceled, result: column } = await os.select({
 | 
			
		||||
		title: i18n.ts._deck.addColumn,
 | 
			
		||||
		items: columns.map(column => ({
 | 
			
		||||
			value: column, text: i18n.t('_deck._columns.' + column),
 | 
			
		||||
			value: column, text: i18n.ts._deck._columns[column],
 | 
			
		||||
		})),
 | 
			
		||||
	});
 | 
			
		||||
	if (canceled) return;
 | 
			
		||||
| 
						 | 
				
			
			@ -197,7 +197,7 @@ const addColumn = async (ev) => {
 | 
			
		|||
	addColumnToStore({
 | 
			
		||||
		type: column,
 | 
			
		||||
		id: uuid(),
 | 
			
		||||
		name: i18n.t('_deck._columns.' + column),
 | 
			
		||||
		name: i18n.ts._deck._columns[column],
 | 
			
		||||
		width: 330,
 | 
			
		||||
	});
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			@ -256,7 +256,7 @@ function changeProfile(ev: MouseEvent) {
 | 
			
		|||
async function deleteProfile() {
 | 
			
		||||
	const { canceled } = await os.confirm({
 | 
			
		||||
		type: 'warning',
 | 
			
		||||
		text: i18n.t('deleteAreYouSure', { x: deckStore.state.profile }),
 | 
			
		||||
		text: i18n.tsx.deleteAreYouSure({ x: deckStore.state.profile }),
 | 
			
		||||
	});
 | 
			
		||||
	if (canceled) return;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -7,11 +7,11 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		|||
<div :class="[$style.root, { _panel: !widgetProps.transparent }]" data-cy-mkw-calendar>
 | 
			
		||||
	<div :class="[$style.calendar, { [$style.isHoliday]: isHoliday }]">
 | 
			
		||||
		<p :class="$style.monthAndYear">
 | 
			
		||||
			<span :class="$style.year">{{ i18n.t('yearX', { year }) }}</span>
 | 
			
		||||
			<span :class="$style.month">{{ i18n.t('monthX', { month }) }}</span>
 | 
			
		||||
			<span :class="$style.year">{{ i18n.tsx.yearX({ year }) }}</span>
 | 
			
		||||
			<span :class="$style.month">{{ i18n.tsx.monthX({ month }) }}</span>
 | 
			
		||||
		</p>
 | 
			
		||||
		<p v-if="month === 1 && day === 1" class="day">🎉{{ i18n.t('dayX', { day }) }}<span style="display: inline-block; transform: scaleX(-1);">🎉</span></p>
 | 
			
		||||
		<p v-else :class="$style.day">{{ i18n.t('dayX', { day }) }}</p>
 | 
			
		||||
		<p v-if="month === 1 && day === 1" class="day">🎉{{ i18n.tsx.dayX({ day }) }}<span style="display: inline-block; transform: scaleX(-1);">🎉</span></p>
 | 
			
		||||
		<p v-else :class="$style.day">{{ i18n.tsx.dayX({ day }) }}</p>
 | 
			
		||||
		<p :class="$style.weekDay">{{ weekDay }}</p>
 | 
			
		||||
	</div>
 | 
			
		||||
	<div :class="$style.info">
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		|||
		<p v-if="widgetProps.folderId == null">
 | 
			
		||||
			{{ i18n.ts.folder }}
 | 
			
		||||
		</p>
 | 
			
		||||
		<p v-if="widgetProps.folderId != null && images.length === 0 && !fetching">{{ i18n.t('no-image') }}</p>
 | 
			
		||||
		<p v-if="widgetProps.folderId != null && images.length === 0 && !fetching">{{ i18n.ts['no-image'] }}</p>
 | 
			
		||||
		<div ref="slideA" class="slide a"></div>
 | 
			
		||||
		<div ref="slideB" class="slide b"></div>
 | 
			
		||||
	</div>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -15,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		|||
	</template>
 | 
			
		||||
	<template #header>
 | 
			
		||||
		<button class="_button" @click="choose">
 | 
			
		||||
			<span>{{ widgetProps.src === 'list' ? widgetProps.list.name : widgetProps.src === 'antenna' ? widgetProps.antenna.name : i18n.t('_timelines.' + widgetProps.src) }}</span>
 | 
			
		||||
			<span>{{ widgetProps.src === 'list' ? widgetProps.list.name : widgetProps.src === 'antenna' ? widgetProps.antenna.name : i18n.ts._timelines[widgetProps.src] }}</span>
 | 
			
		||||
			<i :class="menuOpened ? 'ti ti-chevron-up' : 'ti ti-chevron-down'" style="margin-left: 8px;"></i>
 | 
			
		||||
		</button>
 | 
			
		||||
	</template>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -14,7 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		|||
			<div v-for="stat in stats" :key="stat.tag">
 | 
			
		||||
				<div class="tag">
 | 
			
		||||
					<MkA class="a" :to="`/tags/${ encodeURIComponent(stat.tag) }`" :title="stat.tag">#{{ stat.tag }}</MkA>
 | 
			
		||||
					<p>{{ i18n.t('nUsersMentioned', { n: stat.usersCount }) }}</p>
 | 
			
		||||
					<p>{{ i18n.tsx.nUsersMentioned({ n: stat.usersCount }) }}</p>
 | 
			
		||||
				</div>
 | 
			
		||||
				<MkMiniChart class="chart" :src="stat.chart"/>
 | 
			
		||||
			</div>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -33,6 +33,7 @@
 | 
			
		|||
		],
 | 
			
		||||
		"types": [
 | 
			
		||||
			"vite/client",
 | 
			
		||||
			"vitest/importMeta",
 | 
			
		||||
		],
 | 
			
		||||
		"lib": [
 | 
			
		||||
			"esnext",
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -155,6 +155,7 @@ export function getConfig(): UserConfig {
 | 
			
		|||
					},
 | 
			
		||||
				},
 | 
			
		||||
			},
 | 
			
		||||
			includeSource: ['src/**/*.ts'],
 | 
			
		||||
		},
 | 
			
		||||
	};
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue