Setup i18n

This commit is contained in:
AkiraFukushima 2023-11-04 15:32:37 +09:00
parent 5fd257dc7a
commit 0375e02d93
No known key found for this signature in database
GPG Key ID: B6E51BAC4DE1A957
11 changed files with 252 additions and 22 deletions

View File

@ -0,0 +1,25 @@
{
"timeline": {
"home": "Home",
"notifications": "Notifications",
"local": "Local",
"public": "Public",
"search": "Search",
"status": {
"boosted": "{user} boosted",
"poll": {
"refresh": "Refresh",
"vote": "Vote",
"peolpe": "{num} people",
"closed": "Closed"
}
}
},
"accounts": {
"new": {
"title": "Add account",
"sign_in": "Sign in",
"authorize": "Authorize"
}
}
}

View File

@ -18,6 +18,7 @@
"flowbite": "^2.0.0",
"flowbite-react": "^0.6.4",
"megalodon": "^9.1.1",
"react-intl": "^6.5.1",
"react-virtuoso": "^4.6.2"
},
"devDependencies": {

View File

@ -2,6 +2,7 @@ import { Label, Modal, TextInput, Button } from 'flowbite-react'
import generator, { MegalodonInterface, detector } from 'megalodon'
import { useState } from 'react'
import { db } from '@/db'
import { FormattedMessage } from 'react-intl'
type NewProps = {
opened: boolean
@ -63,7 +64,9 @@ export default function New(props: NewProps) {
return (
<>
<Modal dismissible={false} show={props.opened} onClose={close}>
<Modal.Header>Add account</Modal.Header>
<Modal.Header>
<FormattedMessage id="accounts.new.title" />
</Modal.Header>
<Modal.Body>
<form className="flex max-w-md flex-col gap-2">
{sns === null && (
@ -72,7 +75,9 @@ export default function New(props: NewProps) {
<Label htmlFor="domain" value="Domain" />
</div>
<TextInput id="domain" placeholder="mastodon.social" required type="text" />
<Button onClick={checkDomain}>Sign In</Button>{' '}
<Button onClick={checkDomain}>
<FormattedMessage id="accounts.new.sign_in" />
</Button>
</>
)}
{sns && (
@ -81,7 +86,9 @@ export default function New(props: NewProps) {
<Label htmlFor="authorization" value="Authorization Code" />
</div>
<TextInput id="authorization" required type="text" />
<Button onClick={authorize}>Authorize</Button>{' '}
<Button onClick={authorize}>
<FormattedMessage id="accounts.new.authorize" />
</Button>
</>
)}
</form>

View File

@ -2,6 +2,7 @@ import { Account, db } from '@/db'
import { CustomFlowbiteTheme, Flowbite, Sidebar } from 'flowbite-react'
import { useRouter } from 'next/router'
import { useEffect, useState } from 'react'
import { useIntl } from 'react-intl'
type LayoutProps = {
children: React.ReactNode
@ -21,6 +22,7 @@ const customTheme: CustomFlowbiteTheme = {
export default function Layout({ children }: LayoutProps) {
const router = useRouter()
const { formatMessage } = useIntl()
const [account, setAccount] = useState<Account | null>(null)
useEffect(() => {
@ -37,22 +39,22 @@ export default function Layout({ children }: LayoutProps) {
const pages = [
{
id: 'home',
title: 'Home',
title: formatMessage({ id: 'timeline.home' }),
path: `/accounts/${router.query.id}/home`
},
{
id: 'notifications',
title: 'Notifications',
title: formatMessage({ id: 'timeline.notifications' }),
path: `/accounts/${router.query.id}/notifications`
},
{
id: 'local',
title: 'Local',
title: formatMessage({ id: 'timeline.local' }),
path: `/accounts/${router.query.id}/local`
},
{
id: 'public',
title: 'Public',
title: formatMessage({ id: 'timeline.public' }),
path: `/accounts/${router.query.id}/public`
}
]

View File

@ -4,6 +4,7 @@ import { Entity, MegalodonInterface } from 'megalodon'
import { useEffect, useState } from 'react'
import { Virtuoso } from 'react-virtuoso'
import Status from './status/Status'
import { FormattedMessage, useIntl } from 'react-intl'
type Props = {
timeline: string
@ -12,6 +13,7 @@ type Props = {
}
export default function Timeline(props: Props) {
const [statuses, setStatuses] = useState<Array<Entity.Status>>([])
const { formatMessage } = useIntl()
useEffect(() => {
const f = async () => {
@ -65,10 +67,12 @@ export default function Timeline(props: Props) {
return (
<section className="h-full w-full">
<div className="w-full bg-blue-950 text-blue-100 p-2 flex justify-between">
<div className="text-lg font-bold">{props.timeline}</div>
<div className="text-lg font-bold">
<FormattedMessage id={`timeline.${props.timeline}`} />
</div>
<div className="w-64 text-xs">
<form>
<TextInput type="text" placeholder="search" disabled sizing="sm" />
<TextInput type="text" placeholder={formatMessage({ id: 'timeline.search' })} disabled sizing="sm" />
</form>
</div>
</div>

View File

@ -1,6 +1,7 @@
import dayjs from 'dayjs'
import { Progress, Button, Radio, Label, Checkbox } from 'flowbite-react'
import { Entity, MegalodonInterface } from 'megalodon'
import { FormattedMessage } from 'react-intl'
type Props = {
poll: Entity.Poll
@ -41,9 +42,11 @@ function SimplePoll(props: Props) {
))}
<div className="flex gap-2 items-center mt-2">
<Button outline={true} size="xs" onClick={vote}>
Vote
<FormattedMessage id="timeline.status.poll.vote" />
</Button>
<div>{props.poll.votes_count} people</div>
<div>
<FormattedMessage id="timeline.status.poll.people" values={{ num: props.poll.votes_count }} />
</div>
<div>
<time dateTime={props.poll.expires_at}>{dayjs(props.poll.expires_at).format('YYYY-MM-DD HH:mm:ss')}</time>
</div>
@ -77,9 +80,11 @@ function MultiplePoll(props: Props) {
))}
<div className="flex gap-2 items-center mt-2">
<Button outline={true} size="xs" onClick={vote}>
Vote
<FormattedMessage id="timeline.status.poll.vote" />
</Button>
<div>{props.poll.votes_count} people</div>
<div>
<FormattedMessage id="timeline.status.poll.people" values={{ num: props.poll.votes_count }} />
</div>
<div>
<time dateTime={props.poll.expires_at}>{dayjs(props.poll.expires_at).format('YYYY-MM-DD HH:mm:ss')}</time>
</div>
@ -100,11 +105,15 @@ function PollResult(props: Props) {
))}
<div className="flex gap-2 items-center mt-2">
<Button outline={true} size="xs" onClick={props.onRefresh}>
Refresh
<FormattedMessage id="timeline.status.poll.refresh" />
</Button>
<div>{props.poll.votes_count} people</div>
<div>
<FormattedMessage id="timeline.status.poll.people" values={{ num: props.poll.votes_count }} />
</div>
{props.poll.expired ? (
<div>Closed</div>
<div>
<FormattedMessage id="timeline.status.poll.closed" />
</div>
) : (
<div>
<time dateTime={props.poll.expires_at}>{dayjs(props.poll.expires_at).format('YYYY-MM-DD HH:mm:ss')}</time>

View File

@ -6,6 +6,7 @@ import Media from './Media'
import emojify from '@/utils/emojify'
import Card from './Card'
import Poll from './Poll'
import { FormattedMessage } from 'react-intl'
type Props = {
status: Entity.Status
@ -66,7 +67,9 @@ const rebloggedHeader = (status: Entity.Status) => {
<div className="grid justify-items-end pr-2" style={{ width: '56px' }}>
<Avatar img={status.account.avatar} size="xs" />
</div>
<div style={{ width: 'calc(100% - 56px)' }}>{status.account.username} boosted</div>
<div style={{ width: 'calc(100% - 56px)' }}>
<FormattedMessage id="timeline.status.boosted" values={{ user: status.account.username }} />
</div>
</div>
)
} else {

View File

@ -2,13 +2,16 @@ import type { AppProps } from 'next/app'
import '../app.css'
import AccountLayout from '@/components/layouts/account'
import TimelineLayout from '@/components/layouts/timelines'
import { IntlProviderWrapper } from '@/utils/i18n'
export default function MyApp({ Component, pageProps }: AppProps) {
return (
<AccountLayout>
<TimelineLayout>
<Component {...pageProps} />
</TimelineLayout>
</AccountLayout>
<IntlProviderWrapper>
<AccountLayout>
<TimelineLayout>
<Component {...pageProps} />
</TimelineLayout>
</AccountLayout>
</IntlProviderWrapper>
)
}

View File

@ -0,0 +1,14 @@
export function flattenMessages(nestedMessages: { [key: string]: any }, prefix = ''): { [key: string]: string } {
return Object.keys(nestedMessages).reduce((messages, key) => {
const value = nestedMessages[key]
const prefixedKey = prefix ? `${prefix}.${key}` : key
if (typeof value === 'string') {
messages[prefixedKey] = value
} else {
Object.assign(messages, flattenMessages(value, prefixedKey))
}
return messages
}, {})
}

37
renderer/utils/i18n.tsx Normal file
View File

@ -0,0 +1,37 @@
import en from '../../locales/en/translation.json'
import { flattenMessages } from './flattenMessage'
import { createContext, useState } from 'react'
import { IntlProvider } from 'react-intl'
export type localeType = 'en' | 'ja' | 'it' | 'pt-BR' | 'fr'
type Props = {
children: React.ReactNode
}
interface Lang {
switchLang(lang: string): void
}
export const Context = createContext<Lang>({} as Lang)
export const IntlProviderWrapper: React.FC<Props> = props => {
const langs = [{ locale: 'en', messages: flattenMessages(en) }]
const [lang, setLang] = useState(langs[0])
const switchLang = (locale: string) => {
const changeLang = langs.find(lang => lang.locale === locale)
if (changeLang == null) {
return
}
setLang(changeLang)
}
return (
<Context.Provider value={{ switchLang }}>
<IntlProvider {...lang} defaultLocale="en" fallbackOnEmptyString={true}>
{props.children}
</IntlProvider>
</Context.Provider>
)
}

125
yarn.lock
View File

@ -1106,6 +1106,76 @@
resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.1.6.tgz#22958c042e10b67463997bd6ea7115fe28cbcaf9"
integrity sha512-OfX7E2oUDYxtBvsuS4e/jSn4Q9Qb6DzgeYtsAdkPZ47znpoNsMgZw0+tVijiv3uGNR6dgNlty6r9rzIzHjtd/A==
"@formatjs/ecma402-abstract@1.17.2":
version "1.17.2"
resolved "https://registry.yarnpkg.com/@formatjs/ecma402-abstract/-/ecma402-abstract-1.17.2.tgz#d197c6e26b9fd96ff7ba3b3a0cc2f25f1f2dcac3"
integrity sha512-k2mTh0m+IV1HRdU0xXM617tSQTi53tVR2muvYOsBeYcUgEAyxV1FOC7Qj279th3fBVQ+Dj6muvNJZcHSPNdbKg==
dependencies:
"@formatjs/intl-localematcher" "0.4.2"
tslib "^2.4.0"
"@formatjs/fast-memoize@2.2.0":
version "2.2.0"
resolved "https://registry.yarnpkg.com/@formatjs/fast-memoize/-/fast-memoize-2.2.0.tgz#33bd616d2e486c3e8ef4e68c99648c196887802b"
integrity sha512-hnk/nY8FyrL5YxwP9e4r9dqeM6cAbo8PeU9UjyXojZMNvVad2Z06FAVHyR3Ecw6fza+0GH7vdJgiKIVXTMbSBA==
dependencies:
tslib "^2.4.0"
"@formatjs/icu-messageformat-parser@2.7.0":
version "2.7.0"
resolved "https://registry.yarnpkg.com/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.7.0.tgz#9b13f2710a3b4efddfeb544480f684f27a53483b"
integrity sha512-7uqC4C2RqOaBQtcjqXsSpGRYVn+ckjhNga5T/otFh6MgxRrCJQqvjfbrGLpX1Lcbxdm5WH3Z2WZqt1+Tm/cn/Q==
dependencies:
"@formatjs/ecma402-abstract" "1.17.2"
"@formatjs/icu-skeleton-parser" "1.6.2"
tslib "^2.4.0"
"@formatjs/icu-skeleton-parser@1.6.2":
version "1.6.2"
resolved "https://registry.yarnpkg.com/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.6.2.tgz#00303034dc08583973c8aa67b96534c49c0bad8d"
integrity sha512-VtB9Slo4ZL6QgtDFJ8Injvscf0xiDd4bIV93SOJTBjUF4xe2nAWOoSjLEtqIG+hlIs1sNrVKAaFo3nuTI4r5ZA==
dependencies:
"@formatjs/ecma402-abstract" "1.17.2"
tslib "^2.4.0"
"@formatjs/intl-displaynames@6.6.1":
version "6.6.1"
resolved "https://registry.yarnpkg.com/@formatjs/intl-displaynames/-/intl-displaynames-6.6.1.tgz#2099dbd0d3dffba3176d7b470c73bdd578850d76"
integrity sha512-TIPaDu0SlwJUXlIyeSL9052jrUC4QviLnvUEJ53Ldc3Q4nZJnT2wD8NHIroTOYX9lgp5m3BeTlhpRcsnuExDkA==
dependencies:
"@formatjs/ecma402-abstract" "1.17.2"
"@formatjs/intl-localematcher" "0.4.2"
tslib "^2.4.0"
"@formatjs/intl-listformat@7.5.0":
version "7.5.0"
resolved "https://registry.yarnpkg.com/@formatjs/intl-listformat/-/intl-listformat-7.5.0.tgz#dbccf2e0f07792aa1c273702796bdad061dc27ae"
integrity sha512-n9FsXGl1T2ZbX6wSyrzCDJHrbJR0YJ9ZNsAqUvHXfbY3nsOmGnSTf5+bkuIp1Xiywu7m1X1Pfm/Ngp/yK1H84A==
dependencies:
"@formatjs/ecma402-abstract" "1.17.2"
"@formatjs/intl-localematcher" "0.4.2"
tslib "^2.4.0"
"@formatjs/intl-localematcher@0.4.2":
version "0.4.2"
resolved "https://registry.yarnpkg.com/@formatjs/intl-localematcher/-/intl-localematcher-0.4.2.tgz#7e6e596dbaf2f0c5a7c22da5a01d5c55f4c37e9a"
integrity sha512-BGdtJFmaNJy5An/Zan4OId/yR9Ih1OojFjcduX/xOvq798OgWSyDtd6Qd5jqJXwJs1ipe4Fxu9+cshic5Ox2tA==
dependencies:
tslib "^2.4.0"
"@formatjs/intl@2.9.5":
version "2.9.5"
resolved "https://registry.yarnpkg.com/@formatjs/intl/-/intl-2.9.5.tgz#30087e97db940038ede523439c2fb2bdf84989dd"
integrity sha512-WEdEv8Jf2nKBErTK4MJ2xCesUJVHH9iunXzfHzZo4tnn2NSj48g04FNH9w17XDpEbj9KEM39fLkwBz7ay/ErPQ==
dependencies:
"@formatjs/ecma402-abstract" "1.17.2"
"@formatjs/fast-memoize" "2.2.0"
"@formatjs/icu-messageformat-parser" "2.7.0"
"@formatjs/intl-displaynames" "6.6.1"
"@formatjs/intl-listformat" "7.5.0"
intl-messageformat "10.5.4"
tslib "^2.4.0"
"@jridgewell/gen-mapping@^0.3.0", "@jridgewell/gen-mapping@^0.3.2":
version "0.3.3"
resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz#7e02e6eb5df901aaedb08514203b096614024098"
@ -1328,6 +1398,14 @@
dependencies:
"@types/node" "*"
"@types/hoist-non-react-statics@^3.3.1":
version "3.3.4"
resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.4.tgz#cc477ce0283bb9d19ea0cbfa2941fe2c8493a1be"
integrity sha512-ZchYkbieA+7tnxwX/SCBySx9WwvWR8TaP5tb2jRAzwvLb/rWchGw3v0w3pqUbUvj0GCwW2Xz/AVPSk6kUGctXQ==
dependencies:
"@types/react" "*"
hoist-non-react-statics "^3.3.0"
"@types/http-cache-semantics@*":
version "4.0.3"
resolved "https://registry.yarnpkg.com/@types/http-cache-semantics/-/http-cache-semantics-4.0.3.tgz#a3ff232bf7d5c55f38e4e45693eda2ebb545794d"
@ -1377,6 +1455,15 @@
resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.9.tgz#b6f785caa7ea1fe4414d9df42ee0ab67f23d8a6d"
integrity sha512-n1yyPsugYNSmHgxDFjicaI2+gCNjsBck8UX9kuofAKlc0h1bL+20oSF72KeNaW2DUlesbEVCFgyV2dPGTiY42g==
"@types/react@*", "@types/react@16 || 17 || 18":
version "18.2.34"
resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.34.tgz#aed20f19473721ba328feb99d1ec3307ebc1a8dd"
integrity sha512-U6eW/alrRk37FU/MS2RYMjx0Va2JGIVXELTODaTIYgvWGCV4Y4TfTUzG8DdmpDNIT0Xpj/R7GfyHOJJrDttcvg==
dependencies:
"@types/prop-types" "*"
"@types/scheduler" "*"
csstype "^3.0.2"
"@types/react@^18.0.26":
version "18.2.33"
resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.33.tgz#055356243dc4350a9ee6c6a2c07c5cae12e38877"
@ -2814,6 +2901,13 @@ hasown@^2.0.0:
dependencies:
function-bind "^1.1.2"
hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.2:
version "3.3.2"
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45"
integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==
dependencies:
react-is "^16.7.0"
hosted-git-info@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-4.1.0.tgz#827b82867e9ff1c8d0c4d9d53880397d2c86d224"
@ -2889,6 +2983,16 @@ inherits@2:
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
intl-messageformat@10.5.4:
version "10.5.4"
resolved "https://registry.yarnpkg.com/intl-messageformat/-/intl-messageformat-10.5.4.tgz#7b212b083f1b354d7e282518e78057e025134af9"
integrity sha512-z+hrFdiJ/heRYlzegrdFYqU1m/KOMOVMqNilIArj+PbsuU8TNE7v4TWdQgSoxlxbT4AcZH3Op3/Fu15QTp+W1w==
dependencies:
"@formatjs/ecma402-abstract" "1.17.2"
"@formatjs/fast-memoize" "2.2.0"
"@formatjs/icu-messageformat-parser" "2.7.0"
tslib "^2.4.0"
is-binary-path@~2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09"
@ -3664,6 +3768,27 @@ react-indiana-drag-scroll@^2.2.0:
debounce "^1.2.0"
easy-bem "^1.1.1"
react-intl@^6.5.1:
version "6.5.1"
resolved "https://registry.yarnpkg.com/react-intl/-/react-intl-6.5.1.tgz#c44f67798e25b2778b2091563e004f54e8dc911b"
integrity sha512-mKxfH7GV5P4dJcQmbq/xU8FVBl//xRudXgS5r1Gt62NEr+T8pnzQZZ2th1jP5BQ+Ne/3kS3uYpFcynj5KyXVhg==
dependencies:
"@formatjs/ecma402-abstract" "1.17.2"
"@formatjs/icu-messageformat-parser" "2.7.0"
"@formatjs/intl" "2.9.5"
"@formatjs/intl-displaynames" "6.6.1"
"@formatjs/intl-listformat" "7.5.0"
"@types/hoist-non-react-statics" "^3.3.1"
"@types/react" "16 || 17 || 18"
hoist-non-react-statics "^3.3.2"
intl-messageformat "10.5.4"
tslib "^2.4.0"
react-is@^16.7.0:
version "16.13.1"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
react-virtuoso@^4.6.2:
version "4.6.2"
resolved "https://registry.yarnpkg.com/react-virtuoso/-/react-virtuoso-4.6.2.tgz#74b59ebe3260e1f73e92340ffec84a6853285a12"