mirror of
https://github.com/aljazceru/rabbit.git
synced 2025-12-17 05:54:19 +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",
|
"@types/lodash": "^4.14.195",
|
||||||
"emoji-mart": "^5.5.2",
|
"emoji-mart": "^5.5.2",
|
||||||
"heroicons": "^2.0.18",
|
"heroicons": "^2.0.18",
|
||||||
|
"i18next": "^23.1.0",
|
||||||
|
"i18next-browser-languagedetector": "^7.0.2",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"nostr-tools": "^1.11.2",
|
"nostr-tools": "^1.11.2",
|
||||||
"solid-js": "^1.7.5",
|
"solid-js": "^1.7.5",
|
||||||
@@ -507,10 +509,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/runtime": {
|
"node_modules/@babel/runtime": {
|
||||||
"version": "7.20.7",
|
"version": "7.22.5",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.20.7.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.22.5.tgz",
|
||||||
"integrity": "sha512-UF0tvkUtxwAgZ5W/KrkHf0Rn0fdnLDU9ScxBrEVNUprE/MzirjK4MJUX1/BVDv00Sv8cljtukVK1aky++X1SjQ==",
|
"integrity": "sha512-ecjvYlnAaZ/KVneE/OdKYBYfgXV3Ptu6zQWmgEF7vwKhQnvVS6bjMD2XYgj+SNvQ1GfK/pjgokfPkC/2CO8CuA==",
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"regenerator-runtime": "^0.13.11"
|
"regenerator-runtime": "^0.13.11"
|
||||||
},
|
},
|
||||||
@@ -3999,6 +4000,36 @@
|
|||||||
"url": "https://github.com/sponsors/typicode"
|
"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": {
|
"node_modules/ignore": {
|
||||||
"version": "5.2.0",
|
"version": "5.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz",
|
||||||
@@ -5920,8 +5951,7 @@
|
|||||||
"node_modules/regenerator-runtime": {
|
"node_modules/regenerator-runtime": {
|
||||||
"version": "0.13.11",
|
"version": "0.13.11",
|
||||||
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz",
|
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz",
|
||||||
"integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==",
|
"integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"node_modules/regexp.prototype.flags": {
|
"node_modules/regexp.prototype.flags": {
|
||||||
"version": "1.4.3",
|
"version": "1.4.3",
|
||||||
@@ -7726,10 +7756,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@babel/runtime": {
|
"@babel/runtime": {
|
||||||
"version": "7.20.7",
|
"version": "7.22.5",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.20.7.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.22.5.tgz",
|
||||||
"integrity": "sha512-UF0tvkUtxwAgZ5W/KrkHf0Rn0fdnLDU9ScxBrEVNUprE/MzirjK4MJUX1/BVDv00Sv8cljtukVK1aky++X1SjQ==",
|
"integrity": "sha512-ecjvYlnAaZ/KVneE/OdKYBYfgXV3Ptu6zQWmgEF7vwKhQnvVS6bjMD2XYgj+SNvQ1GfK/pjgokfPkC/2CO8CuA==",
|
||||||
"dev": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"regenerator-runtime": "^0.13.11"
|
"regenerator-runtime": "^0.13.11"
|
||||||
}
|
}
|
||||||
@@ -10140,6 +10169,22 @@
|
|||||||
"integrity": "sha512-+dQSyqPh4x1hlO1swXBiNb2HzTDN1I2IGLQx1GrBuiqFJfoMrnZWwVmatvSiO+Iz8fBUnf+lekwNo4c2LlXItg==",
|
"integrity": "sha512-+dQSyqPh4x1hlO1swXBiNb2HzTDN1I2IGLQx1GrBuiqFJfoMrnZWwVmatvSiO+Iz8fBUnf+lekwNo4c2LlXItg==",
|
||||||
"dev": true
|
"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": {
|
"ignore": {
|
||||||
"version": "5.2.0",
|
"version": "5.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz",
|
||||||
@@ -11499,8 +11544,7 @@
|
|||||||
"regenerator-runtime": {
|
"regenerator-runtime": {
|
||||||
"version": "0.13.11",
|
"version": "0.13.11",
|
||||||
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz",
|
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz",
|
||||||
"integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==",
|
"integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"regexp.prototype.flags": {
|
"regexp.prototype.flags": {
|
||||||
"version": "1.4.3",
|
"version": "1.4.3",
|
||||||
|
|||||||
@@ -57,6 +57,8 @@
|
|||||||
"@types/lodash": "^4.14.195",
|
"@types/lodash": "^4.14.195",
|
||||||
"emoji-mart": "^5.5.2",
|
"emoji-mart": "^5.5.2",
|
||||||
"heroicons": "^2.0.18",
|
"heroicons": "^2.0.18",
|
||||||
|
"i18next": "^23.1.0",
|
||||||
|
"i18next-browser-languagedetector": "^7.0.2",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"nostr-tools": "^1.11.2",
|
"nostr-tools": "^1.11.2",
|
||||||
"solid-js": "^1.7.5",
|
"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 { createSyncStoragePersister } from '@tanstack/query-sync-storage-persister';
|
||||||
import { QueryClient, QueryClientProvider } from '@tanstack/solid-query';
|
import { QueryClient, QueryClientProvider } from '@tanstack/solid-query';
|
||||||
|
|
||||||
|
import i18nextInstance from '@/i18n/i18n';
|
||||||
|
import { I18NextProvider } from '@/i18n/useTranslation';
|
||||||
|
|
||||||
const Home = lazy(() => import('@/pages/Home'));
|
const Home = lazy(() => import('@/pages/Home'));
|
||||||
const Hello = lazy(() => import('@/pages/Hello'));
|
const Hello = lazy(() => import('@/pages/Hello'));
|
||||||
const NotFound = lazy(() => import('@/pages/NotFound'));
|
const NotFound = lazy(() => import('@/pages/NotFound'));
|
||||||
|
|
||||||
const queryClient = new QueryClient({});
|
const queryClient = new QueryClient({});
|
||||||
|
|
||||||
|
const i18next = i18nextInstance();
|
||||||
|
|
||||||
const localStoragePersister = createSyncStoragePersister({
|
const localStoragePersister = createSyncStoragePersister({
|
||||||
storage: window.localStorage,
|
storage: window.localStorage,
|
||||||
});
|
});
|
||||||
@@ -25,13 +30,15 @@ const App: Component = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<QueryClientProvider client={queryClient}>
|
<I18NextProvider i18next={i18next}>
|
||||||
<Routes>
|
<QueryClientProvider client={queryClient}>
|
||||||
<Route path="/hello" element={<Hello />} />
|
<Routes>
|
||||||
<Route path="/" element={<Home />} />
|
<Route path="/hello" element={<Hello />} />
|
||||||
<Route path="/*" element={<NotFound />} />
|
<Route path="/" element={<Home />} />
|
||||||
</Routes>
|
<Route path="/*" element={<NotFound />} />
|
||||||
</QueryClientProvider>
|
</Routes>
|
||||||
|
</QueryClientProvider>
|
||||||
|
</I18NextProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import ColumnSettings from '@/components/column/ColumnSettings';
|
|||||||
import Bookmark from '@/components/timeline/Bookmark';
|
import Bookmark from '@/components/timeline/Bookmark';
|
||||||
import { BookmarkColumnType } from '@/core/column';
|
import { BookmarkColumnType } from '@/core/column';
|
||||||
import useConfig from '@/core/useConfig';
|
import useConfig from '@/core/useConfig';
|
||||||
|
import { useTranslation } from '@/i18n/useTranslation';
|
||||||
import useDecrypt from '@/nostr/useDecrypt';
|
import useDecrypt from '@/nostr/useDecrypt';
|
||||||
import useParameterizedReplaceableEvent from '@/nostr/useParameterizedReplaceableEvent';
|
import useParameterizedReplaceableEvent from '@/nostr/useParameterizedReplaceableEvent';
|
||||||
|
|
||||||
@@ -18,6 +19,7 @@ type BookmarkColumnDisplayProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const BookmarkColumn: Component<BookmarkColumnDisplayProps> = (props) => {
|
const BookmarkColumn: Component<BookmarkColumnDisplayProps> = (props) => {
|
||||||
|
const i18n = useTranslation();
|
||||||
const { removeColumn } = useConfig();
|
const { removeColumn } = useConfig();
|
||||||
|
|
||||||
const { event } = useParameterizedReplaceableEvent(() => ({
|
const { event } = useParameterizedReplaceableEvent(() => ({
|
||||||
@@ -30,7 +32,7 @@ const BookmarkColumn: Component<BookmarkColumnDisplayProps> = (props) => {
|
|||||||
<Column
|
<Column
|
||||||
header={
|
header={
|
||||||
<BasicColumnHeader
|
<BasicColumnHeader
|
||||||
name={props.column.name ?? 'ブックマーク'}
|
name={props.column.name ?? i18n()('column.bookmark')}
|
||||||
icon={<BookmarkIcon />}
|
icon={<BookmarkIcon />}
|
||||||
settings={() => <ColumnSettings column={props.column} columnIndex={props.columnIndex} />}
|
settings={() => <ColumnSettings column={props.column} columnIndex={props.columnIndex} />}
|
||||||
onClose={() => removeColumn(props.column.id)}
|
onClose={() => removeColumn(props.column.id)}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import Timeline from '@/components/timeline/Timeline';
|
|||||||
import { ChannelColumnType, FollowingColumnType } from '@/core/column';
|
import { ChannelColumnType, FollowingColumnType } from '@/core/column';
|
||||||
import { applyContentFilter } from '@/core/contentFilter';
|
import { applyContentFilter } from '@/core/contentFilter';
|
||||||
import useConfig from '@/core/useConfig';
|
import useConfig from '@/core/useConfig';
|
||||||
import useFollowings from '@/nostr/useFollowings';
|
import { useTranslation } from '@/i18n/useTranslation';
|
||||||
import useSubscription from '@/nostr/useSubscription';
|
import useSubscription from '@/nostr/useSubscription';
|
||||||
import epoch from '@/utils/epoch';
|
import epoch from '@/utils/epoch';
|
||||||
|
|
||||||
@@ -22,6 +22,7 @@ export type ChannelColumnProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const ChannelColumn: Component<ChannelColumnProps> = (props) => {
|
const ChannelColumn: Component<ChannelColumnProps> = (props) => {
|
||||||
|
const i18n = useTranslation();
|
||||||
const { config, removeColumn } = useConfig();
|
const { config, removeColumn } = useConfig();
|
||||||
|
|
||||||
const { events } = useSubscription(() => ({
|
const { events } = useSubscription(() => ({
|
||||||
@@ -44,7 +45,7 @@ const ChannelColumn: Component<ChannelColumnProps> = (props) => {
|
|||||||
<Column
|
<Column
|
||||||
header={
|
header={
|
||||||
<BasicColumnHeader
|
<BasicColumnHeader
|
||||||
name={props.column.name ?? 'チャンネル'}
|
name={props.column.name ?? i18n()('column.channel')}
|
||||||
icon={<ChatBubbleLeftRight />}
|
icon={<ChatBubbleLeftRight />}
|
||||||
settings={() => <ColumnSettings column={props.column} columnIndex={props.columnIndex} />}
|
settings={() => <ColumnSettings column={props.column} columnIndex={props.columnIndex} />}
|
||||||
onClose={() => removeColumn(props.column.id)}
|
onClose={() => removeColumn(props.column.id)}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import Trash from 'heroicons/24/outline/trash.svg';
|
|||||||
import { ColumnType } from '@/core/column';
|
import { ColumnType } from '@/core/column';
|
||||||
import useConfig from '@/core/useConfig';
|
import useConfig from '@/core/useConfig';
|
||||||
import { useRequestCommand } from '@/hooks/useCommandBus';
|
import { useRequestCommand } from '@/hooks/useCommandBus';
|
||||||
|
import { useTranslation } from '@/i18n/useTranslation';
|
||||||
|
|
||||||
type ColumnSettingsProps = {
|
type ColumnSettingsProps = {
|
||||||
column: ColumnType;
|
column: ColumnType;
|
||||||
@@ -28,6 +29,7 @@ const ColumnSettingsSection: Component<ColumnSettingsSectionProps> = (props) =>
|
|||||||
};
|
};
|
||||||
|
|
||||||
const ColumnSettings: Component<ColumnSettingsProps> = (props) => {
|
const ColumnSettings: Component<ColumnSettingsProps> = (props) => {
|
||||||
|
const i18n = useTranslation();
|
||||||
const { saveColumn, removeColumn, moveColumn } = useConfig();
|
const { saveColumn, removeColumn, moveColumn } = useConfig();
|
||||||
const request = useRequestCommand();
|
const request = useRequestCommand();
|
||||||
|
|
||||||
@@ -42,41 +44,49 @@ const ColumnSettings: Component<ColumnSettingsProps> = (props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="flex flex-col border-t">
|
<div class="flex flex-col border-t">
|
||||||
<ColumnSettingsSection title="カラム幅">
|
<ColumnSettingsSection title={i18n()('column.config.columnWidth')}>
|
||||||
<div class="flex h-9 gap-2">
|
<div class="scrollbar flex h-9 gap-2 overflow-x-scroll">
|
||||||
<button
|
<button
|
||||||
class="rounded-md border px-4 hover:bg-stone-100"
|
class="rounded-md border px-4 hover:bg-stone-100"
|
||||||
onClick={() => setColumnWidth('widest')}
|
onClick={() => setColumnWidth('widest')}
|
||||||
>
|
>
|
||||||
特大
|
{i18n()('column.config.widest')}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="rounded-md border px-4 hover:bg-stone-100"
|
class="rounded-md border px-4 hover:bg-stone-100"
|
||||||
onClick={() => setColumnWidth('wide')}
|
onClick={() => setColumnWidth('wide')}
|
||||||
>
|
>
|
||||||
大
|
{i18n()('column.config.wide')}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="rounded-md border px-4 hover:bg-stone-100"
|
class="rounded-md border px-4 hover:bg-stone-100"
|
||||||
onClick={() => setColumnWidth('medium')}
|
onClick={() => setColumnWidth('medium')}
|
||||||
>
|
>
|
||||||
中
|
{i18n()('column.config.medium')}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="rounded-md border px-4 hover:bg-stone-100"
|
class="rounded-md border px-4 hover:bg-stone-100"
|
||||||
onClick={() => setColumnWidth('narrow')}
|
onClick={() => setColumnWidth('narrow')}
|
||||||
>
|
>
|
||||||
小
|
{i18n()('column.config.narrow')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</ColumnSettingsSection>
|
</ColumnSettingsSection>
|
||||||
<div class="flex h-10 items-center gap-2">
|
<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">
|
<span class="inline-block h-4 w-4">
|
||||||
<ChevronLeft />
|
<ChevronLeft />
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</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">
|
<span class="inline-block h-4 w-4">
|
||||||
<ChevronRight />
|
<ChevronRight />
|
||||||
</span>
|
</span>
|
||||||
@@ -84,10 +94,10 @@ const ColumnSettings: Component<ColumnSettingsProps> = (props) => {
|
|||||||
<div class="flex-1" />
|
<div class="flex-1" />
|
||||||
<button
|
<button
|
||||||
class="px-2 py-4 text-rose-500 hover:text-rose-600"
|
class="px-2 py-4 text-rose-500 hover:text-rose-600"
|
||||||
title="削除"
|
title={i18n()('column.config.removeColumn')}
|
||||||
onClick={() => removeColumn(props.column.id)}
|
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 />
|
<Trash />
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import Timeline from '@/components/timeline/Timeline';
|
|||||||
import { FollowingColumnType } from '@/core/column';
|
import { FollowingColumnType } from '@/core/column';
|
||||||
import { applyContentFilter } from '@/core/contentFilter';
|
import { applyContentFilter } from '@/core/contentFilter';
|
||||||
import useConfig from '@/core/useConfig';
|
import useConfig from '@/core/useConfig';
|
||||||
|
import { useTranslation } from '@/i18n/useTranslation';
|
||||||
import useFollowings from '@/nostr/useFollowings';
|
import useFollowings from '@/nostr/useFollowings';
|
||||||
import useSubscription from '@/nostr/useSubscription';
|
import useSubscription from '@/nostr/useSubscription';
|
||||||
import epoch from '@/utils/epoch';
|
import epoch from '@/utils/epoch';
|
||||||
@@ -21,6 +22,7 @@ type FollowingColumnDisplayProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const FollowingColumn: Component<FollowingColumnDisplayProps> = (props) => {
|
const FollowingColumn: Component<FollowingColumnDisplayProps> = (props) => {
|
||||||
|
const i18n = useTranslation();
|
||||||
const { config, removeColumn } = useConfig();
|
const { config, removeColumn } = useConfig();
|
||||||
|
|
||||||
const { followingPubkeys } = useFollowings(() => ({ pubkey: props.column.pubkey }));
|
const { followingPubkeys } = useFollowings(() => ({ pubkey: props.column.pubkey }));
|
||||||
@@ -57,7 +59,7 @@ const FollowingColumn: Component<FollowingColumnDisplayProps> = (props) => {
|
|||||||
<Column
|
<Column
|
||||||
header={
|
header={
|
||||||
<BasicColumnHeader
|
<BasicColumnHeader
|
||||||
name={props.column.name ?? 'ホーム'}
|
name={props.column.name ?? i18n()('column.home')}
|
||||||
icon={<Home />}
|
icon={<Home />}
|
||||||
settings={() => <ColumnSettings column={props.column} columnIndex={props.columnIndex} />}
|
settings={() => <ColumnSettings column={props.column} columnIndex={props.columnIndex} />}
|
||||||
onClose={() => removeColumn(props.column.id)}
|
onClose={() => removeColumn(props.column.id)}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import Notification from '@/components/timeline/Notification';
|
|||||||
import { NotificationColumnType } from '@/core/column';
|
import { NotificationColumnType } from '@/core/column';
|
||||||
import { applyContentFilter } from '@/core/contentFilter';
|
import { applyContentFilter } from '@/core/contentFilter';
|
||||||
import useConfig from '@/core/useConfig';
|
import useConfig from '@/core/useConfig';
|
||||||
|
import { useTranslation } from '@/i18n/useTranslation';
|
||||||
import useSubscription from '@/nostr/useSubscription';
|
import useSubscription from '@/nostr/useSubscription';
|
||||||
|
|
||||||
type NotificationColumnDisplayProps = {
|
type NotificationColumnDisplayProps = {
|
||||||
@@ -18,6 +19,7 @@ type NotificationColumnDisplayProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const NotificationColumn: Component<NotificationColumnDisplayProps> = (props) => {
|
const NotificationColumn: Component<NotificationColumnDisplayProps> = (props) => {
|
||||||
|
const i18n = useTranslation();
|
||||||
const { config, removeColumn } = useConfig();
|
const { config, removeColumn } = useConfig();
|
||||||
|
|
||||||
const { events: notifications } = useSubscription(() => ({
|
const { events: notifications } = useSubscription(() => ({
|
||||||
@@ -39,7 +41,7 @@ const NotificationColumn: Component<NotificationColumnDisplayProps> = (props) =>
|
|||||||
<Column
|
<Column
|
||||||
header={
|
header={
|
||||||
<BasicColumnHeader
|
<BasicColumnHeader
|
||||||
name={props.column.name ?? '通知'}
|
name={props.column.name ?? i18n()('column.notification')}
|
||||||
icon={<Bell />}
|
icon={<Bell />}
|
||||||
settings={() => <ColumnSettings column={props.column} columnIndex={props.columnIndex} />}
|
settings={() => <ColumnSettings column={props.column} columnIndex={props.columnIndex} />}
|
||||||
onClose={() => removeColumn(props.column.id)}
|
onClose={() => removeColumn(props.column.id)}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import Timeline from '@/components/timeline/Timeline';
|
|||||||
import { PostsColumnType } from '@/core/column';
|
import { PostsColumnType } from '@/core/column';
|
||||||
import { applyContentFilter } from '@/core/contentFilter';
|
import { applyContentFilter } from '@/core/contentFilter';
|
||||||
import useConfig from '@/core/useConfig';
|
import useConfig from '@/core/useConfig';
|
||||||
|
import { useTranslation } from '@/i18n/useTranslation';
|
||||||
import useSubscription from '@/nostr/useSubscription';
|
import useSubscription from '@/nostr/useSubscription';
|
||||||
|
|
||||||
type PostsColumnDisplayProps = {
|
type PostsColumnDisplayProps = {
|
||||||
@@ -18,6 +19,7 @@ type PostsColumnDisplayProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const PostsColumn: Component<PostsColumnDisplayProps> = (props) => {
|
const PostsColumn: Component<PostsColumnDisplayProps> = (props) => {
|
||||||
|
const i18n = useTranslation();
|
||||||
const { config, removeColumn } = useConfig();
|
const { config, removeColumn } = useConfig();
|
||||||
|
|
||||||
const { events } = useSubscription(() => ({
|
const { events } = useSubscription(() => ({
|
||||||
@@ -39,7 +41,7 @@ const PostsColumn: Component<PostsColumnDisplayProps> = (props) => {
|
|||||||
<Column
|
<Column
|
||||||
header={
|
header={
|
||||||
<BasicColumnHeader
|
<BasicColumnHeader
|
||||||
name={props.column.name ?? '投稿'}
|
name={props.column.name ?? i18n()('column.posts')}
|
||||||
icon={<User />}
|
icon={<User />}
|
||||||
settings={() => <ColumnSettings column={props.column} columnIndex={props.columnIndex} />}
|
settings={() => <ColumnSettings column={props.column} columnIndex={props.columnIndex} />}
|
||||||
onClose={() => removeColumn(props.column.id)}
|
onClose={() => removeColumn(props.column.id)}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import Notification from '@/components/timeline/Notification';
|
|||||||
import { ReactionsColumnType } from '@/core/column';
|
import { ReactionsColumnType } from '@/core/column';
|
||||||
import { applyContentFilter } from '@/core/contentFilter';
|
import { applyContentFilter } from '@/core/contentFilter';
|
||||||
import useConfig from '@/core/useConfig';
|
import useConfig from '@/core/useConfig';
|
||||||
|
import { useTranslation } from '@/i18n/useTranslation';
|
||||||
import useSubscription from '@/nostr/useSubscription';
|
import useSubscription from '@/nostr/useSubscription';
|
||||||
|
|
||||||
type ReactionsColumnDisplayProps = {
|
type ReactionsColumnDisplayProps = {
|
||||||
@@ -18,6 +19,7 @@ type ReactionsColumnDisplayProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const ReactionsColumn: Component<ReactionsColumnDisplayProps> = (props) => {
|
const ReactionsColumn: Component<ReactionsColumnDisplayProps> = (props) => {
|
||||||
|
const i18n = useTranslation();
|
||||||
const { config, removeColumn } = useConfig();
|
const { config, removeColumn } = useConfig();
|
||||||
|
|
||||||
const { events: reactions } = useSubscription(() => ({
|
const { events: reactions } = useSubscription(() => ({
|
||||||
@@ -39,7 +41,7 @@ const ReactionsColumn: Component<ReactionsColumnDisplayProps> = (props) => {
|
|||||||
<Column
|
<Column
|
||||||
header={
|
header={
|
||||||
<BasicColumnHeader
|
<BasicColumnHeader
|
||||||
name={props.column.name ?? 'リアクション'}
|
name={props.column.name ?? i18n()('column.reactions')}
|
||||||
icon={<Heart />}
|
icon={<Heart />}
|
||||||
settings={() => <ColumnSettings column={props.column} columnIndex={props.columnIndex} />}
|
settings={() => <ColumnSettings column={props.column} columnIndex={props.columnIndex} />}
|
||||||
onClose={() => removeColumn(props.column.id)}
|
onClose={() => removeColumn(props.column.id)}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import Timeline from '@/components/timeline/Timeline';
|
|||||||
import { RelaysColumnType } from '@/core/column';
|
import { RelaysColumnType } from '@/core/column';
|
||||||
import { applyContentFilter } from '@/core/contentFilter';
|
import { applyContentFilter } from '@/core/contentFilter';
|
||||||
import useConfig from '@/core/useConfig';
|
import useConfig from '@/core/useConfig';
|
||||||
|
import { useTranslation } from '@/i18n/useTranslation';
|
||||||
import useSubscription from '@/nostr/useSubscription';
|
import useSubscription from '@/nostr/useSubscription';
|
||||||
import epoch from '@/utils/epoch';
|
import epoch from '@/utils/epoch';
|
||||||
|
|
||||||
@@ -19,6 +20,7 @@ type RelaysColumnDisplayProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const RelaysColumn: Component<RelaysColumnDisplayProps> = (props) => {
|
const RelaysColumn: Component<RelaysColumnDisplayProps> = (props) => {
|
||||||
|
const i18n = useTranslation();
|
||||||
const { removeColumn } = useConfig();
|
const { removeColumn } = useConfig();
|
||||||
|
|
||||||
const { events } = useSubscription(() => ({
|
const { events } = useSubscription(() => ({
|
||||||
@@ -40,7 +42,7 @@ const RelaysColumn: Component<RelaysColumnDisplayProps> = (props) => {
|
|||||||
<Column
|
<Column
|
||||||
header={
|
header={
|
||||||
<BasicColumnHeader
|
<BasicColumnHeader
|
||||||
name={props.column.name ?? 'リレー'}
|
name={props.column.name ?? i18n()('column.relay')}
|
||||||
icon={<GlobeAlt />}
|
icon={<GlobeAlt />}
|
||||||
settings={() => <ColumnSettings column={props.column} columnIndex={props.columnIndex} />}
|
settings={() => <ColumnSettings column={props.column} columnIndex={props.columnIndex} />}
|
||||||
onClose={() => removeColumn(props.column.id)}
|
onClose={() => removeColumn(props.column.id)}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import TextNoteDisplay from '@/components/event/textNote/TextNoteDisplay';
|
|||||||
import UserDisplayName from '@/components/UserDisplayName';
|
import UserDisplayName from '@/components/UserDisplayName';
|
||||||
import useConfig from '@/core/useConfig';
|
import useConfig from '@/core/useConfig';
|
||||||
import useModalState from '@/hooks/useModalState';
|
import useModalState from '@/hooks/useModalState';
|
||||||
|
import { useTranslation } from '@/i18n/useTranslation';
|
||||||
import { genericEvent } from '@/nostr/event';
|
import { genericEvent } from '@/nostr/event';
|
||||||
import useEvent from '@/nostr/useEvent';
|
import useEvent from '@/nostr/useEvent';
|
||||||
import useProfile from '@/nostr/useProfile';
|
import useProfile from '@/nostr/useProfile';
|
||||||
@@ -17,6 +18,7 @@ type ReactionProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const Reaction: Component<ReactionProps> = (props) => {
|
const Reaction: Component<ReactionProps> = (props) => {
|
||||||
|
const i18n = useTranslation();
|
||||||
const { shouldMuteEvent } = useConfig();
|
const { shouldMuteEvent } = useConfig();
|
||||||
const { showProfile } = useModalState();
|
const { showProfile } = useModalState();
|
||||||
const event = () => genericEvent(props.event);
|
const event = () => genericEvent(props.event);
|
||||||
@@ -65,7 +67,7 @@ const Reaction: Component<ReactionProps> = (props) => {
|
|||||||
>
|
>
|
||||||
<UserDisplayName pubkey={props.event.pubkey} />
|
<UserDisplayName pubkey={props.event.pubkey} />
|
||||||
</button>
|
</button>
|
||||||
{' がリアクション'}
|
{i18n()('notification.reacted')}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import EventDisplayById from '@/components/event/EventDisplayById';
|
|||||||
import UserDisplayName from '@/components/UserDisplayName';
|
import UserDisplayName from '@/components/UserDisplayName';
|
||||||
import useFormatDate from '@/hooks/useFormatDate';
|
import useFormatDate from '@/hooks/useFormatDate';
|
||||||
import useModalState from '@/hooks/useModalState';
|
import useModalState from '@/hooks/useModalState';
|
||||||
|
import { useTranslation } from '@/i18n/useTranslation';
|
||||||
import { genericEvent } from '@/nostr/event';
|
import { genericEvent } from '@/nostr/event';
|
||||||
|
|
||||||
export type RepostProps = {
|
export type RepostProps = {
|
||||||
@@ -16,6 +17,7 @@ export type RepostProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const Repost: Component<RepostProps> = (props) => {
|
const Repost: Component<RepostProps> = (props) => {
|
||||||
|
const i18n = useTranslation();
|
||||||
const { showProfile } = useModalState();
|
const { showProfile } = useModalState();
|
||||||
const formatDate = useFormatDate();
|
const formatDate = useFormatDate();
|
||||||
const event = createMemo(() => genericEvent(props.event));
|
const event = createMemo(() => genericEvent(props.event));
|
||||||
@@ -34,7 +36,7 @@ const Repost: Component<RepostProps> = (props) => {
|
|||||||
>
|
>
|
||||||
<UserDisplayName pubkey={props.event.pubkey} />
|
<UserDisplayName pubkey={props.event.pubkey} />
|
||||||
</button>
|
</button>
|
||||||
{' がリポスト'}
|
{i18n()('notification.reposted')}
|
||||||
</div>
|
</div>
|
||||||
<div>{formatDate(event().createdAtAsDate())}</div>
|
<div>{formatDate(event().createdAtAsDate())}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -35,8 +35,7 @@ const ZapReceipt: Component<ZapReceiptProps> = (props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Show when={!shouldMuteEvent(props.event)}>
|
<Show when={!shouldMuteEvent(props.event)}>
|
||||||
⚡
|
⚡{/* <UserNameDisplay pubkey={zapRequest().pubkey} /> */}
|
||||||
<UserNameDisplay pubkey={zapRequest().pubkey} />
|
|
||||||
<pre>{JSON.stringify(props.event, null, 2)}</pre>
|
<pre>{JSON.stringify(props.event, null, 2)}</pre>
|
||||||
</Show>
|
</Show>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ import Post from '@/components/Post';
|
|||||||
import { useTimelineContext } from '@/components/timeline/TimelineContext';
|
import { useTimelineContext } from '@/components/timeline/TimelineContext';
|
||||||
import useConfig from '@/core/useConfig';
|
import useConfig from '@/core/useConfig';
|
||||||
import useModalState from '@/hooks/useModalState';
|
import useModalState from '@/hooks/useModalState';
|
||||||
|
import { useTranslation } from '@/i18n/useTranslation';
|
||||||
import { textNote } from '@/nostr/event';
|
import { textNote } from '@/nostr/event';
|
||||||
import useCommands from '@/nostr/useCommands';
|
import useCommands from '@/nostr/useCommands';
|
||||||
import usePubkey from '@/nostr/usePubkey';
|
import usePubkey from '@/nostr/usePubkey';
|
||||||
@@ -97,6 +98,7 @@ const EmojiReactions: Component<EmojiReactionsProps> = (props) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const TextNoteDisplay: Component<TextNoteDisplayProps> = (props) => {
|
const TextNoteDisplay: Component<TextNoteDisplayProps> = (props) => {
|
||||||
|
const i18n = useTranslation();
|
||||||
const { config } = useConfig();
|
const { config } = useConfig();
|
||||||
const pubkey = usePubkey();
|
const pubkey = usePubkey();
|
||||||
const { showProfile } = useModalState();
|
const { showProfile } = useModalState();
|
||||||
@@ -180,11 +182,11 @@ const TextNoteDisplay: Component<TextNoteDisplayProps> = (props) => {
|
|||||||
const succeeded = results.filter((res) => res.status === 'fulfilled').length;
|
const succeeded = results.filter((res) => res.status === 'fulfilled').length;
|
||||||
const failed = results.length - succeeded;
|
const failed = results.length - succeeded;
|
||||||
if (succeeded === results.length) {
|
if (succeeded === results.length) {
|
||||||
window.alert('削除しました(画面の反映にはリロード)');
|
window.alert(i18n()('post.deletedSuccessfully'));
|
||||||
} else if (succeeded > 0) {
|
} else if (succeeded > 0) {
|
||||||
window.alert(`${failed}個のリレーで削除に失敗しました`);
|
window.alert(i18n()('post.failedToDeletePartially', { count: failed }));
|
||||||
} else {
|
} else {
|
||||||
window.alert('すべてのリレーで削除に失敗しました');
|
window.alert(i18n()('post.failedToDelete'));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onError: (err) => {
|
onError: (err) => {
|
||||||
@@ -194,37 +196,37 @@ const TextNoteDisplay: Component<TextNoteDisplayProps> = (props) => {
|
|||||||
|
|
||||||
const menu: MenuItem[] = [
|
const menu: MenuItem[] = [
|
||||||
{
|
{
|
||||||
content: () => 'IDをコピー',
|
content: () => i18n()('post.copyEventId'),
|
||||||
onSelect: () => {
|
onSelect: () => {
|
||||||
navigator.clipboard.writeText(noteEncode(props.event.id)).catch((err) => window.alert(err));
|
navigator.clipboard.writeText(noteEncode(props.event.id)).catch((err) => window.alert(err));
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
content: () => 'JSONを確認',
|
content: () => i18n()('post.showJSON'),
|
||||||
onSelect: () => {
|
onSelect: () => {
|
||||||
setModal('EventDebugModal');
|
setModal('EventDebugModal');
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
content: () => 'リポスト一覧',
|
content: () => i18n()('post.showReposts'),
|
||||||
onSelect: () => {
|
onSelect: () => {
|
||||||
setModal('Reposts');
|
setModal('Reposts');
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
content: () => 'リアクション一覧',
|
content: () => i18n()('post.showReactions'),
|
||||||
onSelect: () => {
|
onSelect: () => {
|
||||||
setModal('Reactions');
|
setModal('Reactions');
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
when: () => event().pubkey === pubkey(),
|
when: () => event().pubkey === pubkey(),
|
||||||
content: () => <span class="text-red-500">削除</span>,
|
content: () => <span class="text-red-500">{i18n()('post.deletePost')}</span>,
|
||||||
onSelect: () => {
|
onSelect: () => {
|
||||||
const p = pubkey();
|
const p = pubkey();
|
||||||
if (p == null) return;
|
if (p == null) return;
|
||||||
|
|
||||||
if (!window.confirm('本当に削除しますか?')) return;
|
if (!window.confirm(i18n()('post.confirmDelete'))) return;
|
||||||
deleteMutation.mutate({
|
deleteMutation.mutate({
|
||||||
relayUrls: config().relayUrls,
|
relayUrls: config().relayUrls,
|
||||||
pubkey: p,
|
pubkey: p,
|
||||||
@@ -326,6 +328,7 @@ const TextNoteDisplay: Component<TextNoteDisplayProps> = (props) => {
|
|||||||
</Show>
|
</Show>
|
||||||
<Show when={event().taggedPubkeys().length > 0}>
|
<Show when={event().taggedPubkeys().length > 0}>
|
||||||
<div class="text-xs">
|
<div class="text-xs">
|
||||||
|
{i18n()('post.replyToPre')}
|
||||||
<For each={event().taggedPubkeys()}>
|
<For each={event().taggedPubkeys()}>
|
||||||
{(replyToPubkey: string) => (
|
{(replyToPubkey: string) => (
|
||||||
<button
|
<button
|
||||||
@@ -339,7 +342,7 @@ const TextNoteDisplay: Component<TextNoteDisplayProps> = (props) => {
|
|||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
{'への返信'}
|
{i18n()('post.replyToPost')}
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
<ContentWarningDisplay contentWarning={event().contentWarning()}>
|
<ContentWarningDisplay contentWarning={event().contentWarning()}>
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import {
|
|||||||
} from '@/core/column';
|
} from '@/core/column';
|
||||||
import useConfig from '@/core/useConfig';
|
import useConfig from '@/core/useConfig';
|
||||||
import { useRequestCommand } from '@/hooks/useCommandBus';
|
import { useRequestCommand } from '@/hooks/useCommandBus';
|
||||||
|
import { useTranslation } from '@/i18n/useTranslation';
|
||||||
import usePubkey from '@/nostr/usePubkey';
|
import usePubkey from '@/nostr/usePubkey';
|
||||||
import ensureNonNull from '@/utils/ensureNonNull';
|
import ensureNonNull from '@/utils/ensureNonNull';
|
||||||
|
|
||||||
@@ -28,6 +29,7 @@ type AddColumnProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const AddColumn: Component<AddColumnProps> = (props) => {
|
const AddColumn: Component<AddColumnProps> = (props) => {
|
||||||
|
const i18n = useTranslation();
|
||||||
const pubkey = usePubkey();
|
const pubkey = usePubkey();
|
||||||
const { saveColumn } = useConfig();
|
const { saveColumn } = useConfig();
|
||||||
const request = useRequestCommand();
|
const request = useRequestCommand();
|
||||||
@@ -85,7 +87,7 @@ const AddColumn: Component<AddColumnProps> = (props) => {
|
|||||||
<span class="inline-block h-8 w-8">
|
<span class="inline-block h-8 w-8">
|
||||||
<Home />
|
<Home />
|
||||||
</span>
|
</span>
|
||||||
ホーム
|
{i18n()('column.home')}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="flex basis-1/2 flex-col items-center gap-2 py-8 sm:basis-1/4"
|
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">
|
<span class="inline-block h-8 w-8">
|
||||||
<Bell />
|
<Bell />
|
||||||
</span>
|
</span>
|
||||||
通知
|
{i18n()('column.notification')}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="flex basis-1/2 flex-col items-center gap-2 py-8 sm:basis-1/4"
|
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">
|
<span class="inline-block h-8 w-8">
|
||||||
<GlobeAlt />
|
<GlobeAlt />
|
||||||
</span>
|
</span>
|
||||||
日本リレー
|
{i18n()('column.japanese')}
|
||||||
</button>
|
</button>
|
||||||
{/*
|
{/*
|
||||||
<button
|
<button
|
||||||
@@ -134,7 +136,7 @@ const AddColumn: Component<AddColumnProps> = (props) => {
|
|||||||
<span class="inline-block h-8 w-8">
|
<span class="inline-block h-8 w-8">
|
||||||
<MagnifyingGlass />
|
<MagnifyingGlass />
|
||||||
</span>
|
</span>
|
||||||
検索
|
{i18n()('column.search')}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="flex basis-1/2 flex-col items-center gap-2 py-8 sm:basis-1/4"
|
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">
|
<span class="inline-block h-8 w-8">
|
||||||
<User />
|
<User />
|
||||||
</span>
|
</span>
|
||||||
自分の投稿
|
{i18n()('column.myPosts')}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="flex basis-1/2 flex-col items-center gap-2 py-8 sm:basis-1/4"
|
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">
|
<span class="inline-block h-8 w-8">
|
||||||
<Heart />
|
<Heart />
|
||||||
</span>
|
</span>
|
||||||
自分のリアクション
|
{i18n()('column.myReactions')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</BasicModal>
|
</BasicModal>
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import BasicModal from '@/components/modal/BasicModal';
|
|||||||
import UserNameDisplay from '@/components/UserDisplayName';
|
import UserNameDisplay from '@/components/UserDisplayName';
|
||||||
import useConfig, { type Config } from '@/core/useConfig';
|
import useConfig, { type Config } from '@/core/useConfig';
|
||||||
import useModalState from '@/hooks/useModalState';
|
import useModalState from '@/hooks/useModalState';
|
||||||
|
import { useTranslation } from '@/i18n/useTranslation';
|
||||||
import usePubkey from '@/nostr/usePubkey';
|
import usePubkey from '@/nostr/usePubkey';
|
||||||
import { simpleEmojiPackSchema, convertToEmojiConfig } from '@/utils/emojipack';
|
import { simpleEmojiPackSchema, convertToEmojiConfig } from '@/utils/emojipack';
|
||||||
import ensureNonNull from '@/utils/ensureNonNull';
|
import ensureNonNull from '@/utils/ensureNonNull';
|
||||||
@@ -26,12 +27,13 @@ const HttpUrlRegex = BaseUrlRegex('https?');
|
|||||||
const RelayUrlRegex = BaseUrlRegex('wss?');
|
const RelayUrlRegex = BaseUrlRegex('wss?');
|
||||||
|
|
||||||
const ProfileSection = () => {
|
const ProfileSection = () => {
|
||||||
|
const i18n = useTranslation();
|
||||||
const pubkey = usePubkey();
|
const pubkey = usePubkey();
|
||||||
const { showProfile, showProfileEdit } = useModalState();
|
const { showProfile, showProfileEdit } = useModalState();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="py-2">
|
<div class="py-2">
|
||||||
<h3 class="font-bold">プロフィール</h3>
|
<h3 class="font-bold">{i18n()('config.profile.profile')}</h3>
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<button
|
<button
|
||||||
class="rounded border border-rose-300 px-4 py-2 font-bold text-rose-300"
|
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>
|
||||||
<button
|
<button
|
||||||
class="rounded border border-rose-300 px-4 py-2 font-bold text-rose-300"
|
class="rounded border border-rose-300 px-4 py-2 font-bold text-rose-300"
|
||||||
onClick={() => showProfileEdit()}
|
onClick={() => showProfileEdit()}
|
||||||
>
|
>
|
||||||
編集
|
{i18n()('config.profile.editProfile')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -55,6 +57,7 @@ const ProfileSection = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const RelayConfig = () => {
|
const RelayConfig = () => {
|
||||||
|
const i18n = useTranslation();
|
||||||
const { config, addRelay, removeRelay } = useConfig();
|
const { config, addRelay, removeRelay } = useConfig();
|
||||||
|
|
||||||
const [relayUrlInput, setRelayUrlInput] = createSignal<string>('');
|
const [relayUrlInput, setRelayUrlInput] = createSignal<string>('');
|
||||||
@@ -73,11 +76,11 @@ const RelayConfig = () => {
|
|||||||
const relayUrls = importedRelays.map(([relayUrl]) => relayUrl).join('\n');
|
const relayUrls = importedRelays.map(([relayUrl]) => relayUrl).join('\n');
|
||||||
|
|
||||||
if (importedRelays.length === 0) {
|
if (importedRelays.length === 0) {
|
||||||
window.alert('リレーが設定されていません');
|
window.alert(i18n()('config.relays.notConfigured'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!window.confirm(`これらのリレーをインポートしますか:\n${relayUrls}`)) {
|
if (!window.confirm(`${i18n()('config.relays.askImport')}\n\n${relayUrls}`)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,14 +92,16 @@ const RelayConfig = () => {
|
|||||||
});
|
});
|
||||||
const currentCount = config().relayUrls.length;
|
const currentCount = config().relayUrls.length;
|
||||||
const importedCount = currentCount - lastCount;
|
const importedCount = currentCount - lastCount;
|
||||||
window.alert(`${importedCount} 個のリレーをインポートしました`);
|
window.alert(i18n()('config.relays.imported', { count: importedCount }));
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div class="py-2">
|
<div class="py-2">
|
||||||
<h3 class="font-bold">リレー</h3>
|
<h3 class="font-bold">{i18n()('config.relays.relays')}</h3>
|
||||||
<p class="py-1">{config().relayUrls.length} 個のリレーが設定されています</p>
|
<p class="py-1">
|
||||||
|
{i18n()('config.relays.numOfRelays', { count: config().relayUrls.length })}
|
||||||
|
</p>
|
||||||
<ul>
|
<ul>
|
||||||
<For each={config().relayUrls}>
|
<For each={config().relayUrls}>
|
||||||
{(relayUrl: string) => {
|
{(relayUrl: string) => {
|
||||||
@@ -121,61 +126,62 @@ const RelayConfig = () => {
|
|||||||
onChange={(ev) => setRelayUrlInput(ev.currentTarget.value)}
|
onChange={(ev) => setRelayUrlInput(ev.currentTarget.value)}
|
||||||
/>
|
/>
|
||||||
<button type="submit" class="rounded bg-rose-300 p-2 font-bold text-white">
|
<button type="submit" class="rounded bg-rose-300 p-2 font-bold text-white">
|
||||||
追加
|
{i18n()('config.relays.addRelay')}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div class="py-2">
|
<div class="py-2">
|
||||||
<h3 class="pb-1 font-bold">インポート</h3>
|
<h3 class="pb-1 font-bold">{i18n()('config.relays.importRelays')}</h3>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="rounded bg-rose-300 p-2 font-bold text-white"
|
class="rounded bg-rose-300 p-2 font-bold text-white"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
importFromNIP07().catch((err) => {
|
importFromNIP07().catch((err) => {
|
||||||
console.error('failed to import relays', err);
|
console.error('failed to import relays', err);
|
||||||
window.alert('インポートに失敗しました');
|
window.alert(i18n()('config.relays.failedToImport'));
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
拡張機能からインポート
|
{i18n()('config.relays.importFromExtension')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</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 DateFormatConfig = () => {
|
||||||
|
const i18n = useTranslation();
|
||||||
const { config, setConfig } = useConfig();
|
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']) => {
|
const updateDateFormat = (dateFormat: Config['dateFormat']) => {
|
||||||
setConfig((current) => ({ ...current, dateFormat }));
|
setConfig((current) => ({ ...current, dateFormat }));
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="py-2">
|
<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">
|
<div class="flex flex-col justify-evenly gap-2 sm:flex-row">
|
||||||
<For each={dateFormats}>
|
<For each={dateFormats}>
|
||||||
{({ id, name, example }) => (
|
{({ id, name, example }) => (
|
||||||
@@ -224,6 +230,7 @@ const ToggleButton = (props: {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const ReactionConfig = () => {
|
const ReactionConfig = () => {
|
||||||
|
const i18n = useTranslation();
|
||||||
const { config, setConfig } = useConfig();
|
const { config, setConfig } = useConfig();
|
||||||
|
|
||||||
const toggleUseEmojiReaction = () => {
|
const toggleUseEmojiReaction = () => {
|
||||||
@@ -242,17 +249,17 @@ const ReactionConfig = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="py-2">
|
<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 flex-col justify-evenly gap-2">
|
||||||
<div class="flex w-full">
|
<div class="flex w-full">
|
||||||
<div class="flex-1">絵文字を選べるようにする</div>
|
<div class="flex-1">{i18n()('config.display.enableEmojiReaction')}</div>
|
||||||
<ToggleButton
|
<ToggleButton
|
||||||
value={config().useEmojiReaction}
|
value={config().useEmojiReaction}
|
||||||
onClick={() => toggleUseEmojiReaction()}
|
onClick={() => toggleUseEmojiReaction()}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex w-full">
|
<div class="flex w-full">
|
||||||
<div class="flex-1">投稿にリアクションされた絵文字を表示する</div>
|
<div class="flex-1">{i18n()('config.display.showEmojiReaction')}</div>
|
||||||
<ToggleButton
|
<ToggleButton
|
||||||
value={config().showEmojiReaction}
|
value={config().showEmojiReaction}
|
||||||
onClick={() => toggleShowEmojiReaction()}
|
onClick={() => toggleShowEmojiReaction()}
|
||||||
@@ -264,6 +271,7 @@ const ReactionConfig = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const EmojiConfig = () => {
|
const EmojiConfig = () => {
|
||||||
|
const i18n = useTranslation();
|
||||||
const { config, saveEmoji, removeEmoji } = useConfig();
|
const { config, saveEmoji, removeEmoji } = useConfig();
|
||||||
|
|
||||||
const [shortcodeInput, setShortcodeInput] = createSignal('');
|
const [shortcodeInput, setShortcodeInput] = createSignal('');
|
||||||
@@ -279,7 +287,7 @@ const EmojiConfig = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="py-2">
|
<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">
|
<ul class="flex flex-col gap-1 py-2">
|
||||||
<For each={Object.values(config().customEmojis)}>
|
<For each={Object.values(config().customEmojis)}>
|
||||||
{({ shortcode, url }) => (
|
{({ shortcode, url }) => (
|
||||||
@@ -295,7 +303,7 @@ const EmojiConfig = () => {
|
|||||||
</ul>
|
</ul>
|
||||||
<form class="flex flex-col gap-2" onSubmit={handleClickSaveEmoji}>
|
<form class="flex flex-col gap-2" onSubmit={handleClickSaveEmoji}>
|
||||||
<label class="flex flex-1 items-center gap-1">
|
<label class="flex flex-1 items-center gap-1">
|
||||||
<div class="w-9">名前</div>
|
<div class="w-9">{i18n()('config.customEmoji.shortcode')}</div>
|
||||||
<input
|
<input
|
||||||
class="flex-1 rounded-md focus:border-rose-100 focus:ring-rose-300"
|
class="flex-1 rounded-md focus:border-rose-100 focus:ring-rose-300"
|
||||||
type="text"
|
type="text"
|
||||||
@@ -308,7 +316,7 @@ const EmojiConfig = () => {
|
|||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<label class="flex flex-1 items-center gap-1">
|
<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
|
<input
|
||||||
class="flex-1 rounded-md focus:border-rose-100 focus:ring-rose-300"
|
class="flex-1 rounded-md focus:border-rose-100 focus:ring-rose-300"
|
||||||
type="text"
|
type="text"
|
||||||
@@ -321,7 +329,7 @@ const EmojiConfig = () => {
|
|||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<button type="submit" class="w-24 self-end rounded bg-rose-300 p-2 font-bold text-white">
|
<button type="submit" class="w-24 self-end rounded bg-rose-300 p-2 font-bold text-white">
|
||||||
追加
|
{i18n()('config.customEmoji.addEmoji')}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
@@ -329,6 +337,7 @@ const EmojiConfig = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const EmojiImport = () => {
|
const EmojiImport = () => {
|
||||||
|
const i18n = useTranslation();
|
||||||
const { saveEmojis } = useConfig();
|
const { saveEmojis } = useConfig();
|
||||||
|
|
||||||
const [jsonInput, setJSONInput] = createSignal('');
|
const [jsonInput, setJSONInput] = createSignal('');
|
||||||
@@ -350,8 +359,8 @@ const EmojiImport = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="py-2">
|
<div class="py-2">
|
||||||
<h3 class="font-bold">絵文字のインポート</h3>
|
<h3 class="font-bold">{i18n()('config.customEmoji.emojiImport')}</h3>
|
||||||
<p>絵文字の名前をキー、画像のURLを値とするJSONを読み込むことができます。</p>
|
<p>{i18n()('config.customEmoji.emojiImportDescription')}</p>
|
||||||
<form class="flex flex-col gap-2" onSubmit={handleClickSaveEmoji}>
|
<form class="flex flex-col gap-2" onSubmit={handleClickSaveEmoji}>
|
||||||
<textarea
|
<textarea
|
||||||
class="flex-1 rounded-md focus:border-rose-100 focus:ring-rose-300"
|
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)}
|
onChange={(ev) => setJSONInput(ev.currentTarget.value)}
|
||||||
/>
|
/>
|
||||||
<button type="submit" class="w-24 self-end rounded bg-rose-300 p-2 font-bold text-white">
|
<button type="submit" class="w-24 self-end rounded bg-rose-300 p-2 font-bold text-white">
|
||||||
インポート
|
{i18n()('config.customEmoji.importEmoji')}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
@@ -369,6 +378,7 @@ const EmojiImport = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const MuteConfig = () => {
|
const MuteConfig = () => {
|
||||||
|
const i18n = useTranslation();
|
||||||
const { config, removeMutedPubkey, addMutedKeyword, removeMutedKeyword } = useConfig();
|
const { config, removeMutedPubkey, addMutedKeyword, removeMutedKeyword } = useConfig();
|
||||||
|
|
||||||
const [keywordInput, setKeywordInput] = createSignal('');
|
const [keywordInput, setKeywordInput] = createSignal('');
|
||||||
@@ -383,7 +393,7 @@ const MuteConfig = () => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div class="py-2">
|
<div class="py-2">
|
||||||
<h3 class="font-bold">ミュートしたユーザ</h3>
|
<h3 class="font-bold">{i18n()('config.mute.mutedUsers')}</h3>
|
||||||
<ul class="flex flex-col">
|
<ul class="flex flex-col">
|
||||||
<For each={config().mutedPubkeys}>
|
<For each={config().mutedPubkeys}>
|
||||||
{(pubkey) => (
|
{(pubkey) => (
|
||||||
@@ -400,7 +410,7 @@ const MuteConfig = () => {
|
|||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div class="py-2">
|
<div class="py-2">
|
||||||
<h3 class="font-bold">ミュートした単語</h3>
|
<h3 class="font-bold">{i18n()('config.mute.mutedKeywords')}</h3>
|
||||||
<ul class="flex flex-col">
|
<ul class="flex flex-col">
|
||||||
<For each={config().mutedKeywords}>
|
<For each={config().mutedKeywords}>
|
||||||
{(keyword) => (
|
{(keyword) => (
|
||||||
@@ -422,7 +432,7 @@ const MuteConfig = () => {
|
|||||||
onChange={(ev) => setKeywordInput(ev.currentTarget.value)}
|
onChange={(ev) => setKeywordInput(ev.currentTarget.value)}
|
||||||
/>
|
/>
|
||||||
<button type="submit" class="rounded bg-rose-300 p-2 font-bold text-white">
|
<button type="submit" class="rounded bg-rose-300 p-2 font-bold text-white">
|
||||||
追加
|
{i18n()('config.mute.add')}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
@@ -431,6 +441,7 @@ const MuteConfig = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const OtherConfig = () => {
|
const OtherConfig = () => {
|
||||||
|
const i18n = useTranslation();
|
||||||
const { config, setConfig } = useConfig();
|
const { config, setConfig } = useConfig();
|
||||||
|
|
||||||
const toggleKeepOpenPostForm = () => {
|
const toggleKeepOpenPostForm = () => {
|
||||||
@@ -456,21 +467,21 @@ const OtherConfig = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="py-2">
|
<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 flex-col justify-evenly gap-2">
|
||||||
<div class="flex w-full">
|
<div class="flex w-full">
|
||||||
<div class="flex-1">投稿欄を開いたままにする</div>
|
<div class="flex-1">{i18n()('config.display.keepOpenPostForm')}</div>
|
||||||
<ToggleButton
|
<ToggleButton
|
||||||
value={config().keepOpenPostForm}
|
value={config().keepOpenPostForm}
|
||||||
onClick={() => toggleKeepOpenPostForm()}
|
onClick={() => toggleKeepOpenPostForm()}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex w-full">
|
<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()} />
|
<ToggleButton value={config().showImage} onClick={() => toggleShowImage()} />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex w-full">
|
<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()} />
|
<ToggleButton value={config().hideCount} onClick={() => toggleHideCount()} />
|
||||||
</div>
|
</div>
|
||||||
{/*
|
{/*
|
||||||
@@ -489,21 +500,22 @@ const OtherConfig = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const ConfigUI = (props: ConfigProps) => {
|
const ConfigUI = (props: ConfigProps) => {
|
||||||
|
const i18n = useTranslation();
|
||||||
const [menuIndex, setMenuIndex] = createSignal<number | null>(null);
|
const [menuIndex, setMenuIndex] = createSignal<number | null>(null);
|
||||||
|
|
||||||
const menu = [
|
const menu = [
|
||||||
{
|
{
|
||||||
name: () => 'プロフィール',
|
name: () => i18n()('config.profile.profile'),
|
||||||
icon: () => <User />,
|
icon: () => <User />,
|
||||||
render: () => <ProfileSection />,
|
render: () => <ProfileSection />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: () => 'リレー',
|
name: () => i18n()('config.relays.relays'),
|
||||||
icon: () => <ServerStack />,
|
icon: () => <ServerStack />,
|
||||||
render: () => <RelayConfig />,
|
render: () => <RelayConfig />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: () => '表示',
|
name: () => i18n()('config.display.display'),
|
||||||
icon: () => <PaintBrush />,
|
icon: () => <PaintBrush />,
|
||||||
render: () => (
|
render: () => (
|
||||||
<>
|
<>
|
||||||
@@ -514,7 +526,7 @@ const ConfigUI = (props: ConfigProps) => {
|
|||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: () => 'カスタム絵文字',
|
name: () => i18n()('config.customEmoji.customEmoji'),
|
||||||
icon: () => <FaceSmile />,
|
icon: () => <FaceSmile />,
|
||||||
render: () => (
|
render: () => (
|
||||||
<>
|
<>
|
||||||
@@ -524,7 +536,7 @@ const ConfigUI = (props: ConfigProps) => {
|
|||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: () => 'ミュート',
|
name: () => i18n()('config.mute.mute'),
|
||||||
icon: () => <EyeSlash />,
|
icon: () => <EyeSlash />,
|
||||||
render: () => <MuteConfig />,
|
render: () => <MuteConfig />,
|
||||||
},
|
},
|
||||||
@@ -543,7 +555,7 @@ const ConfigUI = (props: ConfigProps) => {
|
|||||||
when={getMenuItem()}
|
when={getMenuItem()}
|
||||||
fallback={
|
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">
|
<ul class="flex flex-col">
|
||||||
<For each={menu}>
|
<For each={menu}>
|
||||||
{(menuItem, i) => (
|
{(menuItem, i) => (
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import Timeline from '@/components/timeline/Timeline';
|
|||||||
import SafeLink from '@/components/utils/SafeLink';
|
import SafeLink from '@/components/utils/SafeLink';
|
||||||
import useConfig from '@/core/useConfig';
|
import useConfig from '@/core/useConfig';
|
||||||
import useModalState from '@/hooks/useModalState';
|
import useModalState from '@/hooks/useModalState';
|
||||||
|
import { useTranslation } from '@/i18n/useTranslation';
|
||||||
import useCommands from '@/nostr/useCommands';
|
import useCommands from '@/nostr/useCommands';
|
||||||
import useFollowers from '@/nostr/useFollowers';
|
import useFollowers from '@/nostr/useFollowers';
|
||||||
import useFollowings from '@/nostr/useFollowings';
|
import useFollowings from '@/nostr/useFollowings';
|
||||||
@@ -42,6 +43,7 @@ const FollowersCount: Component<{ pubkey: string }> = (props) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const ProfileDisplay: Component<ProfileDisplayProps> = (props) => {
|
const ProfileDisplay: Component<ProfileDisplayProps> = (props) => {
|
||||||
|
const i18n = useTranslation();
|
||||||
const { config, addMutedPubkey, removeMutedPubkey, isPubkeyMuted } = useConfig();
|
const { config, addMutedPubkey, removeMutedPubkey, isPubkeyMuted } = useConfig();
|
||||||
const commands = useCommands();
|
const commands = useCommands();
|
||||||
const myPubkey = usePubkey();
|
const myPubkey = usePubkey();
|
||||||
@@ -52,7 +54,7 @@ const ProfileDisplay: Component<ProfileDisplayProps> = (props) => {
|
|||||||
const [updatingContacts, setUpdatingContacts] = createSignal(false);
|
const [updatingContacts, setUpdatingContacts] = createSignal(false);
|
||||||
const [hoverFollowButton, setHoverFollowButton] = createSignal(false);
|
const [hoverFollowButton, setHoverFollowButton] = createSignal(false);
|
||||||
const [showFollowers, setShowFollowers] = 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 closeModal = () => setModal(null);
|
||||||
|
|
||||||
const { profile, query: profileQuery } = useProfile(() => ({
|
const { profile, query: profileQuery } = useProfile(() => ({
|
||||||
@@ -169,13 +171,13 @@ const ProfileDisplay: Component<ProfileDisplayProps> = (props) => {
|
|||||||
},
|
},
|
||||||
*/
|
*/
|
||||||
{
|
{
|
||||||
content: () => 'IDをコピー',
|
content: () => i18n()('profile.copyPubkey'),
|
||||||
onSelect: () => {
|
onSelect: () => {
|
||||||
navigator.clipboard.writeText(npub()).catch((err) => window.alert(err));
|
navigator.clipboard.writeText(npub()).catch((err) => window.alert(err));
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
content: () => (!isMuted() ? 'ミュート' : 'ミュート解除'),
|
content: () => (!isMuted() ? i18n()('profile.mute') : i18n()('profile.unmute')),
|
||||||
onSelect: () => {
|
onSelect: () => {
|
||||||
if (!isMuted()) {
|
if (!isMuted()) {
|
||||||
addMutedPubkey(props.pubkey);
|
addMutedPubkey(props.pubkey);
|
||||||
@@ -186,7 +188,8 @@ const ProfileDisplay: Component<ProfileDisplayProps> = (props) => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
when: () => props.pubkey === myPubkey(),
|
when: () => props.pubkey === myPubkey(),
|
||||||
content: () => (!following() ? '自分をフォロー' : '自分をフォロー解除'),
|
content: () =>
|
||||||
|
!following() ? i18n()('profile.followMyself') : i18n()('profile.unfollowMyself'),
|
||||||
onSelect: () => {
|
onSelect: () => {
|
||||||
if (!following()) {
|
if (!following()) {
|
||||||
follow();
|
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"
|
text-center font-bold text-primary hover:bg-primary hover:text-white sm:w-20"
|
||||||
onClick={() => showProfileEdit()}
|
onClick={() => showProfileEdit()}
|
||||||
>
|
>
|
||||||
編集
|
{i18n()('profile.editProfile')}
|
||||||
</button>
|
</button>
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={updateContactsMutation.isLoading || updatingContacts()}>
|
<Match when={updateContactsMutation.isLoading || updatingContacts()}>
|
||||||
<span class="rounded-full border border-primary px-4 py-2 text-primary sm:text-base">
|
<span class="rounded-full border border-primary px-4 py-2 text-primary sm:text-base">
|
||||||
更新中
|
{i18n()('profile.updating')}
|
||||||
</span>
|
</span>
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={myFollowingQuery.isLoading || myFollowingQuery.isFetching}>
|
<Match when={myFollowingQuery.isLoading || myFollowingQuery.isFetching}>
|
||||||
<span class="rounded-full border border-primary px-4 py-2 text-primary sm:text-base">
|
<span class="rounded-full border border-primary px-4 py-2 text-primary sm:text-base">
|
||||||
読み込み中
|
{i18n()('profile.loading')}
|
||||||
</span>
|
</span>
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={following()}>
|
<Match when={following()}>
|
||||||
@@ -265,8 +268,8 @@ const ProfileDisplay: Component<ProfileDisplayProps> = (props) => {
|
|||||||
onClick={() => unfollow()}
|
onClick={() => unfollow()}
|
||||||
disabled={updateContactsMutation.isLoading}
|
disabled={updateContactsMutation.isLoading}
|
||||||
>
|
>
|
||||||
<Show when={!hoverFollowButton()} fallback="フォロー解除">
|
<Show when={!hoverFollowButton()} fallback={i18n()('profile.unfollow')}>
|
||||||
フォロー中
|
{i18n()('profile.followingCurrently')}
|
||||||
</Show>
|
</Show>
|
||||||
</button>
|
</button>
|
||||||
</Match>
|
</Match>
|
||||||
@@ -277,7 +280,7 @@ const ProfileDisplay: Component<ProfileDisplayProps> = (props) => {
|
|||||||
onClick={() => follow()}
|
onClick={() => follow()}
|
||||||
disabled={updateContactsMutation.isLoading}
|
disabled={updateContactsMutation.isLoading}
|
||||||
>
|
>
|
||||||
フォロー
|
{i18n()('profile.follow')}
|
||||||
</button>
|
</button>
|
||||||
</Match>
|
</Match>
|
||||||
</Switch>
|
</Switch>
|
||||||
@@ -292,10 +295,10 @@ const ProfileDisplay: Component<ProfileDisplayProps> = (props) => {
|
|||||||
</div>
|
</div>
|
||||||
<Switch>
|
<Switch>
|
||||||
<Match when={userFollowingQuery.isLoading}>
|
<Match when={userFollowingQuery.isLoading}>
|
||||||
<div class="shrink-0 text-xs">読み込み中</div>
|
<div class="shrink-0 text-xs">{i18n()('profile.loading')}</div>
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={followed()}>
|
<Match when={followed()}>
|
||||||
<div class="shrink-0 text-xs">フォローされています</div>
|
<div class="shrink-0 text-xs">{i18n()('profile.followsYou')}</div>
|
||||||
</Match>
|
</Match>
|
||||||
</Switch>
|
</Switch>
|
||||||
</div>
|
</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 { Event as NostrEvent } from 'nostr-tools';
|
||||||
|
|
||||||
import useConfig from '@/core/useConfig';
|
import useConfig from '@/core/useConfig';
|
||||||
import { BatchedEventsTask, exec, registerTask } from '@/nostr/useBatchedEvents';
|
import { BatchedEventsTask, registerTask } from '@/nostr/useBatchedEvents';
|
||||||
import timeout from '@/utils/timeout';
|
import timeout from '@/utils/timeout';
|
||||||
|
|
||||||
export type UseRepostsProps = {
|
export type UseRepostsProps = {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { createSignal, onMount, Switch, Match, type Component } from 'solid-js';
|
|||||||
import { useNavigate } from '@solidjs/router';
|
import { useNavigate } from '@solidjs/router';
|
||||||
|
|
||||||
import usePersistStatus from '@/hooks/usePersistStatus';
|
import usePersistStatus from '@/hooks/usePersistStatus';
|
||||||
|
import { useTranslation } from '@/i18n/useTranslation';
|
||||||
import resolveAsset from '@/utils/resolveAsset';
|
import resolveAsset from '@/utils/resolveAsset';
|
||||||
|
|
||||||
type SignerStatus = 'checking' | 'available' | 'unavailable';
|
type SignerStatus = 'checking' | 'available' | 'unavailable';
|
||||||
@@ -31,6 +32,7 @@ const useSignerStatus = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const Hello: Component = () => {
|
const Hello: Component = () => {
|
||||||
|
const i18n = useTranslation();
|
||||||
const signerStatus = useSignerStatus();
|
const signerStatus = useSignerStatus();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { persistStatus, loggedIn } = usePersistStatus();
|
const { persistStatus, loggedIn } = usePersistStatus();
|
||||||
@@ -61,10 +63,10 @@ const Hello: Component = () => {
|
|||||||
<div class="rounded-md p-8 shadow-md">
|
<div class="rounded-md p-8 shadow-md">
|
||||||
<Switch>
|
<Switch>
|
||||||
<Match when={signerStatus() === 'checking'}>
|
<Match when={signerStatus() === 'checking'}>
|
||||||
<p>拡張機能のインストール状況を確認中です...</p>
|
<p>{i18n()('hello.signerChecking')}</p>
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={signerStatus() === 'unavailable'}>
|
<Match when={signerStatus() === 'unavailable'}>
|
||||||
<h2 class="font-bold">利用にはNIP-07に対応した拡張機能が必要です。</h2>
|
<h2 class="font-bold">{i18n()('hello.signerUnavailable')}</h2>
|
||||||
<p>
|
<p>
|
||||||
<br />
|
<br />
|
||||||
初めて利用する方も、他のクライアントをつかっている方も
|
初めて利用する方も、他のクライアントをつかっている方も
|
||||||
@@ -87,7 +89,7 @@ const Hello: Component = () => {
|
|||||||
class="rounded bg-rose-400 p-4 text-lg font-bold text-white hover:shadow-md"
|
class="rounded bg-rose-400 p-4 text-lg font-bold text-white hover:shadow-md"
|
||||||
onClick={handleLogin}
|
onClick={handleLogin}
|
||||||
>
|
>
|
||||||
NIP-07 拡張機能でログイン
|
{i18n()('hello.loginWithSigner')}
|
||||||
</button>
|
</button>
|
||||||
</Match>
|
</Match>
|
||||||
</Switch>
|
</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",
|
"target": "ESNext",
|
||||||
"module": "ESNext",
|
"module": "ESNext",
|
||||||
"moduleResolution": "node",
|
"moduleResolution": "node",
|
||||||
|
"resolveJsonModule": true,
|
||||||
"lib": [
|
"lib": [
|
||||||
"dom",
|
"dom",
|
||||||
"dom.iterable",
|
"dom.iterable",
|
||||||
|
|||||||
Reference in New Issue
Block a user