feat: i18n

This commit is contained in:
Shusui MOYATANI
2023-09-09 01:09:31 +09:00
parent d2be5a9229
commit 15c8d0c924
7 changed files with 142 additions and 59 deletions

View File

@@ -13,6 +13,7 @@ import UserNameDisplay from '@/components/UserDisplayName';
import useConfig from '@/core/useConfig';
import useEmojiComplete from '@/hooks/useEmojiComplete';
import usePersistStatus from '@/hooks/usePersistStatus';
import { useTranslation } from '@/i18n/useTranslation';
import { textNote } from '@/nostr/event';
import parseTextNote, { ParsedTextNote } from '@/nostr/parseTextNote';
import useCommands, { PublishTextNoteParams } from '@/nostr/useCommands';
@@ -28,16 +29,6 @@ type NotePostFormProps = {
textAreaRef?: (textAreaRef: HTMLTextAreaElement) => void;
};
const placeholder = (mode: NotePostFormProps['mode']) => {
switch (mode) {
case 'reply':
return '返信を投稿';
case 'normal':
default:
return 'いまどうしてる?';
}
};
const extract = (parsed: ParsedTextNote) => {
const hashtags: string[] = [];
const pubkeyReferences: string[] = [];
@@ -86,6 +77,7 @@ const format = (parsed: ParsedTextNote) => {
};
const NotePostForm: Component<NotePostFormProps> = (props) => {
const i18n = useTranslation();
let textAreaRef: HTMLTextAreaElement | undefined;
let fileInputRef: HTMLInputElement | undefined;
@@ -108,6 +100,16 @@ const NotePostForm: Component<NotePostFormProps> = (props) => {
props.onClose();
};
const placeholder = (mode: NotePostFormProps['mode']) => {
switch (mode) {
case 'reply':
return i18n()('posting.placeholderReply');
case 'normal':
default:
return i18n()('posting.placeholder');
}
};
const { config, getEmoji } = useConfig();
const { persistStatus, didAgreeToToS, agreeToToS } = usePersistStatus();
const getPubkey = usePubkey();
@@ -152,7 +154,7 @@ const NotePostForm: Component<NotePostFormProps> = (props) => {
if (failed.length > 0) {
const filenames = failed.map((f) => f.name).join(', ');
window.alert(`ファイルのアップロードに失敗しました: ${filenames}`);
window.alert(i18n()('posting.failedToUploadFile', { filenames }));
}
},
});
@@ -194,7 +196,7 @@ const NotePostForm: Component<NotePostFormProps> = (props) => {
if (publishTextNoteMutation.isLoading) return;
if (/nsec1[0-9a-zA-Z]+/.test(text())) {
window.alert('投稿に秘密鍵(nsec)を含めることはできません。');
window.alert(i18n()('posting.forbiddenToIncludeNsec'));
return;
}
@@ -337,6 +339,7 @@ const NotePostForm: Component<NotePostFormProps> = (props) => {
<div class="p-1">
<Show when={props.replyTo != null}>
<div>
{i18n()('posting.replyToPre')}
<For each={notifyPubkeys()}>
{(pubkey, index) => (
<>
@@ -345,7 +348,7 @@ const NotePostForm: Component<NotePostFormProps> = (props) => {
</>
)}
</For>
{i18n()('posting.replyToPost')}
</div>
</Show>
<form class="flex flex-col gap-1" onSubmit={handleSubmit}>
@@ -353,7 +356,7 @@ const NotePostForm: Component<NotePostFormProps> = (props) => {
<input
type="text"
class="rounded"
placeholder="警告の理由"
placeholder={i18n()('posting.contentWarningReason')}
maxLength={32}
onInput={(ev) => setContentWarningReason(ev.currentTarget.value)}
value={contentWarningReason()}
@@ -400,8 +403,8 @@ const NotePostForm: Component<NotePostFormProps> = (props) => {
'w-7': mode() === 'reply',
}}
type="button"
aria-label="コンテンツ警告を設定"
title="コンテンツ警告を設定"
aria-label={i18n()('posting.contentWarning')}
title={i18n()('posting.contentWarning')}
onClick={() => setContentWarning((e) => !e)}
>
<span>CW</span>
@@ -417,8 +420,8 @@ const NotePostForm: Component<NotePostFormProps> = (props) => {
'w-7': mode() === 'reply',
}}
type="button"
title="画像を投稿"
aria-label="画像を投稿"
title={i18n()('posting.uploadImage')}
aria-label={i18n()('posting.uploadImage')}
disabled={fileUploadDisabled()}
onClick={() => fileInputRef?.click()}
>
@@ -435,8 +438,8 @@ const NotePostForm: Component<NotePostFormProps> = (props) => {
'w-7': mode() === 'reply',
}}
type="submit"
aria-label="投稿"
title="投稿"
aria-label={i18n()('posting.submit')}
title={i18n()('posting.submit')}
disabled={submitDisabled()}
>
<PaperAirplane />

View File

@@ -5,6 +5,7 @@ import EventDisplay from '@/components/event/EventDisplay';
import { type EventDisplayProps } from '@/components/event/EventDisplay';
import EventLink from '@/components/EventLink';
import useConfig from '@/core/useConfig';
import { useTranslation } from '@/i18n/useTranslation';
import useEvent from '@/nostr/useEvent';
import ensureNonNull from '@/utils/ensureNonNull';
@@ -13,6 +14,7 @@ type EventDisplayByIdProps = Omit<EventDisplayProps, 'event'> & {
};
const EventDisplayById: Component<EventDisplayByIdProps> = (props) => {
const i18n = useTranslation();
const [localProps, restProps] = splitProps(props, ['eventId']);
const { shouldMuteEvent } = useConfig();
@@ -31,7 +33,7 @@ const EventDisplayById: Component<EventDisplayByIdProps> = (props) => {
<Switch
fallback={
<span>
稿
{i18n()('post.failedToFetchEvent')}
{props.eventId}
</span>
}
@@ -43,8 +45,7 @@ const EventDisplayById: Component<EventDisplayByIdProps> = (props) => {
<Match when={eventQuery.isLoading && localProps.eventId} keyed>
{(id) => (
<div class="truncate">
{'読み込み中 '}
<EventLink eventId={id} />
{i18n()('general.loading')} <EventLink eventId={id} />
</div>
)}
</Match>

View File

@@ -68,7 +68,11 @@ const ReactionDisplay: Component<ReactionDisplayProps> = (props) => {
<div class="notification-event py-1">
<Show
when={reactedEvent()}
fallback={<div class="truncate"> {eventId()}</div>}
fallback={
<div class="truncate">
{i18n()('general.loading')} {eventId()}
</div>
}
keyed
>
{(ev) => <TextNoteDisplay event={ev} />}

View File

@@ -131,21 +131,14 @@ const ProfileDisplay: Component<ProfileDisplayProps> = (props) => {
const latest = await fetchLatestFollowings({ pubkey: p });
const msg = stripMargin`
フォローリストが空のようです。初めてのフォローであれば問題ありません。
そうでなければ、リレーとの接続がうまくいっていない可能性があります。ページを再読み込みしてリレーと再接続してください。
また、他のクライアントと同じリレーを設定できているどうかご確認ください。
続行しますか?
`;
if ((latest.data() == null || latest.followingPubkeys().length === 0) && !window.confirm(msg))
if (
(latest.data() == null || latest.followingPubkeys().length === 0) &&
!window.confirm(i18n()('profile.confirmUpdateEvenIfEmpty'))
)
return;
if ((latest?.data()?.created_at ?? 0) < (myFollowingQuery.data?.created_at ?? 0)) {
window.alert(
'最新のフォローリストを取得できませんでした。リレーの接続状況が悪い可能性があります。',
);
window.alert(i18n()('profile.failedToFetchLatestFollowList'));
return;
}
@@ -171,7 +164,7 @@ const ProfileDisplay: Component<ProfileDisplayProps> = (props) => {
});
} catch (err) {
console.error('failed to update contact list', err);
window.alert('フォローリストの更新に失敗しました。');
window.alert(i18n()('profile.failedToUpdateFollowList'));
} finally {
setUpdatingContacts(false);
}
@@ -184,7 +177,7 @@ const ProfileDisplay: Component<ProfileDisplayProps> = (props) => {
};
const unfollow = () => {
if (!window.confirm('本当にフォロー解除しますか?')) return;
if (!window.confirm(i18n()('profile.confirmUnfollow'))) return;
updateContacts('unfollow', props.pubkey).catch((err) => {
console.log('failed to unfollow', err);
@@ -384,11 +377,11 @@ const ProfileDisplay: Component<ProfileDisplayProps> = (props) => {
</Show>
<div class="flex border-t px-4 py-2">
<button class="flex flex-1 flex-col items-start" onClick={() => setModal('Following')}>
<div class="text-sm"></div>
<div class="text-sm">{i18n()('profile.following')}</div>
<div class="text-xl">
<Show
when={userFollowingQuery.isFetched}
fallback={<span class="text-sm"></span>}
fallback={<span class="text-sm">{i18n()('general.loading')}</span>}
>
{userFollowingPubkeys().length}
</Show>
@@ -396,7 +389,7 @@ const ProfileDisplay: Component<ProfileDisplayProps> = (props) => {
</button>
<Show when={!config().hideCount}>
<div class="flex flex-1 flex-col items-start">
<div class="text-sm"></div>
<div class="text-sm">{i18n()('profile.followers')}</div>
<div class="text-xl">
<Show
when={showFollowers()}
@@ -405,7 +398,7 @@ const ProfileDisplay: Component<ProfileDisplayProps> = (props) => {
class="text-sm hover:text-stone-800 hover:underline"
onClick={() => setShowFollowers(true)}
>
{i18n()('profile.loadFollowers')}
</button>
}
keyed

View File

@@ -7,6 +7,7 @@ import omitBy from 'lodash/omitBy';
import BasicModal from '@/components/modal/BasicModal';
import useConfig from '@/core/useConfig';
import { useTranslation } from '@/i18n/useTranslation';
import { Profile } from '@/nostr/event/Profile';
import useCommands from '@/nostr/useCommands';
import useProfile from '@/nostr/useProfile';
@@ -29,6 +30,7 @@ const isLNURL = (s: string) => LNURLRegex.test(s);
const isInternetIdentifier = (s: string) => InternetIdentifierRegex.test(s);
const ProfileEdit: Component<ProfileEditProps> = (props) => {
const i18n = useTranslation();
const pubkey = usePubkey();
const { config } = useConfig();
@@ -56,11 +58,11 @@ const ProfileEdit: Component<ProfileEditProps> = (props) => {
const succeeded = results.filter((res) => res.status === 'fulfilled').length;
const failed = results.length - succeeded;
if (succeeded === results.length) {
window.alert('更新しました');
window.alert(i18n()('profile.edit.updateSucceeded'));
} else if (succeeded > 0) {
window.alert(`${failed}個のリレーで更新に失敗しました`);
window.alert(i18n()('profile.edit.failedToUpdatePartially', { count: failed }));
} else {
window.alert('すべてのリレーで更新に失敗しました');
window.alert(i18n()('profile.edit.failedToUpdate'));
}
invalidateProfile()
.then(() => query.refetch())
@@ -159,13 +161,13 @@ const ProfileEdit: Component<ProfileEditProps> = (props) => {
</div>
</div>
<Show when={loading()}>
<div class="px-4 pt-4">...</div>
<div class="px-4 pt-4">{i18n()('general.loading')}</div>
</Show>
<div>
<form class="flex flex-col gap-4 p-4" onSubmit={handleSubmit}>
<div class="flex flex-col items-start gap-1">
<label class="font-bold" for="picture">
{i18n()('profile.edit.icon')}
</label>
<input
class="w-full rounded-md focus:border-rose-100 focus:ring-rose-300"
@@ -181,7 +183,7 @@ const ProfileEdit: Component<ProfileEditProps> = (props) => {
</div>
<div class="flex flex-col items-start gap-1">
<label class="font-bold" for="picture">
{i18n()('profile.edit.banner')}
</label>
<input
class="w-full rounded-md focus:border-rose-100 focus:ring-rose-300"
@@ -197,7 +199,7 @@ const ProfileEdit: Component<ProfileEditProps> = (props) => {
</div>
<div class="flex flex-col items-start gap-1">
<label class="font-bold" for="name">
{i18n()('profile.edit.name')}
</label>
<div class="flex w-full items-center gap-2">
<span>@</span>
@@ -218,7 +220,7 @@ const ProfileEdit: Component<ProfileEditProps> = (props) => {
</div>
<div class="flex flex-col items-start gap-1">
<label class="font-bold" for="name">
{i18n()('profile.edit.displayName')}
</label>
<input
class="w-full rounded-md focus:border-rose-100 focus:ring-rose-300"
@@ -233,7 +235,7 @@ const ProfileEdit: Component<ProfileEditProps> = (props) => {
</div>
<div class="flex flex-col items-start gap-1">
<label class="font-bold" for="name">
{i18n()('profile.edit.about')}
</label>
<textarea
class="w-full rounded-md focus:border-rose-100 focus:ring-rose-300"
@@ -246,7 +248,7 @@ const ProfileEdit: Component<ProfileEditProps> = (props) => {
</div>
<div class="flex flex-col items-start gap-1">
<label class="font-bold" for="name">
{i18n()('profile.edit.website')}
</label>
<input
class="w-full rounded-md focus:border-rose-100 focus:ring-rose-300"
@@ -261,7 +263,7 @@ const ProfileEdit: Component<ProfileEditProps> = (props) => {
</div>
<div class="flex flex-col items-start gap-1">
<label class="font-bold" for="name">
NIP-05
{i18n()('profile.edit.nip05')}
</label>
<input
class="w-full rounded-md focus:border-rose-100 focus:ring-rose-300"
@@ -277,9 +279,9 @@ const ProfileEdit: Component<ProfileEditProps> = (props) => {
</div>
<div class="flex flex-col items-start gap-1">
<label class="font-bold" for="name">
LNURLアドレス /
{i18n()('profile.edit.lightningAddress')}
</label>
<span class="text-xs"></span>
<span class="text-xs">{i18n()('profile.edit.lightningAddressDescription')}</span>
<input
class="w-full rounded-md focus:border-rose-100 focus:ring-rose-300"
type="text"
@@ -294,7 +296,7 @@ const ProfileEdit: Component<ProfileEditProps> = (props) => {
</div>
<Show when={Object.entries(otherProperties()).length > 0}>
<div>
<span class="font-bold"></span>
<span class="font-bold">{i18n()('profile.edit.otherProperties')}</span>
<div>
<For each={Object.entries(otherProperties())}>
{([key, value]) => (
@@ -313,17 +315,17 @@ const ProfileEdit: Component<ProfileEditProps> = (props) => {
class="rounded bg-rose-300 p-2 font-bold text-white hover:bg-rose-400"
disabled={mutation.isLoading}
>
{i18n()('profile.edit.save')}
</button>
<button
type="button"
class="rounded border border-rose-300 p-2 font-bold text-rose-300 hover:border-rose-400 hover:text-rose-400"
onClick={() => props.onClose()}
>
{i18n()('profile.edit.cancel')}
</button>
</div>
<Show when={mutation.isLoading}>...</Show>
<Show when={mutation.isLoading}>{i18n()('profile.edit.updating')}</Show>
</form>
</div>
</BasicModal>

View File

@@ -1,4 +1,5 @@
import ja from '@/locales/ja';
import stripMargin from '@/utils/stripMargin';
export default {
general: {
@@ -7,9 +8,16 @@ export default {
},
posting: {
placeholder: "What's happening?",
placeholderReply: 'Post a reply',
contentWarning: 'Content warning',
contentWarningReason: 'Reason of warning',
uploadImage: 'Upload image',
submit: 'Submit',
forbiddenToIncludeNsec: 'You cannot include private key (nsec).',
failedToUploadFile: 'Failed to upload files: {{filenames}}',
replyToPre: 'Reply to',
replyToAnd: ' and ',
replyToPost: '',
},
column: {
home: 'Home',
@@ -49,6 +57,38 @@ export default {
unmute: 'Unmute',
followMyself: 'Follow myself',
unfollowMyself: 'Unfollow myself',
confirmUnfollow: 'Do you really want to unfollow?',
confirmUpdateEvenIfEmpty: stripMargin`
Your follow list appears to be empty.
There is no problem if you are trying to follow for the first time.
Otherwise, it may be caused by poor connections to relays.
You should reload this page to reconnect to relays.
You also should make sure you have configured the same relay list as the other clients.
Do you want to continue?
`,
failedToUpdateFollowList: 'Failed to update the follow list',
failedToFetchLatestFollowList:
'Failed to fetch the latest follow list. It may be disconnected from some relays.',
edit: {
icon: 'Icon',
banner: 'Banner image',
name: 'Username',
displayName: 'Display Name',
about: 'About',
website: 'Website',
nip05: 'Domain verification (NIP-05)',
lightningAddress: 'LNURL address / lightning address',
lightningAddressDescription: 'Only one side will be saved.',
otherProperties: 'Other properties',
save: 'Save',
cancel: 'Cancel',
updating: 'updating...',
updateSucceeded: 'Updated successfully',
failedToUpdatePartially: 'Failed to update on {{count}} relays',
failedToUpdate: 'Failed to update on all relays',
},
},
post: {
replyToPre: 'Replying to ',
@@ -71,6 +111,7 @@ export default {
show: 'Click to display',
reason: 'Reason',
},
failedToFetchEvent: 'Failed to fetch event',
},
notification: {
reposted: ' reposted',

View File

@@ -1,3 +1,5 @@
import stripMargin from '@/utils/stripMargin';
export default {
general: {
loading: '読み込み中',
@@ -5,9 +7,16 @@ export default {
},
posting: {
placeholder: 'いまどうしてる?',
placeholderReply: '返信を投稿',
contentWarning: 'コンテンツ警告を設定',
contentWarningReason: '警告の理由',
uploadImage: '画像を投稿',
submit: '投稿',
forbiddenToIncludeNsec: '投稿に秘密鍵(nsec)を含めることはできません。',
failedToUploadFile: 'ファイルのアップロードにしました: {{filenames}}',
replyToPre: '',
replyToAnd: ' と ',
replyToPost: 'に返信',
},
column: {
home: 'ホーム',
@@ -47,6 +56,35 @@ export default {
unmute: 'ミュート解除',
followMyself: '自分をフォロー',
unfollowMyself: '自分をフォロー解除',
confirmUnfollow: '本当にフォロー解除しますか?',
confirmUpdateEvenIfEmpty: stripMargin`
フォローリストが空のようです。初めてのフォローであれば問題ありません。
そうでなければ、リレーとの接続がうまくいっていない可能性があります。ページを再読み込みしてリレーと再接続してください。
また、他のクライアントと同じリレーを設定できているどうかご確認ください。
続行しますか?
`,
failedToUpdateFollowList: 'フォローリストの更新に失敗しました',
failedToFetchLatestFollowList:
'最新のフォローリストを取得できませんでした。リレーの接続状況が悪い可能性があります。',
edit: {
icon: 'アイコン',
banner: 'バナー',
name: 'ユーザ名',
displayName: 'ユーザ名',
about: '自己紹介',
website: 'ウェブサイト',
nip05: 'ドメイン認証NIP-05',
lightningAddress: 'LNURLアドレス / ライトニングアドレス',
lightningAddressDescription: 'どちらか片方のみが保存されます。',
otherProperties: 'その他の項目',
save: '更新',
cancel: 'キャンセル',
updating: '更新中...',
updateSucceeded: '更新しました',
failedToUpdatePartially: '{{count}}個のリレーで更新に失敗しました',
failedToUpdate: 'すべてのリレーで更新に失敗しました',
},
},
post: {
replyToPre: '',
@@ -69,6 +107,7 @@ export default {
show: '表示するにはクリック',
reason: '理由',
},
failedToFetchEvent: '取得に失敗しました',
},
notification: {
reposted: 'がリポスト',