feat: i18n

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

68
package-lock.json generated
View File

@@ -20,6 +20,8 @@
"@types/lodash": "^4.14.195", "@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",

View File

@@ -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",

View File

@@ -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,6 +30,7 @@ const App: Component = () => {
}); });
return ( return (
<I18NextProvider i18next={i18next}>
<QueryClientProvider client={queryClient}> <QueryClientProvider client={queryClient}>
<Routes> <Routes>
<Route path="/hello" element={<Hello />} /> <Route path="/hello" element={<Hello />} />
@@ -32,6 +38,7 @@ const App: Component = () => {
<Route path="/*" element={<NotFound />} /> <Route path="/*" element={<NotFound />} />
</Routes> </Routes>
</QueryClientProvider> </QueryClientProvider>
</I18NextProvider>
); );
}; };

View File

@@ -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)}

View File

@@ -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)}

View File

@@ -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>

View File

@@ -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)}

View File

@@ -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)}

View File

@@ -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)}

View File

@@ -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)}

View File

@@ -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)}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>
); );

View File

@@ -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()}>

View File

@@ -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>

View File

@@ -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,29 +126,33 @@ 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 DateFormatConfig = () => {
const i18n = useTranslation();
const { config, setConfig } = useConfig();
const dateFormats: { const dateFormats: {
id: Config['dateFormat']; id: Config['dateFormat'];
name: string; name: string;
@@ -151,31 +160,28 @@ const dateFormats: {
}[] = [ }[] = [
{ {
id: 'relative', id: 'relative',
name: '相対表記', name: i18n()('config.display.relativeTimeNotation'),
example: '7秒前', example: i18n()('config.display.relativeTimeNotationExample'),
}, },
{ {
id: 'absolute-short', id: 'absolute-short',
name: '絶対表記 (短形式)', name: i18n()('config.display.absoluteTimeNotationShort'),
example: '昨日 23:55', example: i18n()('config.display.absoluteTimeNotationShortExample'),
}, },
{ {
id: 'absolute-long', id: 'absolute-long',
name: '絶対表記 (長形式)', name: i18n()('config.display.absoluteTimeNotationLong'),
example: '2020/11/8 21:02:53', example: i18n()('config.display.absoluteTimeNotationLongExample'),
}, },
]; ];
const DateFormatConfig = () => {
const { config, setConfig } = useConfig();
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) => (

View File

@@ -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
View File

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

View File

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

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

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

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

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

View File

@@ -4,7 +4,7 @@ import { createQuery, useQueryClient, type CreateQueryResult } from '@tanstack/s
import { Event as NostrEvent } from 'nostr-tools'; import { 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 = {

View File

@@ -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
View File

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

View File

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