feat: i18n

This commit is contained in:
Shusui MOYATANI
2023-06-17 23:42:54 +09:00
parent 1a4c9dc49b
commit b3a0bfe772
26 changed files with 555 additions and 131 deletions

68
package-lock.json generated
View File

@@ -20,6 +20,8 @@
"@types/lodash": "^4.14.195",
"emoji-mart": "^5.5.2",
"heroicons": "^2.0.18",
"i18next": "^23.1.0",
"i18next-browser-languagedetector": "^7.0.2",
"lodash": "^4.17.21",
"nostr-tools": "^1.11.2",
"solid-js": "^1.7.5",
@@ -507,10 +509,9 @@
}
},
"node_modules/@babel/runtime": {
"version": "7.20.7",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.20.7.tgz",
"integrity": "sha512-UF0tvkUtxwAgZ5W/KrkHf0Rn0fdnLDU9ScxBrEVNUprE/MzirjK4MJUX1/BVDv00Sv8cljtukVK1aky++X1SjQ==",
"dev": true,
"version": "7.22.5",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.22.5.tgz",
"integrity": "sha512-ecjvYlnAaZ/KVneE/OdKYBYfgXV3Ptu6zQWmgEF7vwKhQnvVS6bjMD2XYgj+SNvQ1GfK/pjgokfPkC/2CO8CuA==",
"dependencies": {
"regenerator-runtime": "^0.13.11"
},
@@ -3999,6 +4000,36 @@
"url": "https://github.com/sponsors/typicode"
}
},
"node_modules/i18next": {
"version": "23.1.0",
"resolved": "https://registry.npmjs.org/i18next/-/i18next-23.1.0.tgz",
"integrity": "sha512-CObNPofJpw7zGVGYLd58mtMZUF+NZQl9czYMihbJkStjX+Nlu9kC3PHiC6uE1niP3qxP/3ocLXIBc2zqbAb1dg==",
"funding": [
{
"type": "individual",
"url": "https://locize.com"
},
{
"type": "individual",
"url": "https://locize.com/i18next.html"
},
{
"type": "individual",
"url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project"
}
],
"dependencies": {
"@babel/runtime": "^7.22.5"
}
},
"node_modules/i18next-browser-languagedetector": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-7.0.2.tgz",
"integrity": "sha512-5ViaK+gikxfqZ9M3jJ7gJkUzzu/p3HwiqfLoL1bdiL7CUb0IylcTyVLdPaTU3pH5VFWFCiGFuJDg3VkLUikWgg==",
"dependencies": {
"@babel/runtime": "^7.19.4"
}
},
"node_modules/ignore": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz",
@@ -5920,8 +5951,7 @@
"node_modules/regenerator-runtime": {
"version": "0.13.11",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz",
"integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==",
"dev": true
"integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg=="
},
"node_modules/regexp.prototype.flags": {
"version": "1.4.3",
@@ -7726,10 +7756,9 @@
}
},
"@babel/runtime": {
"version": "7.20.7",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.20.7.tgz",
"integrity": "sha512-UF0tvkUtxwAgZ5W/KrkHf0Rn0fdnLDU9ScxBrEVNUprE/MzirjK4MJUX1/BVDv00Sv8cljtukVK1aky++X1SjQ==",
"dev": true,
"version": "7.22.5",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.22.5.tgz",
"integrity": "sha512-ecjvYlnAaZ/KVneE/OdKYBYfgXV3Ptu6zQWmgEF7vwKhQnvVS6bjMD2XYgj+SNvQ1GfK/pjgokfPkC/2CO8CuA==",
"requires": {
"regenerator-runtime": "^0.13.11"
}
@@ -10140,6 +10169,22 @@
"integrity": "sha512-+dQSyqPh4x1hlO1swXBiNb2HzTDN1I2IGLQx1GrBuiqFJfoMrnZWwVmatvSiO+Iz8fBUnf+lekwNo4c2LlXItg==",
"dev": true
},
"i18next": {
"version": "23.1.0",
"resolved": "https://registry.npmjs.org/i18next/-/i18next-23.1.0.tgz",
"integrity": "sha512-CObNPofJpw7zGVGYLd58mtMZUF+NZQl9czYMihbJkStjX+Nlu9kC3PHiC6uE1niP3qxP/3ocLXIBc2zqbAb1dg==",
"requires": {
"@babel/runtime": "^7.22.5"
}
},
"i18next-browser-languagedetector": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-7.0.2.tgz",
"integrity": "sha512-5ViaK+gikxfqZ9M3jJ7gJkUzzu/p3HwiqfLoL1bdiL7CUb0IylcTyVLdPaTU3pH5VFWFCiGFuJDg3VkLUikWgg==",
"requires": {
"@babel/runtime": "^7.19.4"
}
},
"ignore": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz",
@@ -11499,8 +11544,7 @@
"regenerator-runtime": {
"version": "0.13.11",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz",
"integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==",
"dev": true
"integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg=="
},
"regexp.prototype.flags": {
"version": "1.4.3",

View File

@@ -57,6 +57,8 @@
"@types/lodash": "^4.14.195",
"emoji-mart": "^5.5.2",
"heroicons": "^2.0.18",
"i18next": "^23.1.0",
"i18next-browser-languagedetector": "^7.0.2",
"lodash": "^4.17.21",
"nostr-tools": "^1.11.2",
"solid-js": "^1.7.5",

View File

@@ -5,12 +5,17 @@ import { persistQueryClient } from '@tanstack/query-persist-client-core';
import { createSyncStoragePersister } from '@tanstack/query-sync-storage-persister';
import { QueryClient, QueryClientProvider } from '@tanstack/solid-query';
import i18nextInstance from '@/i18n/i18n';
import { I18NextProvider } from '@/i18n/useTranslation';
const Home = lazy(() => import('@/pages/Home'));
const Hello = lazy(() => import('@/pages/Hello'));
const NotFound = lazy(() => import('@/pages/NotFound'));
const queryClient = new QueryClient({});
const i18next = i18nextInstance();
const localStoragePersister = createSyncStoragePersister({
storage: window.localStorage,
});
@@ -25,6 +30,7 @@ const App: Component = () => {
});
return (
<I18NextProvider i18next={i18next}>
<QueryClientProvider client={queryClient}>
<Routes>
<Route path="/hello" element={<Hello />} />
@@ -32,6 +38,7 @@ const App: Component = () => {
<Route path="/*" element={<NotFound />} />
</Routes>
</QueryClientProvider>
</I18NextProvider>
);
};

View File

