mirror of
https://github.com/aljazceru/rabbit.git
synced 2025-12-17 14:04:21 +01:00
feat: i18n
This commit is contained in:
68
package-lock.json
generated
68
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
21
src/App.tsx
21
src/App.tsx
@@ -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,13 +30,15 @@ const App: Component = () => {
|
||||
});
|
||||
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<Routes>
|
||||
<Route path="/hello" element={<Hello />} />
|
||||
<Route path="/" element={<Home />} />
|
||||
<Route path="/*" element={<NotFound />} />
|
||||
</Routes>
|
||||
</QueryClientProvider>
|
||||
<I18NextProvider i18next={i18next}>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<Routes>
|
||||
<Route path="/hello" element={<Hello />} />
|
||||
<Route path="/" element={<Home />} />
|
||||
<Route path="/*" element={<NotFound />} />
|
||||
</Routes>
|
||||
</QueryClientProvider>
|
||||
</I18NextProvider>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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()}>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,61 +126,62 @@ 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: {
|
||||
id: Config['dateFormat'];
|
||||
name: string;
|
||||
example: string;
|
||||
}[] = [
|
||||
{
|
||||
id: 'relative',
|
||||
name: '相対表記',
|
||||
example: '7秒前',
|
||||
},
|
||||
{
|
||||
id: 'absolute-short',
|
||||
name: '絶対表記 (短形式)',
|
||||
example: '昨日 23:55',
|
||||
},
|
||||
{
|
||||
id: 'absolute-long',
|
||||
name: '絶対表記 (長形式)',
|
||||
example: '2020/11/8 21:02:53',
|
||||
},
|
||||
];
|
||||
|
||||
const DateFormatConfig = () => {
|
||||
const i18n = useTranslation();
|
||||
const { config, setConfig } = useConfig();
|
||||
|
||||
const dateFormats: {
|
||||
id: Config['dateFormat'];
|
||||
name: string;
|
||||
example: string;
|
||||
}[] = [
|
||||
{
|
||||
id: 'relative',
|
||||
name: i18n()('config.display.relativeTimeNotation'),
|
||||
example: i18n()('config.display.relativeTimeNotationExample'),
|
||||
},
|
||||
{
|
||||
id: 'absolute-short',
|
||||
name: i18n()('config.display.absoluteTimeNotationShort'),
|
||||
example: i18n()('config.display.absoluteTimeNotationShortExample'),
|
||||
},
|
||||
{
|
||||
id: 'absolute-long',
|
||||
name: i18n()('config.display.absoluteTimeNotationLong'),
|
||||
example: i18n()('config.display.absoluteTimeNotationLongExample'),
|
||||
},
|
||||
];
|
||||
|
||||
const updateDateFormat = (dateFormat: Config['dateFormat']) => {
|
||||
setConfig((current) => ({ ...current, dateFormat }));
|
||||
};
|
||||
|
||||
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) => (
|
||||
|
||||
@@ -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
22
src/i18n/i18n.ts
Normal 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;
|
||||
39
src/i18n/useTranslation.tsx
Normal file
39
src/i18n/useTranslation.tsx
Normal 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
125
src/locales/en.ts
Normal 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
124
src/locales/ja.ts
Normal 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 拡張機能でログイン',
|
||||
},
|
||||
};
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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
12
src/types/i18next.d.ts
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
import 'i18next';
|
||||
|
||||
import ja from '@/locales/ja';
|
||||
|
||||
declare module 'i18next' {
|
||||
interface CustomTypeOptions {
|
||||
defaultNS: 'translation';
|
||||
resources: {
|
||||
translation: typeof ja;
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@
|
||||
"target": "ESNext",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
|
||||
Reference in New Issue
Block a user