@@ -8,6 +8,7 @@ import ColumnSettings from '@/components/column/ColumnSettings';
import Bookmark from '@/components/timeline/Bookmark';
import { BookmarkColumnType } from '@/core/column';
import useConfig from '@/core/useConfig';
import { useTranslation } from '@/i18n/useTranslation';
import useDecrypt from '@/nostr/useDecrypt';
import useParameterizedReplaceableEvent from '@/nostr/useParameterizedReplaceableEvent';
@@ -18,6 +19,7 @@ type BookmarkColumnDisplayProps = {
};
const BookmarkColumn: Component<BookmarkColumnDisplayProps> = (props) => {
const i18n = useTranslation();
const { removeColumn } = useConfig();
const { event } = useParameterizedReplaceableEvent(() => ({
@@ -30,7 +32,7 @@ const BookmarkColumn: Component<BookmarkColumnDisplayProps> = (props) => {
<Column
header={
<BasicColumnHeader
name={props.column.name ?? 'ブックマーク'}
name={props.column.name ?? i18n()('column.bookmark')}
icon={<BookmarkIcon />}
settings={() => <ColumnSettings column={props.column} columnIndex={props.columnIndex} />}
onClose={() => removeColumn(props.column.id)}

View File

@@ -11,7 +11,7 @@ import Timeline from '@/components/timeline/Timeline';
import { ChannelColumnType, FollowingColumnType } from '@/core/column';
import { applyContentFilter } from '@/core/contentFilter';
import useConfig from '@/core/useConfig';
import useFollowings from '@/nostr/useFollowings';
import { useTranslation } from '@/i18n/useTranslation';
import useSubscription from '@/nostr/useSubscription';
import epoch from '@/utils/epoch';
@@ -22,6 +22,7 @@ export type ChannelColumnProps = {
};
const ChannelColumn: Component<ChannelColumnProps> = (props) => {
const i18n = useTranslation();
const { config, removeColumn } = useConfig();
const { events } = useSubscription(() => ({
@@ -44,7 +45,7 @@ const ChannelColumn: Component<ChannelColumnProps> = (props) => {
<Column
header={
<BasicColumnHeader
name={props.column.name ?? 'チャンネル'}
name={props.column.name ?? i18n()('column.channel')}
icon={<ChatBubbleLeftRight />}
settings={() => <ColumnSettings column={props.column} columnIndex={props.columnIndex} />}
onClose={() => removeColumn(props.column.id)}

View File

@@ -7,6 +7,7 @@ import Trash from 'heroicons/24/outline/trash.svg';
import { ColumnType } from '@/core/column';
import useConfig from '@/core/useConfig';
import { useRequestCommand } from '@/hooks/useCommandBus';
import { useTranslation } from '@/i18n/useTranslation';
type ColumnSettingsProps = {
column: ColumnType;
@@ -28,6 +29,7 @@ const ColumnSettingsSection: Component<ColumnSettingsSectionProps> = (props) =>
};
const ColumnSettings: Component<ColumnSettingsProps> = (props) => {
const i18n = useTranslation();
const { saveColumn, removeColumn, moveColumn } = useConfig();
const request = useRequestCommand();
@@ -42,41 +44,49 @@ const ColumnSettings: Component<ColumnSettingsProps> = (props) => {
return (
<div class="flex flex-col border-t">
<ColumnSettingsSection title="カラム幅">
<div class="flex h-9 gap-2">
<ColumnSettingsSection title={i18n()('column.config.columnWidth')}>
<div class="scrollbar flex h-9 gap-2 overflow-x-scroll">
<button
class="rounded-md border px-4 hover:bg-stone-100"
onClick={() => setColumnWidth('widest')}
>
{i18n()('column.config.widest')}
</button>
<button
class="rounded-md border px-4 hover:bg-stone-100"
onClick={() => setColumnWidth('wide')}
>
{i18n()('column.config.wide')}
</button>
<button
class="rounded-md border px-4 hover:bg-stone-100"
onClick={() => setColumnWidth('medium')}
>
{i18n()('column.config.medium')}
</button>
<button
class="rounded-md border px-4 hover:bg-stone-100"
onClick={() => setColumnWidth('narrow')}
>
{i18n()('column.config.narrow')}
</button>
</div>
</ColumnSettingsSection>
<div class="flex h-10 items-center gap-2">
<button class="py-4 pl-2" title="左に移動" onClick={() => move(props.columnIndex - 1)}>
<button
class="py-4 pl-2"
title={i18n()('column.config.moveLeft')}
onClick={() => move(props.columnIndex - 1)}
>
<span class="inline-block h-4 w-4">
<ChevronLeft />
</span>
</button>
<button class="py-4 pr-2" title="右に移動" onClick={() => move(props.columnIndex + 1)}>
<button
class="py-4 pr-2"
title={i18n()('column.config.moveRight')}
onClick={() => move(props.columnIndex + 1)}
>
<span class="inline-block h-4 w-4">
<ChevronRight />
</span>
@@ -84,10 +94,10 @@ const ColumnSettings: Component<ColumnSettingsProps> = (props) => {
<div class="flex-1" />
<button
class="px-2 py-4 text-rose-500 hover:text-rose-600"
title="削除"
title={i18n()('column.config.removeColumn')}
onClick={() => removeColumn(props.column.id)}
>
<span class="inline-block h-4 w-4" aria-label="削除">
<span class="inline-block h-4 w-4" aria-label={i18n()('column.config.removeColumn')}>
<Trash />
</span>
</button>

View File

@@ -10,6 +10,7 @@ import Timeline from '@/components/timeline/Timeline';
import { FollowingColumnType } from '@/core/column';
import { applyContentFilter } from '@/core/contentFilter';
import useConfig from '@/core/useConfig';
import { useTranslation } from '@/i18n/useTranslation';
import useFollowings from '@/nostr/useFollowings';
import useSubscription from '@/nostr/useSubscription';
import epoch from '@/utils/epoch';
@@ -21,6 +22,7 @@ type FollowingColumnDisplayProps = {
};
const FollowingColumn: Component<FollowingColumnDisplayProps> = (props) => {
const i18n = useTranslation();
const { config, removeColumn } = useConfig();
const { followingPubkeys } = useFollowings(() => ({ pubkey: props.column.pubkey }));
@@ -57,7 +59,7 @@ const FollowingColumn: Component<FollowingColumnDisplayProps> = (props) => {
<Column
header={
<BasicColumnHeader
name={props.column.name ?? 'ホーム'}
name={props.column.name ?? i18n()('column.home')}
icon={<Home />}
settings={() => <ColumnSettings column={props.column} columnIndex={props.columnIndex} />}
onClose={() => removeColumn(props.column.id)}

View File

@@ -9,6 +9,7 @@ import Notification from '@/components/timeline/Notification';
import { NotificationColumnType } from '@/core/column';
import { applyContentFilter } from '@/core/contentFilter';
import useConfig from '@/core/useConfig';
import { useTranslation } from '@/i18n/useTranslation';
import useSubscription from '@/nostr/useSubscription';
type NotificationColumnDisplayProps = {
@@ -18,6 +19,7 @@ type NotificationColumnDisplayProps = {
};
const NotificationColumn: Component<NotificationColumnDisplayProps> = (props) => {
const i18n = useTranslation();
const { config, removeColumn } = useConfig();
const { events: notifications } = useSubscription(() => ({
@@ -39,7 +41,7 @@ const NotificationColumn: Component<NotificationColumnDisplayProps> = (props) =>
<Column
header={
<BasicColumnHeader
name={props.column.name ?? '通知'}
name={props.column.name ?? i18n()('column.notification')}
icon={<Bell />}
settings={() => <ColumnSettings column={props.column} columnIndex={props.columnIndex} />}
onClose={() => removeColumn(props.column.id)}

View File

@@ -9,6 +9,7 @@ import Timeline from '@/components/timeline/Timeline';
import { PostsColumnType } from '@/core/column';
import { applyContentFilter } from '@/core/contentFilter';
import useConfig from '@/core/useConfig';
import { useTranslation } from '@/i18n/useTranslation';
import useSubscription from '@/nostr/useSubscription';
type PostsColumnDisplayProps = {
@@ -18,6 +19,7 @@ type PostsColumnDisplayProps = {
};
const PostsColumn: Component<PostsColumnDisplayProps> = (props) => {
const i18n = useTranslation();
const { config, removeColumn } = useConfig();
const { events } = useSubscription(() => ({
@@ -39,7 +41,7 @@ const PostsColumn: Component<PostsColumnDisplayProps> = (props) => {
<Column
header={
<BasicColumnHeader
name={props.column.name ?? '投稿'}
name={props.column.name ?? i18n()('column.posts')}
icon={<User />}
settings={() => <ColumnSettings column={props.column} columnIndex={props.columnIndex} />}
onClose={() => removeColumn(props.column.id)}

View File

@@ -9,6 +9,7 @@ import Notification from '@/components/timeline/Notification';
import { ReactionsColumnType } from '@/core/column';
import { applyContentFilter } from '@/core/contentFilter';
import useConfig from '@/core/useConfig';
import { useTranslation } from '@/i18n/useTranslation';
import useSubscription from '@/nostr/useSubscription';
type ReactionsColumnDisplayProps = {
@@ -18,6 +19,7 @@ type ReactionsColumnDisplayProps = {
};
const ReactionsColumn: Component<ReactionsColumnDisplayProps> = (props) => {
const i18n = useTranslation();
const { config, removeColumn } = useConfig();
const { events: reactions } = useSubscription(() => ({
@@ -39,7 +41,7 @@ const ReactionsColumn: Component<ReactionsColumnDisplayProps> = (props) => {
<Column
header={
<BasicColumnHeader
name={props.column.name ?? 'リアクション'}
name={props.column.name ?? i18n()('column.reactions')}
icon={<Heart />}
settings={() => <ColumnSettings column={props.column} columnIndex={props.columnIndex} />}
onClose={() => removeColumn(props.column.id)}

View File

@@ -9,6 +9,7 @@ import Timeline from '@/components/timeline/Timeline';
import { RelaysColumnType } from '@/core/column';
import { applyContentFilter } from '@/core/contentFilter';
import useConfig from '@/core/useConfig';
import { useTranslation } from '@/i18n/useTranslation';
import useSubscription from '@/nostr/useSubscription';
import epoch from '@/utils/epoch';
@@ -19,6 +20,7 @@ type RelaysColumnDisplayProps = {
};
const RelaysColumn: Component<RelaysColumnDisplayProps> = (props) => {
const i18n = useTranslation();
const { removeColumn } = useConfig();
const { events } = useSubscription(() => ({
@@ -40,7 +42,7 @@ const RelaysColumn: Component<RelaysColumnDisplayProps> = (props) => {
<Column
header={
<BasicColumnHeader
name={props.column.name ?? 'リレー'}
name={props.column.name ?? i18n()('column.relay')}
icon={<GlobeAlt />}
settings={() => <ColumnSettings column={props.column} columnIndex={props.columnIndex} />}
onClose={() => removeColumn(props.column.id)}

View File

@@ -7,6 +7,7 @@ import TextNoteDisplay from '@/components/event/textNote/TextNoteDisplay';
import UserDisplayName from '@/components/UserDisplayName';
import useConfig from '@/core/useConfig';
import useModalState from '@/hooks/useModalState';
import { useTranslation } from '@/i18n/useTranslation';
import { genericEvent } from '@/nostr/event';
import useEvent from '@/nostr/useEvent';
import useProfile from '@/nostr/useProfile';
@@ -17,6 +18,7 @@ type ReactionProps = {
};
const Reaction: Component<ReactionProps> = (props) => {
const i18n = useTranslation();
const { shouldMuteEvent } = useConfig();
const { showProfile } = useModalState();
const event = () => genericEvent(props.event);
@@ -65,7 +67,7 @@ const Reaction: Component<ReactionProps> = (props) => {
>
<UserDisplayName pubkey={props.event.pubkey} />
</button>
{' がリアクション'}
{i18n()('notification.reacted')}
</div>
</div>
</div>

View File

@@ -9,6 +9,7 @@ import EventDisplayById from '@/components/event/EventDisplayById';
import UserDisplayName from '@/components/UserDisplayName';
import useFormatDate from '@/hooks/useFormatDate';
import useModalState from '@/hooks/useModalState';
import { useTranslation } from '@/i18n/useTranslation';
import { genericEvent } from '@/nostr/event';
export type RepostProps = {
@@ -16,6 +17,7 @@ export type RepostProps = {
};
const Repost: Component<RepostProps> = (props) => {
const i18n = useTranslation();
const { showProfile } = useModalState();
const formatDate = useFormatDate();
const event = createMemo(() => genericEvent(props.event));
@@ -34,7 +36,7 @@ const Repost: Component<RepostProps> = (props) => {
>
<UserDisplayName pubkey={props.event.pubkey} />
</button>
{' がリポスト'}
{i18n()('notification.reposted')}
</div>
<div>{formatDate(event().createdAtAsDate())}</div>
</div>

View File

@@ -35,8 +35,7 @@ const ZapReceipt: Component<ZapReceiptProps> = (props) => {
return (
<Show when={!shouldMuteEvent(props.event)}>
<UserNameDisplay pubkey={zapRequest().pubkey} />
{/* <UserNameDisplay pubkey={zapRequest().pubkey} /> */}
<pre>{JSON.stringify(props.event, null, 2)}</pre>
</Show>
);

View File

@@ -32,6 +32,7 @@ import Post from '@/components/Post';
import { useTimelineContext } from '@/components/timeline/TimelineContext';
import useConfig from '@/core/useConfig';
import useModalState from '@/hooks/useModalState';
import { useTranslation } from '@/i18n/useTranslation';
import { textNote } from '@/nostr/event';
import useCommands from '@/nostr/useCommands';
import usePubkey from '@/nostr/usePubkey';
@@ -97,6 +98,7 @@ const EmojiReactions: Component<EmojiReactionsProps> = (props) => {
};
const TextNoteDisplay: Component<TextNoteDisplayProps> = (props) => {
const i18n = useTranslation();
const { config } = useConfig();
const pubkey = usePubkey();
const { showProfile } = useModalState();
@@ -180,11 +182,11 @@ const TextNoteDisplay: Component<TextNoteDisplayProps> = (props) => {
const succeeded = results.filter((res) => res.status === 'fulfilled').length;
const failed = results.length - succeeded;
if (succeeded === results.length) {
window.alert('削除しました(画面の反映にはリロード)');
window.alert(i18n()('post.deletedSuccessfully'));
} else if (succeeded > 0) {
window.alert(`${failed}個のリレーで削除に失敗しました`);
window.alert(i18n()('post.failedToDeletePartially', { count: failed }));
} else {
window.alert('すべてのリレーで削除に失敗しました');
window.alert(i18n()('post.failedToDelete'));
}
},
onError: (err) => {
@@ -194,37 +196,37 @@ const TextNoteDisplay: Component<TextNoteDisplayProps> = (props) => {
const menu: MenuItem[] = [
{
content: () => 'IDをコピー',
content: () => i18n()('post.copyEventId'),
onSelect: () => {
navigator.clipboard.writeText(noteEncode(props.event.id)).catch((err) => window.alert(err));
},
},
{
content: () => 'JSONを確認',
content: () => i18n()('post.showJSON'),
onSelect: () => {
setModal('EventDebugModal');
},
},
{
content: () => 'リポスト一覧',
content: () => i18n()('post.showReposts'),
onSelect: () => {
setModal('Reposts');
},
},
{
content: () => 'リアクション一覧',
content: () => i18n()('post.showReactions'),
onSelect: () => {
setModal('Reactions');
},
},
{
when: () => event().pubkey === pubkey(),
content: () => <span class="text-red-500"></span>,
content: () => <span class="text-red-500">{i18n()('post.deletePost')}</span>,
onSelect: () => {
const p = pubkey();
if (p == null) return;
if (!window.confirm('本当に削除しますか?')) return;
if (!window.confirm(i18n()('post.confirmDelete'))) return;
deleteMutation.mutate({
relayUrls: config().relayUrls,
pubkey: p,
@@ -326,6 +328,7 @@ const TextNoteDisplay: Component<TextNoteDisplayProps> = (props) => {
</Show>
<Show when={event().taggedPubkeys().length > 0}>
<div class="text-xs">
{i18n()('post.replyToPre')}
<For each={event().taggedPubkeys()}>
{(replyToPubkey: string) => (
<button
@@ -339,7 +342,7 @@ const TextNoteDisplay: Component<TextNoteDisplayProps> = (props) => {
</button>
)}
</For>
{'への返信'}
{i18n()('post.replyToPost')}
</div>
</Show>
<ContentWarningDisplay contentWarning={event().contentWarning()}>

View File

@@ -20,6 +20,7 @@ import {
} from '@/core/column';
import useConfig from '@/core/useConfig';
import { useRequestCommand } from '@/hooks/useCommandBus';
import { useTranslation } from '@/i18n/useTranslation';
import usePubkey from '@/nostr/usePubkey';
import ensureNonNull from '@/utils/ensureNonNull';
@@ -28,6 +29,7 @@ type AddColumnProps = {
};
const AddColumn: Component<AddColumnProps> = (props) => {
const i18n = useTranslation();
const pubkey = usePubkey();
const { saveColumn } = useConfig();
const request = useRequestCommand();
@@ -85,7 +87,7 @@ const AddColumn: Component<AddColumnProps> = (props) => {
<span class="inline-block h-8 w-8">
<Home />
</span>
{i18n()('column.home')}
</button>
<button
class="flex basis-1/2 flex-col items-center gap-2 py-8 sm:basis-1/4"
@@ -94,7 +96,7 @@ const AddColumn: Component<AddColumnProps> = (props) => {
<span class="inline-block h-8 w-8">
<Bell />
</span>
{i18n()('column.notification')}
</button>
<button
class="flex basis-1/2 flex-col items-center gap-2 py-8 sm:basis-1/4"
@@ -103,7 +105,7 @@ const AddColumn: Component<AddColumnProps> = (props) => {
<span class="inline-block h-8 w-8">
<GlobeAlt />
</span>
{i18n()('column.japanese')}
</button>
{/*
<button
@@ -134,7 +136,7 @@ const AddColumn: Component<AddColumnProps> = (props) => {
<span class="inline-block h-8 w-8">
<MagnifyingGlass />
</span>
{i18n()('column.search')}
</button>
<button
class="flex basis-1/2 flex-col items-center gap-2 py-8 sm:basis-1/4"
@@ -143,7 +145,7 @@ const AddColumn: Component<AddColumnProps> = (props) => {
<span class="inline-block h-8 w-8">
<User />
</span>
稿
{i18n()('column.myPosts')}
</button>
<button
class="flex basis-1/2 flex-col items-center gap-2 py-8 sm:basis-1/4"
@@ -152,7 +154,7 @@ const AddColumn: Component<AddColumnProps> = (props) => {
<span class="inline-block h-8 w-8">
<Heart />
</span>
{i18n()('column.myReactions')}
</button>
</div>
</BasicModal>

View File

@@ -12,6 +12,7 @@ import BasicModal from '@/components/modal/BasicModal';
import UserNameDisplay from '@/components/UserDisplayName';
import useConfig, { type Config } from '@/core/useConfig';
import useModalState from '@/hooks/useModalState';
import { useTranslation } from '@/i18n/useTranslation';
import usePubkey from '@/nostr/usePubkey';
import { simpleEmojiPackSchema, convertToEmojiConfig } from '@/utils/emojipack';
import ensureNonNull from '@/utils/ensureNonNull';
@@ -26,12 +27,13 @@ const HttpUrlRegex = BaseUrlRegex('https?');
const RelayUrlRegex = BaseUrlRegex('wss?');
const ProfileSection = () => {
const i18n = useTranslation();
const pubkey = usePubkey();
const { showProfile, showProfileEdit } = useModalState();
return (
<div class="py-2">
<h3 class="font-bold"></h3>
<h3 class="font-bold">{i18n()('config.profile.profile')}</h3>
<div class="flex gap-2">
<button
class="rounded border border-rose-300 px-4 py-2 font-bold text-rose-300"
@@ -41,13 +43,13 @@ const ProfileSection = () => {
})
}
>
{i18n()('config.profile.openProfile')}
</button>
<button
class="rounded border border-rose-300 px-4 py-2 font-bold text-rose-300"
onClick={() => showProfileEdit()}
>
{i18n()('config.profile.editProfile')}
</button>
</div>
</div>
@@ -55,6 +57,7 @@ const ProfileSection = () => {
};
const RelayConfig = () => {
const i18n = useTranslation();
const { config, addRelay, removeRelay } = useConfig();
const [relayUrlInput, setRelayUrlInput] = createSignal<string>('');
@@ -73,11 +76,11 @@ const RelayConfig = () => {
const relayUrls = importedRelays.map(([relayUrl]) => relayUrl).join('\n');
if (importedRelays.length === 0) {
window.alert('リレーが設定されていません');
window.alert(i18n()('config.relays.notConfigured'));
return;
}
if (!window.confirm(`これらのリレーをインポートしますか:\n${relayUrls}`)) {
if (!window.confirm(`${i18n()('config.relays.askImport')}\n\n${relayUrls}`)) {
return;
}
@@ -89,14 +92,16 @@ const RelayConfig = () => {
});
const currentCount = config().relayUrls.length;
const importedCount = currentCount - lastCount;
window.alert(`${importedCount} 個のリレーをインポートしました`);
window.alert(i18n()('config.relays.imported', { count: importedCount }));
};
return (
<>
<div class="py-2">
<h3 class="font-bold"></h3>
<p class="py-1">{config().relayUrls.length} </p>
<h3 class="font-bold">{i18n()('config.relays.relays')}</h3>
<p class="py-1">
{i18n()('config.relays.numOfRelays', { count: config().relayUrls.length })}
</p>
<ul>
<For each={config().relayUrls}>
{(relayUrl: string) => {
@@ -121,53 +126,54 @@ const RelayConfig = () => {
onChange={(ev) => setRelayUrlInput(ev.currentTarget.value)}
/>
<button type="submit" class="rounded bg-rose-300 p-2 font-bold text-white">
{i18n()('config.relays.addRelay')}
</button>
</form>
</div>
<div class="py-2">
<h3 class="pb-1 font-bold"></h3>
<h3 class="pb-1 font-bold">{i18n()('config.relays.importRelays')}</h3>
<button
type="button"
class="rounded bg-rose-300 p-2 font-bold text-white"
onClick={() => {
importFromNIP07().catch((err) => {
console.error('failed to import relays', err);
window.alert('インポートに失敗しました');
window.alert(i18n()('config.relays.failedToImport'));
});
}}
>
{i18n()('config.relays.importFromExtension')}
</button>
</div>
</>
);
};
const dateFormats: {
const DateFormatConfig = () => {
const i18n = useTranslation();
const { config, setConfig } = useConfig();
const dateFormats: {
id: Config['dateFormat'];
name: string;
example: string;
}[] = [
}[] = [
{
id: 'relative',
name: '相対表記',
example: '7秒前',
name: i18n()('config.display.relativeTimeNotation'),
example: i18n()('config.display.relativeTimeNotationExample'),
},
{
id: 'absolute-short',
name: '絶対表記 (短形式)',
example: '昨日 23:55',
name: i18n()('config.display.absoluteTimeNotationShort'),
example: i18n()('config.display.absoluteTimeNotationShortExample'),
},
{
id: 'absolute-long',
name: '絶対表記 (長形式)',
example: '2020/11/8 21:02:53',
name: i18n()('config.display.absoluteTimeNotationLong'),
example: i18n()('config.display.absoluteTimeNotationLongExample'),
},
];
const DateFormatConfig = () => {
const { config, setConfig } = useConfig();
];
const updateDateFormat = (dateFormat: Config['dateFormat']) => {
setConfig((current) => ({ ...current, dateFormat }));
@@ -175,7 +181,7 @@ const DateFormatConfig = () => {
return (
<div class="py-2">
<h3 class="font-bold"></h3>
<h3 class="font-bold">{i18n()('config.display.timeNotation')}</h3>
<div class="flex flex-col justify-evenly gap-2 sm:flex-row">
<For each={dateFormats}>
{({ id, name, example }) => (
@@ -224,6 +230,7 @@ const ToggleButton = (props: {
};
const ReactionConfig = () => {
const i18n = useTranslation();
const { config, setConfig } = useConfig();
const toggleUseEmojiReaction = () => {
@@ -242,17 +249,17 @@ const ReactionConfig = () => {
return (
<div class="py-2">
<h3 class="font-bold"></h3>
<h3 class="font-bold">{i18n()('config.display.reaction')}</h3>
<div class="flex flex-col justify-evenly gap-2">
<div class="flex w-full">
<div class="flex-1"></div>
<div class="flex-1">{i18n()('config.display.enableEmojiReaction')}</div>
<ToggleButton
value={config().useEmojiReaction}
onClick={() => toggleUseEmojiReaction()}
/>
</div>
<div class="flex w-full">
<div class="flex-1">稿</div>
<div class="flex-1">{i18n()('config.display.showEmojiReaction')}</div>
<ToggleButton
value={config().showEmojiReaction}
onClick={() => toggleShowEmojiReaction()}
@@ -264,6 +271,7 @@ const ReactionConfig = () => {
};
const EmojiConfig = () => {
const i18n = useTranslation();
const { config, saveEmoji, removeEmoji } = useConfig();
const [shortcodeInput, setShortcodeInput] = createSignal('');
@@ -279,7 +287,7 @@ const EmojiConfig = () => {
return (
<div class="py-2">
<h3 class="font-bold"></h3>
<h3 class="font-bold">{i18n()('config.customEmoji.customEmoji')}</h3>
<ul class="flex flex-col gap-1 py-2">
<For each={Object.values(config().customEmojis)}>
{({ shortcode, url }) => (
@@ -295,7 +303,7 @@ const EmojiConfig = () => {
</ul>
<form class="flex flex-col gap-2" onSubmit={handleClickSaveEmoji}>
<label class="flex flex-1 items-center gap-1">
<div class="w-9"></div>
<div class="w-9">{i18n()('config.customEmoji.shortcode')}</div>
<input
class="flex-1 rounded-md focus:border-rose-100 focus:ring-rose-300"
type="text"
@@ -308,7 +316,7 @@ const EmojiConfig = () => {
/>
</label>
<label class="flex flex-1 items-center gap-1">
<div class="w-9">URL</div>
<div class="w-9">{i18n()('config.customEmoji.url')}</div>
<input
class="flex-1 rounded-md focus:border-rose-100 focus:ring-rose-300"
type="text"
@@ -321,7 +329,7 @@ const EmojiConfig = () => {
/>
</label>
<button type="submit" class="w-24 self-end rounded bg-rose-300 p-2 font-bold text-white">
{i18n()('config.customEmoji.addEmoji')}
</button>
</form>
</div>
@@ -329,6 +337,7 @@ const EmojiConfig = () => {
};
const EmojiImport = () => {
const i18n = useTranslation();
const { saveEmojis } = useConfig();
const [jsonInput, setJSONInput] = createSignal('');
@@ -350,8 +359,8 @@ const EmojiImport = () => {
return (
<div class="py-2">
<h3 class="font-bold"></h3>
<p>URLを値とするJSONを読み込むことができます</p>
<h3 class="font-bold">{i18n()('config.customEmoji.emojiImport')}</h3>
<p>{i18n()('config.customEmoji.emojiImportDescription')}</p>
<form class="flex flex-col gap-2" onSubmit={handleClickSaveEmoji}>
<textarea
class="flex-1 rounded-md focus:border-rose-100 focus:ring-rose-300"
@@ -361,7 +370,7 @@ const EmojiImport = () => {
onChange={(ev) => setJSONInput(ev.currentTarget.value)}
/>
<button type="submit" class="w-24 self-end rounded bg-rose-300 p-2 font-bold text-white">
{i18n()('config.customEmoji.importEmoji')}
</button>
</form>
</div>
@@ -369,6 +378,7 @@ const EmojiImport = () => {
};
const MuteConfig = () => {
const i18n = useTranslation();
const { config, removeMutedPubkey, addMutedKeyword, removeMutedKeyword } = useConfig();
const [keywordInput, setKeywordInput] = createSignal('');
@@ -383,7 +393,7 @@ const MuteConfig = () => {
return (
<>
<div class="py-2">
<h3 class="font-bold"></h3>
<h3 class="font-bold">{i18n()('config.mute.mutedUsers')}</h3>
<ul class="flex flex-col">
<For each={config().mutedPubkeys}>
{(pubkey) => (
@@ -400,7 +410,7 @@ const MuteConfig = () => {
</ul>
</div>
<div class="py-2">
<h3 class="font-bold"></h3>
<h3 class="font-bold">{i18n()('config.mute.mutedKeywords')}</h3>
<ul class="flex flex-col">
<For each={config().mutedKeywords}>
{(keyword) => (
@@ -422,7 +432,7 @@ const MuteConfig = () => {
onChange={(ev) => setKeywordInput(ev.currentTarget.value)}
/>
<button type="submit" class="rounded bg-rose-300 p-2 font-bold text-white">
{i18n()('config.mute.add')}
</button>
</form>
</div>
@@ -431,6 +441,7 @@ const MuteConfig = () => {
};
const OtherConfig = () => {
const i18n = useTranslation();
const { config, setConfig } = useConfig();
const toggleKeepOpenPostForm = () => {
@@ -456,21 +467,21 @@ const OtherConfig = () => {
return (
<div class="py-2">
<h3 class="font-bold"></h3>
<h3 class="font-bold">{i18n()('config.display.others')}</h3>
<div class="flex flex-col justify-evenly gap-2">
<div class="flex w-full">
<div class="flex-1">稿</div>
<div class="flex-1">{i18n()('config.display.keepOpenPostForm')}</div>
<ToggleButton
value={config().keepOpenPostForm}
onClick={() => toggleKeepOpenPostForm()}
/>
</div>
<div class="flex w-full">
<div class="flex-1"></div>
<div class="flex-1">{i18n()('config.display.showImagesByDefault')}</div>
<ToggleButton value={config().showImage} onClick={() => toggleShowImage()} />
</div>
<div class="flex w-full">
<div class="flex-1"></div>
<div class="flex-1">{i18n()('config.display.hideNumbers')}</div>
<ToggleButton value={config().hideCount} onClick={() => toggleHideCount()} />
</div>
{/*
@@ -489,21 +500,22 @@ const OtherConfig = () => {
};
const ConfigUI = (props: ConfigProps) => {
const i18n = useTranslation();
const [menuIndex, setMenuIndex] = createSignal<number | null>(null);
const menu = [
{
name: () => 'プロフィール',
name: () => i18n()('config.profile.profile'),
icon: () => <User />,
render: () => <ProfileSection />,
},
{
name: () => 'リレー',
name: () => i18n()('config.relays.relays'),
icon: () => <ServerStack />,
render: () => <RelayConfig />,
},
{
name: () => '表示',
name: () => i18n()('config.display.display'),
icon: () => <PaintBrush />,
render: () => (
<>
@@ -514,7 +526,7 @@ const ConfigUI = (props: ConfigProps) => {
),
},
{
name: () => 'カスタム絵文字',
name: () => i18n()('config.customEmoji.customEmoji'),
icon: () => <FaceSmile />,
render: () => (
<>
@@ -524,7 +536,7 @@ const ConfigUI = (props: ConfigProps) => {
),
},
{
name: () => 'ミュート',
name: () => i18n()('config.mute.mute'),
icon: () => <EyeSlash />,
render: () => <MuteConfig />,
},
@@ -543,7 +555,7 @@ const ConfigUI = (props: ConfigProps) => {
when={getMenuItem()}
fallback={
<>
<h2 class="flex-1 text-center text-lg font-bold"></h2>
<h2 class="flex-1 text-center text-lg font-bold">{i18n()('config.config')}</h2>
<ul class="flex flex-col">
<For each={menu}>
{(menuItem, i) => (

View File

@@ -15,6 +15,7 @@ import Timeline from '@/components/timeline/Timeline';
import SafeLink from '@/components/utils/SafeLink';
import useConfig from '@/core/useConfig';
import useModalState from '@/hooks/useModalState';
import { useTranslation } from '@/i18n/useTranslation';
import useCommands from '@/nostr/useCommands';
import useFollowers from '@/nostr/useFollowers';
import useFollowings from '@/nostr/useFollowings';
@@ -42,6 +43,7 @@ const FollowersCount: Component<{ pubkey: string }> = (props) => {
};
const ProfileDisplay: Component<ProfileDisplayProps> = (props) => {
const i18n = useTranslation();
const { config, addMutedPubkey, removeMutedPubkey, isPubkeyMuted } = useConfig();
const commands = useCommands();
const myPubkey = usePubkey();
@@ -52,7 +54,7 @@ const ProfileDisplay: Component<ProfileDisplayProps> = (props) => {
const [updatingContacts, setUpdatingContacts] = createSignal(false);
const [hoverFollowButton, setHoverFollowButton] = createSignal(false);
const [showFollowers, setShowFollowers] = createSignal(false);
const [modal, setModal] = createSignal<'Following' | null>(false);
const [modal, setModal] = createSignal<'Following' | null>(null);
const closeModal = () => setModal(null);
const { profile, query: profileQuery } = useProfile(() => ({
@@ -169,13 +171,13 @@ const ProfileDisplay: Component<ProfileDisplayProps> = (props) => {
},
*/
{
content: () => 'IDをコピー',
content: () => i18n()('profile.copyPubkey'),
onSelect: () => {
navigator.clipboard.writeText(npub()).catch((err) => window.alert(err));
},
},
{
content: () => (!isMuted() ? 'ミュート' : 'ミュート解除'),
content: () => (!isMuted() ? i18n()('profile.mute') : i18n()('profile.unmute')),
onSelect: () => {
if (!isMuted()) {
addMutedPubkey(props.pubkey);
@@ -186,7 +188,8 @@ const ProfileDisplay: Component<ProfileDisplayProps> = (props) => {
},
{
when: () => props.pubkey === myPubkey(),
content: () => (!following() ? '自分をフォロー' : '自分をフォロー解除'),
content: () =>
!following() ? i18n()('profile.followMyself') : i18n()('profile.unfollowMyself'),
onSelect: () => {
if (!following()) {
follow();
@@ -243,17 +246,17 @@ const ProfileDisplay: Component<ProfileDisplayProps> = (props) => {
text-center font-bold text-primary hover:bg-primary hover:text-white sm:w-20"
onClick={() => showProfileEdit()}
>
{i18n()('profile.editProfile')}
</button>
</Match>
<Match when={updateContactsMutation.isLoading || updatingContacts()}>
<span class="rounded-full border border-primary px-4 py-2 text-primary sm:text-base">
{i18n()('profile.updating')}
</span>
</Match>
<Match when={myFollowingQuery.isLoading || myFollowingQuery.isFetching}>
<span class="rounded-full border border-primary px-4 py-2 text-primary sm:text-base">
{i18n()('profile.loading')}
</span>
</Match>
<Match when={following()}>
@@ -265,8 +268,8 @@ const ProfileDisplay: Component<ProfileDisplayProps> = (props) => {
onClick={() => unfollow()}
disabled={updateContactsMutation.isLoading}
>
<Show when={!hoverFollowButton()} fallback="フォロー解除">
<Show when={!hoverFollowButton()} fallback={i18n()('profile.unfollow')}>
{i18n()('profile.followingCurrently')}
</Show>
</button>
</Match>
@@ -277,7 +280,7 @@ const ProfileDisplay: Component<ProfileDisplayProps> = (props) => {
onClick={() => follow()}
disabled={updateContactsMutation.isLoading}
>
{i18n()('profile.follow')}
</button>
</Match>
</Switch>
@@ -292,10 +295,10 @@ const ProfileDisplay: Component<ProfileDisplayProps> = (props) => {
</div>
<Switch>
<Match when={userFollowingQuery.isLoading}>
<div class="shrink-0 text-xs"></div>
<div class="shrink-0 text-xs">{i18n()('profile.loading')}</div>
</Match>
<Match when={followed()}>
<div class="shrink-0 text-xs"></div>
<div class="shrink-0 text-xs">{i18n()('profile.followsYou')}</div>
</Match>
</Switch>
</div>

22
src/i18n/i18n.ts Normal file
View File

@@ -0,0 +1,22 @@
import i18next from 'i18next';
import LanguageDetector from 'i18next-browser-languagedetector';
import en from '@/locales/en';
import ja from '@/locales/ja';
const i18nextInstance = (): Promise<void | typeof i18next.t> =>
i18next
.use(LanguageDetector)
.init({
fallbackLng: 'en',
debug: true,
resources: {
ja: { translation: ja },
en: { translation: en satisfies typeof ja },
},
})
.catch((err) => {
console.error('failed to setup i18next', err);
});
export default i18nextInstance;

View File

@@ -0,0 +1,39 @@
import { Component, JSX, createContext, createEffect, createSignal, useContext } from 'solid-js';
import i18next from 'i18next';
type I18Next = typeof i18next.t;
export type I18NextProviderProps = {
i18next: I18Next | Promise<I18Next | void> | void;
children?: JSX.Element;
};
const I18NextContext = createContext<I18Next | Promise<I18Next | void> | void>();
export const useTranslation = () => {
const [i18nextFn, setI18nextFn] = createSignal<I18Next>(i18next.t);
const maybePromise = useContext(I18NextContext);
createEffect(() => {
if (maybePromise instanceof Promise) {
maybePromise
.then((instance) => {
if (instance != null) {
setI18nextFn(() => instance);
}
})
.catch((err) => {
console.error('failed to initialize i18next', err);
});
} else if (maybePromise != null) {
setI18nextFn(() => maybePromise);
}
});
return i18nextFn;
};
export const I18NextProvider: Component<I18NextProviderProps> = (props) => {
return <I18NextContext.Provider value={props.i18next}>{props.children}</I18NextContext.Provider>;
};

125
src/locales/en.ts Normal file
View File

@@ -0,0 +1,125 @@
import ja from '@/locales/ja';
export default {
posting: {
placeholder: "What's happening?",
contentWarning: 'Content warning',
uploadImage: 'Upload image',
submit: 'Submit',
},
column: {
home: 'Home',
notification: 'Notification',
relay: 'Relay',
japanese: 'Japanese',
posts: 'User',
reactions: 'Reactions',
channel: 'Channel',
bookmark: 'Bookmark',
search: 'Search',
myPosts: 'My posts',
myReactions: 'My reactions',
config: {
columnWidth: 'Column width',
widest: 'Widest',
wide: 'Wide',
medium: 'Medium',
narrow: 'Narrow',
moveLeft: 'Move left',
moveRight: 'Move right',
removeColumn: 'Remove',
},
},
profile: {
following: 'Following',
followers: 'Followers',
loadFollowers: 'Load',
loading: 'Loading',
updating: 'Updating',
editProfile: 'Edit',
follow: 'Follow',
unfollow: 'Unfollow',
followingCurrently: 'Following',
followsYou: 'follows you',
copyPubkey: 'Copy ID',
mute: 'Mute',
unmute: 'Unmute',
followMyself: 'Follow myself',
unfollowMyself: 'Unfollow myself',
},
post: {
replyToPre: 'Replying to ',
replyToPost: '',
copyEventId: 'Copy ID',
showJSON: 'Show JSON',
showReposts: 'Show reposts',
showReactions: 'Show reactions',
deletePost: 'Delete',
confirmDelete: 'Do you really want to delete?',
deletedSuccessfully: 'Deleted successfully (reload to reflect)',
failedToDeletePartially: 'Failed to delete on {{count}} relays',
failedToDelete: 'Failed to delete',
},
notification: {
reposted: ' reposted',
reacted: ' reacted',
},
config: {
config: 'Settings',
profile: {
profile: 'Profile',
openProfile: 'Open',
editProfile: 'Edit',
},
relays: {
relays: 'Relays',
numOfRelays_one: '{{count}} relay are configured.',
numOfRelays_other: '{{count}} relyas are configured.',
addRelay: 'Add',
importRelays: 'Import',
importFromExtension: 'Import from browser extension',
notConfigured: 'No relays are configured.',
askImport: 'Do you want to import these relays?',
failedToImport: 'Failed to import.',
imported_one: 'Imported {{count}} relay.',
imported_other: 'Imported {{count}} relyas',
},
display: {
display: 'Display',
timeNotation: 'Time notation',
relativeTimeNotation: 'Relative',
relativeTimeNotationExample: '7s',
absoluteTimeNotationShort: 'Absolute (short)',
absoluteTimeNotationShortExample: 'Yesterday 23:55',
absoluteTimeNotationLong: 'Absolute (long)',
absoluteTimeNotationLongExample: '2020/11/8 21:02:53',
reaction: 'Reaction',
enableEmojiReaction: 'Enable emoji reaction',
showEmojiReaction: 'Show emoji reactions on posts',
others: 'Others',
keepOpenPostForm: 'Remain the input field open after posting',
showImagesByDefault: 'Load images by default',
hideNumbers: 'Hide the numbers of reactions, reposts and followers',
},
customEmoji: {
customEmoji: 'Custom emojis',
shortcode: 'Name',
url: 'URL',
addEmoji: 'Add',
emojiImport: 'Emoji import',
emojiImportDescription: 'Paste a JSON where the keys are names and the values are image URLs',
importEmoji: 'Import',
},
mute: {
mute: 'Mute',
mutedUsers: 'Muted users',
mutedKeywords: 'Muted keywords',
add: 'Add',
},
},
hello: {
signerChecking: 'Checking that browser extension is installed...',
signerUnavailable: 'Please install NIP-07 browser extension.',
loginWithSigner: 'Login with NIP-07 browser extension',
},
};

124
src/locales/ja.ts Normal file
View File

@@ -0,0 +1,124 @@
export default {
posting: {
placeholder: 'いまどうしてる?',
contentWarning: 'コンテンツ警告を設定',
uploadImage: '画像を投稿',
submit: '投稿',
},
column: {
home: 'ホーム',
notification: '通知',
relay: 'リレー',
japanese: '日本語',
posts: '投稿',
reactions: 'リアクション',
channel: 'チャンネル',
bookmark: 'ブックマーク',
search: '検索',
myPosts: '自分の投稿',
myReactions: '自分のリアクション',
config: {
columnWidth: 'カラム幅',
widest: '特大',
wide: '大',
medium: '中',
narrow: '小',
moveLeft: '左に移動',
moveRight: '右に移動',
removeColumn: '削除',
},
},
profile: {
following: 'フォロー',
followers: 'フォロワー',
loadFollowers: '読み込む',
loading: '読み込み中',
updating: '更新中',
editProfile: '編集',
follow: 'フォロー',
unfollow: 'フォロー解除',
followingCurrently: 'フォロー中',
followsYou: 'フォローされています',
copyPubkey: 'IDをコピー',
mute: 'ミュート',
unmute: 'ミュート解除',
followMyself: '自分をフォロー',
unfollowMyself: '自分をフォロー解除',
},
post: {
replyToPre: '',
replyToPost: 'への返信',
copyEventId: 'IDをコピー',
showJSON: 'JSONを確認',
showReposts: 'リポスト一覧',
showReactions: 'リアクション一覧',
deletePost: '削除',
confirmDelete: '本当に削除しますか?',
deletedSuccessfully: '削除しました(画面への反映にはリロード)',
failedToDeletePartially: '{{count}}個のリレーで削除に失敗しました',
failedToDelete: 'すべてのリレーで削除に失敗しました',
},
notification: {
reposted: 'がリポスト',
reacted: 'がリアクション',
},
config: {
config: '設定',
profile: {
profile: 'プロフィール',
openProfile: '開く',
editProfile: '編集',
},
relays: {
relays: 'リレー',
numOfRelays_one: '{{count}}個のリレーが設定されています。',
numOfRelays_other: '{{count}}個のリレーが設定されています。',
addRelay: '追加',
importRelays: 'インポート',
importFromExtension: '拡張機能からインポート',
notConfigured: 'リレーが設定されていません',
askImport: 'これらのリレーをインポートしますか?',
failedToImport: 'インポートに失敗しました',
imported_one: '{{count}}個のリレーをインポートしました',
imported_other: '{{count}}個のリレーをインポートしました',
},
display: {
display: '表示',
timeNotation: '時刻の表記',
relativeTimeNotation: '相対表記',
relativeTimeNotationExample: '7秒前',
absoluteTimeNotationShort: '絶対表記 (短形式)',
absoluteTimeNotationShortExample: '昨日 23:55',
absoluteTimeNotationLong: '絶対表記 (長形式)',
absoluteTimeNotationLongExample: '2020/11/8 21:02:53',
reaction: 'リアクション',
enableEmojiReaction: '絵文字を選べるようにする',
showEmojiReaction: '投稿にリアクションされた絵文字を表示する',
others: 'その他',
keepOpenPostForm: '投稿後も投稿欄を開いたままにする',
showImagesByDefault: 'デフォルトで画像を読み込む',
hideNumbers: 'いいねやリポスト、フォロワーなどの数を隠す',
},
customEmoji: {
customEmoji: 'カスタム絵文字',
shortcode: '名前',
url: 'URL',
addEmoji: '追加',
emojiImport: '絵文字のインポート',
emojiImportDescription:
'絵文字の名前をキー、画像のURLを値とするJSONを読み込むことができます。',
importEmoji: 'インポート',
},
mute: {
mute: 'ミュート',
mutedUsers: 'ミュートしたユーザ',
mutedKeywords: 'ミュートした単語',
add: '追加',
},
},
hello: {
signerChecking: '拡張機能のインストール状況を確認中です...',
signerUnavailable: '利用にはNIP-07に対応した拡張機能が必要です。',
loginWithSigner: 'NIP-07 拡張機能でログイン',
},
};

View File

@@ -4,7 +4,7 @@ import { createQuery, useQueryClient, type CreateQueryResult } from '@tanstack/s
import { Event as NostrEvent } from 'nostr-tools';
import useConfig from '@/core/useConfig';
import { BatchedEventsTask, exec, registerTask } from '@/nostr/useBatchedEvents';
import { BatchedEventsTask, registerTask } from '@/nostr/useBatchedEvents';
import timeout from '@/utils/timeout';
export type UseRepostsProps = {

View File

@@ -3,6 +3,7 @@ import { createSignal, onMount, Switch, Match, type Component } from 'solid-js';
import { useNavigate } from '@solidjs/router';
import usePersistStatus from '@/hooks/usePersistStatus';
import { useTranslation } from '@/i18n/useTranslation';
import resolveAsset from '@/utils/resolveAsset';
type SignerStatus = 'checking' | 'available' | 'unavailable';
@@ -31,6 +32,7 @@ const useSignerStatus = () => {
};
const Hello: Component = () => {
const i18n = useTranslation();
const signerStatus = useSignerStatus();
const navigate = useNavigate();
const { persistStatus, loggedIn } = usePersistStatus();
@@ -61,10 +63,10 @@ const Hello: Component = () => {
<div class="rounded-md p-8 shadow-md">
<Switch>
<Match when={signerStatus() === 'checking'}>
<p>...</p>
<p>{i18n()('hello.signerChecking')}</p>
</Match>
<Match when={signerStatus() === 'unavailable'}>
<h2 class="font-bold">NIP-07</h2>
<h2 class="font-bold">{i18n()('hello.signerUnavailable')}</h2>
<p>
<br />
@@ -87,7 +89,7 @@ const Hello: Component = () => {
class="rounded bg-rose-400 p-4 text-lg font-bold text-white hover:shadow-md"
onClick={handleLogin}
>
NIP-07
{i18n()('hello.loginWithSigner')}
</button>
</Match>
</Switch>

12
src/types/i18next.d.ts vendored Normal file
View File

@@ -0,0 +1,12 @@
import 'i18next';
import ja from '@/locales/ja';
declare module 'i18next' {
interface CustomTypeOptions {
defaultNS: 'translation';
resources: {
translation: typeof ja;
};
}
}

View File

@@ -4,6 +4,7 @@
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "node",
"resolveJsonModule": true,
"lib": [
"dom",
"dom.iterable",