Merge pull request #359 from myl7/i18n

This commit is contained in:
Spencer Woo
2022-02-08 22:14:53 +08:00
committed by GitHub
32 changed files with 1425 additions and 194 deletions
+6 -3
View File
@@ -3,6 +3,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import Image from 'next/image'
import { useRouter } from 'next/router'
import { FC, useState } from 'react'
import { useTranslation } from 'next-i18next'
import { matchProtectedRoute } from '../utils/protectedRouteHandler'
import useLocalStorage from '../utils/useLocalStorage'
@@ -14,16 +15,18 @@ const Auth: FC<{ redirect: string }> = ({ redirect }) => {
const [token, setToken] = useState('')
const [_, setPersistedToken] = useLocalStorage(authTokenPath, '')
const { t } = useTranslation()
return (
<div className="md:my-10 flex flex-col max-w-sm mx-auto space-y-4">
<div className="md:w-5/6 w-3/4 mx-auto">
<Image src={'/images/fabulous-wapmire-weekdays.png'} alt="authenticate" width={912} height={912} priority />
</div>
<div className="dark:text-gray-100 text-lg font-bold text-gray-900">Enter Password</div>
<div className="dark:text-gray-100 text-lg font-bold text-gray-900">{t('Enter Password')}</div>
<p className="text-sm text-gray-500 font-medium">
This route (the folder itself and the files inside) is password protected. If you know the password, please
enter it below.
{t('This route (the folder itself and the files inside) is password protected. ') +
t('If you know the password, please enter it below.')}
</p>
<div className="flex items-center space-x-2">
+4 -1
View File
@@ -2,13 +2,16 @@ import type { ParsedUrlQuery } from 'querystring'
import Link from 'next/link'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { useTranslation } from 'next-i18next'
const HomeCrumb = () => {
const { t } = useTranslation()
return (
<Link href="/">
<a>
<FontAwesomeIcon className="h-3 w-3" icon={['far', 'flag']} />
<span className="ml-2 font-medium">Home</span>
<span className="ml-2 font-medium">{t('Home')}</span>
</a>
</Link>
)
+10 -7
View File
@@ -3,6 +3,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { IconProp } from '@fortawesome/fontawesome-svg-core'
import toast from 'react-hot-toast'
import { useClipboard } from 'use-clipboard-copy'
import { useTranslation } from 'next-i18next'
import Image from 'next/image'
import { useRouter } from 'next/router'
@@ -64,31 +65,33 @@ const DownloadButtonGroup: React.FC<{ downloadUrl: string }> = ({ downloadUrl })
const { asPath } = useRouter()
const clipboard = useClipboard()
const { t } = useTranslation()
return (
<div className="flex flex-wrap justify-center gap-2">
<DownloadButton
onClickCallback={() => window.open(downloadUrl)}
btnColor="blue"
btnText="Download"
btnText={t('Download')}
btnIcon="file-download"
btnTitle="Download the file directly through OneDrive"
btnTitle={t('Download the file directly through OneDrive')}
/>
{/* <DownloadButton
onClickCallback={() => window.open(`/api/proxy?url=${encodeURIComponent(downloadUrl)}`)}
btnColor="teal"
btnText="Proxy download"
btnText={t('Proxy download')}
btnIcon="download"
btnTitle="Download the file with the stream proxied through Vercel Serverless"
btnTitle={t('Download the file with the stream proxied through Vercel Serverless')}
/> */}
<DownloadButton
onClickCallback={() => {
clipboard.copy(`${getBaseUrl()}/api?path=${getReadablePath(asPath)}&raw=true`)
toast.success('Copied direct link to clipboard.')
toast.success(t('Copied direct link to clipboard.'))
}}
btnColor="pink"
btnText="Copy direct link"
btnText={t('Copy direct link')}
btnIcon="copy"
btnTitle="Copy the permalink to the file to the clipboard"
btnTitle={t('Copy the permalink to the file to the clipboard')}
/>
</div>
)
+26 -11
View File
@@ -7,6 +7,7 @@ import { FC, MouseEventHandler, SetStateAction, useEffect, useRef, useState } fr
import dynamic from 'next/dynamic'
import { useRouter } from 'next/router'
import { useTranslation } from 'next-i18next'
import useLocalStorage from '../utils/useLocalStorage'
import { getPreviewType, preview } from '../utils/getPreviewType'
@@ -146,6 +147,8 @@ const FileListing: FC<{ query?: ParsedUrlQuery }> = ({ query }) => {
const router = useRouter()
const [layout, _] = useLocalStorage('preferredLayout', layouts[0])
const { t } = useTranslation()
const path = queryToPath(query)
const { data, error, size, setSize } = useProtectedSWRInfinite(path)
@@ -166,7 +169,7 @@ const FileListing: FC<{ query?: ParsedUrlQuery }> = ({ query }) => {
if (!data) {
return (
<PreviewContainer>
<Loading loadingText="Loading ..." />
<Loading loadingText={t('Loading ...')} />
</PreviewContainer>
)
}
@@ -240,13 +243,13 @@ const FileListing: FC<{ query?: ParsedUrlQuery }> = ({ query }) => {
downloadMultipleFiles({ toastId, router, files, folder })
.then(() => {
setTotalGenerating(false)
toast.success('Finished downloading selected files.', {
toast.success(t('Finished downloading selected files.'), {
id: toastId,
})
})
.catch(() => {
setTotalGenerating(false)
toast.error('Failed to download selected files.', { id: toastId })
toast.error(t('Failed to download selected files.'), { id: toastId })
})
}
}
@@ -256,7 +259,13 @@ const FileListing: FC<{ query?: ParsedUrlQuery }> = ({ query }) => {
const files = (async function* () {
for await (const { meta: c, path: p, isFolder, error } of traverseFolder(path)) {
if (error) {
toast.error(`Failed to download folder ${p}: ${error.status} ${error.message} Skipped it to continue.`)
toast.error(
t('Failed to download folder {{path}}: {{status}} {{message}} Skipped it to continue.', {
path: p,
status: error.status,
message: error.message
})
)
continue
}
yield {
@@ -280,11 +289,11 @@ const FileListing: FC<{ query?: ParsedUrlQuery }> = ({ query }) => {
})
.then(() => {
setFolderGenerating({ ...folderGenerating, [id]: false })
toast.success('Finished downloading folder.', { id: toastId })
toast.success(t('Finished downloading folder.'), { id: toastId })
})
.catch(() => {
setFolderGenerating({ ...folderGenerating, [id]: false })
toast.error('Failed to download folder.', { id: toastId })
toast.error(t('Failed to download folder.'), { id: toastId })
})
}
@@ -312,7 +321,13 @@ const FileListing: FC<{ query?: ParsedUrlQuery }> = ({ query }) => {
{!onlyOnePage && (
<div className="rounded-b bg-white dark:bg-gray-900 dark:text-gray-100">
<div className="border-b border-gray-200 p-3 text-center font-mono text-sm text-gray-400 dark:border-gray-700">
- showing {size} page{size > 1 ? 's' : ''} of {isLoadingMore ? '...' : folderChildren.length} files -
{t('- showing {{count}} page(s) ', {
count: size,
totalFileNum: isLoadingMore ? '...' : folderChildren.length
}) +
(isLoadingMore
? t('of {{count}} file(s) -', { count: folderChildren.length, context: 'loading' })
: t('of {{count}} file(s) -', { count: folderChildren.length, context: 'loaded' }))}
</div>
<button
className={`flex w-full items-center justify-center space-x-2 p-3 disabled:cursor-not-allowed ${
@@ -324,13 +339,13 @@ const FileListing: FC<{ query?: ParsedUrlQuery }> = ({ query }) => {
{isLoadingMore ? (
<>
<LoadingIcon className="inline-block h-4 w-4 animate-spin" />
<span>Loading ...</span>{' '}
<span>{t('Loading ...')}</span>{' '}
</>
) : isReachingEnd ? (
<span>No more files</span>
<span>{t('No more files')}</span>
) : (
<>
<span>Load more</span>
<span>{t('Load more')}</span>
<FontAwesomeIcon icon="chevron-circle-down" />
</>
)}
@@ -397,7 +412,7 @@ const FileListing: FC<{ query?: ParsedUrlQuery }> = ({ query }) => {
return (
<PreviewContainer>
<FourOhFour errorMsg={`Cannot preview ${path}`} />
<FourOhFour errorMsg={t('Cannot preview {{path}}', { path })} />
</PreviewContainer>
)
}
+15 -12
View File
@@ -4,6 +4,7 @@ import Link from 'next/link'
import { useState } from 'react'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { useClipboard } from 'use-clipboard-copy'
import { useTranslation } from 'next-i18next'
import { getBaseUrl } from '../utils/getBaseUrl'
import { formatModifiedDateTime } from '../utils/fileDetails'
@@ -66,22 +67,24 @@ const FolderGridLayout = ({
}) => {
const clipboard = useClipboard()
const { t } = useTranslation()
return (
<div className="rounded bg-white dark:bg-gray-900 dark:text-gray-100">
<div className="flex items-center border-b border-gray-900/10 px-3 text-xs font-bold uppercase tracking-widest text-gray-600 dark:border-gray-500/30 dark:text-gray-400">
<div className="flex-1">{folderChildren.length} items</div>
<div className="flex-1">{t('{{count}} item(s)', { count: folderChildren.length })}</div>
<div className="flex p-1.5 text-gray-700 dark:text-gray-400">
<Checkbox
checked={totalSelected}
onChange={toggleTotalSelected}
indeterminate={true}
title={'Select all files'}
title={t('Select all files')}
/>
{totalGenerating ? (
<Downloading title="Downloading selected files, refresh page to cancel" />
<Downloading title={t('Downloading selected files, refresh page to cancel')} />
) : (
<button
title="Download selected files"
title={t('Download selected files')}
className="cursor-pointer rounded p-1.5 hover:bg-gray-300 disabled:cursor-not-allowed disabled:text-gray-400 disabled:hover:bg-white dark:hover:bg-gray-600 disabled:dark:text-gray-600 disabled:hover:dark:bg-gray-900"
disabled={totalSelected === 0}
onClick={handleSelectedDownload}
@@ -102,22 +105,22 @@ const FolderGridLayout = ({
{c.folder ? (
<div>
<span
title="Copy folder permalink"
title={t('Copy folder permalink')}
className="cursor-pointer rounded px-1.5 py-1 hover:bg-gray-300 dark:hover:bg-gray-600"
onClick={() => {
clipboard.copy(
`${getBaseUrl()}${getReadablePath(`${path === '/' ? '' : path}/${encodeURIComponent(c.name)}`)}`
)
toast('Copied folder permalink.', { icon: '👌' })
toast(t('Copied folder permalink.'), { icon: '👌' })
}}
>
<FontAwesomeIcon icon={['far', 'copy']} />
</span>
{folderGenerating[c.id] ? (
<Downloading title="Downloading folder, refresh page to cancel" />
<Downloading title={t('Downloading folder, refresh page to cancel')} />
) : (
<span
title="Download folder"
title={t('Download folder')}
className="cursor-pointer rounded px-1.5 py-1 hover:bg-gray-300 dark:hover:bg-gray-600"
onClick={() => {
const p = `${path === '/' ? '' : path}/${encodeURIComponent(c.name)}`
@@ -131,7 +134,7 @@ const FolderGridLayout = ({
) : (
<div>
<span
title="Copy raw file permalink"
title={t('Copy raw file permalink')}
className="cursor-pointer rounded px-1.5 py-1 hover:bg-gray-300 dark:hover:bg-gray-600"
onClick={() => {
clipboard.copy(
@@ -139,13 +142,13 @@ const FolderGridLayout = ({
`${path === '/' ? '' : path}/${encodeURIComponent(c.name)}`
)}&raw=true`
)
toast.success('Copied raw file permalink.')
toast.success(t('Copied raw file permalink.'))
}}
>
<FontAwesomeIcon icon={['far', 'copy']} />
</span>
<a
title="Download file"
title={t('Download file')}
className="cursor-pointer rounded px-1.5 py-1 hover:bg-gray-300 dark:hover:bg-gray-600"
href={c['@microsoft.graph.downloadUrl']}
>
@@ -164,7 +167,7 @@ const FolderGridLayout = ({
<Checkbox
checked={selected[c.id] ? 2 : 0}
onChange={() => toggleItemSelected(c.id)}
title="Select file"
title={t('Select file')}
/>
)}
</div>
+18 -15
View File
@@ -4,6 +4,7 @@ import Link from 'next/link'
import { FC } from 'react'
import { useClipboard } from 'use-clipboard-copy'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { useTranslation } from 'next-i18next'
import { getBaseUrl } from '../utils/getBaseUrl'
import { humanFileSize, formatModifiedDateTime } from '../utils/fileDetails'
@@ -45,20 +46,22 @@ const FolderListLayout = ({
}) => {
const clipboard = useClipboard()
const { t } = useTranslation()
return (
<div className="rounded bg-white dark:bg-gray-900 dark:text-gray-100">
<div className="grid grid-cols-12 items-center space-x-2 border-b border-gray-900/10 px-3 dark:border-gray-500/30">
<div className="col-span-12 py-2 text-xs font-bold uppercase tracking-widest text-gray-600 dark:text-gray-300 md:col-span-6">
Name
{t('Name')}
</div>
<div className="col-span-3 hidden text-xs font-bold uppercase tracking-widest text-gray-600 dark:text-gray-300 md:block">
Last Modified
{t('Last Modified')}
</div>
<div className="hidden text-xs font-bold uppercase tracking-widest text-gray-600 dark:text-gray-300 md:block">
Size
{t('Size')}
</div>
<div className="hidden text-xs font-bold uppercase tracking-widest text-gray-600 dark:text-gray-300 md:block">
Actions
{t('Actions')}
</div>
<div className="hidden text-xs font-bold uppercase tracking-widest text-gray-600 dark:text-gray-300 md:block">
<div className="hidden p-1.5 text-gray-700 dark:text-gray-400 md:flex">
@@ -66,13 +69,13 @@ const FolderListLayout = ({
checked={totalSelected}
onChange={toggleTotalSelected}
indeterminate={true}
title={'Select files'}
title={t('Select files')}
/>
{totalGenerating ? (
<Downloading title="Downloading selected files, refresh page to cancel" />
<Downloading title={t('Downloading selected files, refresh page to cancel')} />
) : (
<button
title="Download selected files"
title={t('Download selected files')}
className="cursor-pointer rounded p-1.5 hover:bg-gray-300 disabled:cursor-not-allowed disabled:text-gray-400 disabled:hover:bg-white dark:hover:bg-gray-600 disabled:dark:text-gray-600 disabled:hover:dark:bg-gray-900"
disabled={totalSelected === 0}
onClick={handleSelectedDownload}
@@ -98,22 +101,22 @@ const FolderListLayout = ({
{c.folder ? (
<div className="hidden p-1.5 text-gray-700 dark:text-gray-400 md:flex">
<span
title="Copy folder permalink"
title={t('Copy folder permalink')}
className="cursor-pointer rounded px-1.5 py-1 hover:bg-gray-300 dark:hover:bg-gray-600"
onClick={() => {
clipboard.copy(
`${getBaseUrl()}${getReadablePath(`${path === '/' ? '' : path}/${encodeURIComponent(c.name)}`)}`
)
toast('Copied folder permalink.', { icon: '👌' })
toast(t('Copied folder permalink.'), { icon: '👌' })
}}
>
<FontAwesomeIcon icon={['far', 'copy']} />
</span>
{folderGenerating[c.id] ? (
<Downloading title="Downloading folder, refresh page to cancel" />
<Downloading title={t('Downloading folder, refresh page to cancel')} />
) : (
<span
title="Download folder"
title={t('Download folder')}
className="cursor-pointer rounded px-1.5 py-1 hover:bg-gray-300 dark:hover:bg-gray-600"
onClick={() => {
const p = `${path === '/' ? '' : path}/${encodeURIComponent(c.name)}`
@@ -127,7 +130,7 @@ const FolderListLayout = ({
) : (
<div className="hidden p-1.5 text-gray-700 dark:text-gray-400 md:flex">
<span
title="Copy raw file permalink"
title={t('Copy raw file permalink')}
className="cursor-pointer rounded px-1.5 py-1 hover:bg-gray-300 dark:hover:bg-gray-600"
onClick={() => {
clipboard.copy(
@@ -135,13 +138,13 @@ const FolderListLayout = ({
`${path === '/' ? '' : path}/${encodeURIComponent(c.name)}`
)}&raw=true`
)
toast.success('Copied raw file permalink.')
toast.success(t('Copied raw file permalink.'))
}}
>
<FontAwesomeIcon icon={['far', 'copy']} />
</span>
<a
title="Download file"
title={t('Download file')}
className="cursor-pointer rounded px-1.5 py-1 hover:bg-gray-300 dark:hover:bg-gray-600"
href={c['@microsoft.graph.downloadUrl']}
>
@@ -154,7 +157,7 @@ const FolderListLayout = ({
<Checkbox
checked={selected[c.id] ? 2 : 0}
onChange={() => toggleItemSelected(c.id)}
title="Select file"
title={t('Select file')}
/>
)}
</div>
+21 -13
View File
@@ -1,4 +1,5 @@
import Image from 'next/image'
import { Trans } from 'next-i18next'
const FourOhFour: React.FC<{ errorMsg: string }> = ({ errorMsg }) => {
return (
@@ -8,24 +9,31 @@ const FourOhFour: React.FC<{ errorMsg: string }> = ({ errorMsg }) => {
</div>
<div className="mt-6 text-gray-500 max-w-xl mx-auto">
<div className="text-xl font-bold mb-8">
Oops, that&apos;s a <span className="underline decoration-wavy decoration-red-500">four-oh-four</span>.
<Trans>
{/* eslint-disable-next-line react/no-unescaped-entities */}
Oops, that's a <span className="underline decoration-wavy decoration-red-500">four-oh-four</span>.
</Trans>
</div>
<div className="font-mono border border-gray-400/20 rounded p-2 mb-4 text-xs bg-gray-50 dark:bg-gray-800">
{errorMsg}
</div>
<div className="text-sm">
Press{' '}
<kbd className="border-opacity-20 font-mono text-xs p-1 bg-gray-100 dark:bg-gray-800 border rounded">F12</kbd>{' '}
and open devtools for more details, or seek help at{' '}
<a
className="text-blue-600 hover:text-blue-700 hover:underline"
href="https://github.com/spencerwooo/onedrive-vercel-index/discussions"
target="_blank"
rel="noopener noreferrer"
>
onedrive-vercel-index discussions
</a>
.
<Trans>
Press{' '}
<kbd className="border-opacity-20 font-mono text-xs p-1 bg-gray-100 dark:bg-gray-800 border rounded">
F12
</kbd>{' '}
and open devtools for more details, or seek help at{' '}
<a
className="text-blue-600 hover:text-blue-700 hover:underline"
href="https://github.com/spencerwooo/onedrive-vercel-index/discussions"
target="_blank"
rel="noopener noreferrer"
>
onedrive-vercel-index discussions
</a>
.
</Trans>
</div>
</div>
</div>
+5 -2
View File
@@ -1,6 +1,7 @@
import { NextRouter } from 'next/router'
import toast from 'react-hot-toast'
import JSZip from 'jszip'
import { useTranslation } from 'next-i18next'
import { fetcher } from '../utils/fetchWithSWR'
import { getStoredToken } from '../utils/protectedRouteHandler'
@@ -12,10 +13,12 @@ import { getStoredToken } from '../utils/protectedRouteHandler'
* @returns Toast component with loading progress
*/
export function DownloadingToast(router: NextRouter, progress?: string) {
const { t } = useTranslation()
return (
<div className="flex items-center space-x-2">
<div className="w-56">
<span>Downloading {progress ? `${progress}%` : 'selected files...'}</span>
<span>{progress ? t('Downloading {{progress}}%', { progress }) : t('Downloading selected files...')}</span>
<div className="relative mt-2">
<div className="overflow-hidden h-1 flex rounded bg-gray-100">
@@ -27,7 +30,7 @@ export function DownloadingToast(router: NextRouter, progress?: string) {
className="p-2 rounded bg-red-500 hover:bg-red-400 text-white focus:outline-none focus:ring focus:ring-red-300"
onClick={() => router.reload()}
>
Cancel
{t('Cancel')}
</button>
</div>
)
+19 -10
View File
@@ -8,6 +8,7 @@ import Link from 'next/link'
import Image from 'next/image'
import { useRouter } from 'next/router'
import { Fragment, useEffect, useState } from 'react'
import { useTranslation } from 'next-i18next'
import siteConfig from '../config/site.config'
import SearchModal from './SearchModal'
@@ -40,6 +41,8 @@ const Navbar = () => {
setTokenPresent(storedToken())
}, [])
const { t } = useTranslation()
const clearTokens = () => {
setIsOpen(false)
@@ -47,7 +50,7 @@ const Navbar = () => {
localStorage.removeItem(r)
})
toast.success('Cleared all tokens')
toast.success(t('Cleared all tokens'))
setTimeout(() => {
router.reload()
}, 1000)
@@ -74,7 +77,7 @@ const Navbar = () => {
>
<div className="flex items-center space-x-2">
<FontAwesomeIcon className="h-4 w-4" icon="search" />
<span className="text-sm font-medium">Search ...</span>
<span className="text-sm font-medium">{t('Search ...')}</span>
</div>
<div className="flex items-center space-x-1">
@@ -95,14 +98,20 @@ const Navbar = () => {
className="flex items-center space-x-2 hover:opacity-80 dark:text-white"
>
<FontAwesomeIcon icon={['fab', l.name.toLowerCase() as IconName]} />
<span className="hidden text-sm font-medium md:inline-block">{l.name}</span>
<span className="hidden text-sm font-medium md:inline-block">
{
// Append link name comments here to add translations
// t('Weibo')
t(l.name)
}
</span>
</a>
))}
{siteConfig.email && (
<a href={siteConfig.email} className="flex items-center space-x-2 hover:opacity-80 dark:text-white">
<FontAwesomeIcon icon={['far', 'envelope']} />
<span className="hidden text-sm font-medium md:inline-block">Email</span>
<span className="hidden text-sm font-medium md:inline-block">{t('Email')}</span>
</a>
)}
@@ -111,7 +120,7 @@ const Navbar = () => {
className="flex items-center space-x-2 p-2 hover:opacity-80 dark:text-white"
onClick={() => setIsOpen(true)}
>
<span className="text-sm font-medium">Logout</span>
<span className="text-sm font-medium">{t('Logout')}</span>
<FontAwesomeIcon icon="sign-out-alt" />
</button>
)}
@@ -148,12 +157,12 @@ const Navbar = () => {
>
<div className="my-8 inline-block w-full max-w-md transform overflow-hidden rounded-lg bg-white p-6 text-left align-middle transition-all dark:bg-gray-900">
<Dialog.Title className="text-lg font-bold text-gray-900 dark:text-gray-100">
Clear all tokens?
{t('Clear all tokens?')}
</Dialog.Title>
<div className="mt-2">
<p className="text-sm text-gray-500">
These tokens are used to authenticate yourself into password protected folders, clearing them means
that you will need to re-enter the passwords again.
{t('These tokens are used to authenticate yourself into password protected folders, ') +
t('clearing them means that you will need to re-enter the passwords again.')}
</p>
</div>
@@ -171,14 +180,14 @@ const Navbar = () => {
className="mr-3 inline-flex items-center justify-center space-x-2 rounded bg-blue-500 px-4 py-2 text-white hover:bg-blue-400 focus:outline-none focus:ring focus:ring-blue-300"
onClick={() => setIsOpen(false)}
>
Cancel
{t('Cancel')}
</button>
<button
className="inline-flex items-center justify-center space-x-2 rounded bg-red-500 px-4 py-2 text-white hover:bg-red-400 focus:outline-none focus:ring focus:ring-red-300"
onClick={() => clearTokens()}
>
<FontAwesomeIcon icon={['far', 'trash-alt']} />
<span>Clear all</span>
<span>{t('Clear all')}</span>
</button>
</div>
</div>
+17 -6
View File
@@ -4,6 +4,7 @@ import { Dispatch, Fragment, SetStateAction, useState } from 'react'
import AwesomeDebouncePromise from 'awesome-debounce-promise'
import { useAsync } from 'react-async-hook'
import useConstant from 'use-constant'
import { useTranslation } from 'next-i18next'
import Link from 'next/link'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
@@ -112,12 +113,19 @@ function SearchResultItemTemplate({
function SearchResultItemLoadRemote({ result }: { result: OdSearchResult[number] }) {
const { data, error }: SWRResponse<OdDriveItem, string> = useSWR(`/api/item?id=${result.id}`, fetcher)
const { t } = useTranslation()
if (error) {
return <SearchResultItemTemplate driveItem={result} driveItemPath={''} itemDescription={error} disabled={true} />
}
if (!data) {
return (
<SearchResultItemTemplate driveItem={result} driveItemPath={''} itemDescription={'Loading ...'} disabled={true} />
<SearchResultItemTemplate
driveItem={result}
driveItemPath={''}
itemDescription={t('Loading ...')}
disabled={true}
/>
)
}
@@ -159,6 +167,8 @@ export default function SearchModal({
}) {
const { query, setQuery, results } = useDriveItemSearch()
const { t } = useTranslation()
const closeSearchBox = () => {
setSearchOpen(false)
setQuery('')
@@ -199,13 +209,12 @@ export default function SearchModal({
type="text"
id="search-box"
className="w-full bg-transparent focus:outline-none focus-visible:outline-none"
placeholder="Search ..."
placeholder={t('Search ...')}
value={query}
onChange={e => setQuery(e.target.value)}
/>
<div className="px-2 py-1 rounded-lg bg-gray-200 dark:bg-gray-700 font-medium text-xs">ESC</div>
</Dialog.Title>
<div
className="bg-white dark:text-white dark:bg-gray-900 max-h-[80vh] overflow-x-hidden overflow-y-scroll"
onClick={closeSearchBox}
@@ -213,16 +222,18 @@ export default function SearchModal({
{results.loading && (
<div className="text-center px-4 py-12 text-sm font-medium">
<LoadingIcon className="animate-spin w-4 h-4 mr-2 inline-block svg-inline--fa" />
<span>Loading ...</span>
<span>{t('Loading ...')}</span>
</div>
)}
{results.error && (
<div className="text-center px-4 py-12 text-sm font-medium">Error: {results.error.message}</div>
<div className="text-center px-4 py-12 text-sm font-medium">
{t('Error: {{message}}', { message: results.error.message })}
</div>
)}
{results.result && (
<>
{results.result.length === 0 ? (
<div className="text-center px-4 py-12 text-sm font-medium">Nothing here.</div>
<div className="text-center px-4 py-12 text-sm font-medium">{t('Nothing here.')}</div>
) : (
results.result.map(result => <SearchResultItem key={result.id} result={result} />)
)}
+15 -2
View File
@@ -2,6 +2,7 @@ import { Fragment } from 'react'
import { IconProp } from '@fortawesome/fontawesome-svg-core'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { Listbox, Transition } from '@headlessui/react'
import { useTranslation } from 'next-i18next'
import useLocalStorage from '../utils/useLocalStorage'
@@ -13,13 +14,21 @@ export const layouts: Array<{ id: number; name: 'Grid' | 'List'; icon: IconProp
export const SwitchLayout = () => {
const [preferredLayout, setPreferredLayout] = useLocalStorage('preferredLayout', layouts[0])
const { t } = useTranslation()
return (
<div className="relative w-24 flex-shrink-0 text-sm text-gray-600 dark:text-gray-300 md:w-28">
<Listbox value={preferredLayout} onChange={setPreferredLayout}>
<Listbox.Button className="relative w-full cursor-pointer rounded pl-2">
<span className="pointer-events-none flex items-center">
<FontAwesomeIcon className="mr-2 h-3 w-3" icon={preferredLayout.icon} />
<span>{preferredLayout.name}</span>
<span>
{
// t('Grid')
// t('List')
t(preferredLayout.name)
}
</span>
</span>
<span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
<FontAwesomeIcon className="h-3 w-3" icon="chevron-down" />
@@ -39,7 +48,11 @@ export const SwitchLayout = () => {
>
<FontAwesomeIcon className="mr-2 h-3 w-3" icon={layout.icon} />
<span className={layout.name === preferredLayout.name ? 'font-medium' : 'font-normal'}>
{layout.name}
{
// t('Grid')
// t('List')
t(layout.name)
}
</span>
{layout.name === preferredLayout.name && (
<span className="absolute inset-y-0 right-3 flex items-center">
+4 -1
View File
@@ -3,6 +3,7 @@ import { FC, useState } from 'react'
import ReactAudioPlayer from 'react-audio-player'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { useTranslation } from 'next-i18next'
import DownloadButtonGroup from '../DownloadBtnGtoup'
import { DownloadBtnContainer, PreviewContainer } from './Containers'
@@ -19,6 +20,8 @@ enum PlayerState {
const AudioPreview: FC<{ file: OdFileObject }> = ({ file }) => {
const [playerStatus, setPlayerStatus] = useState(PlayerState.Loading)
const { t } = useTranslation()
return (
<>
<PreviewContainer>
@@ -37,7 +40,7 @@ const AudioPreview: FC<{ file: OdFileObject }> = ({ file }) => {
<div className="flex w-full flex-col space-y-2">
<div>{file.name}</div>
<div className="pb-4 text-sm text-gray-500">
Last modified: {formatModifiedDateTime(file.lastModifiedDateTime)}
{t('Last modified:') + ' ' + formatModifiedDateTime(file.lastModifiedDateTime)}
</div>
<ReactAudioPlayer
+4 -1
View File
@@ -1,5 +1,6 @@
import { useEffect, FC } from 'react'
import Prism from 'prismjs'
import { useTranslation } from 'next-i18next'
import { getExtension } from '../../utils/getFileIcon'
import useAxiosGet from '../../utils/fetchOnMount'
@@ -11,6 +12,8 @@ import { DownloadBtnContainer, PreviewContainer } from './Containers'
const CodePreview: FC<{ file: any }> = ({ file }) => {
const { response: content, error, validating } = useAxiosGet(file['@microsoft.graph.downloadUrl'])
const { t } = useTranslation()
useEffect(() => {
if (typeof window !== 'undefined') {
Prism.highlightAll()
@@ -27,7 +30,7 @@ const CodePreview: FC<{ file: any }> = ({ file }) => {
if (validating) {
return (
<PreviewContainer>
<Loading loadingText="Loading file content..." />
<Loading loadingText={t('Loading file content...')} />
</PreviewContainer>
)
}
+8 -5
View File
@@ -2,6 +2,7 @@ import type { OdFileObject } from '../../types'
import { FC } from 'react'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { useTranslation } from 'next-i18next'
import { getFileIcon } from '../../utils/getFileIcon'
import { formatModifiedDateTime, humanFileSize } from '../../utils/fileDetails'
@@ -10,6 +11,8 @@ import DownloadButtonGroup from '../DownloadBtnGtoup'
import { DownloadBtnContainer, PreviewContainer } from './Containers'
const DefaultPreview: FC<{ file: OdFileObject }> = ({ file }) => {
const { t } = useTranslation()
return (
<div>
<PreviewContainer>
@@ -21,22 +24,22 @@ const DefaultPreview: FC<{ file: OdFileObject }> = ({ file }) => {
<div className="flex flex-col space-y-2 py-4 md:flex-1">
<div>
<div className="py-2 text-xs font-medium uppercase opacity-80">Last modified</div>
<div className="py-2 text-xs font-medium uppercase opacity-80">{t('Last modified')}</div>
<div>{formatModifiedDateTime(file.lastModifiedDateTime)}</div>
</div>
<div>
<div className="py-2 text-xs font-medium uppercase opacity-80">File size</div>
<div className="py-2 text-xs font-medium uppercase opacity-80">{t('File size')}</div>
<div>{humanFileSize(file.size)}</div>
</div>
<div>
<div className="py-2 text-xs font-medium uppercase opacity-80">MIME type</div>
<div>{file.file?.mimeType || 'Unavailable'}</div>
<div className="py-2 text-xs font-medium uppercase opacity-80">{t('MIME type')}</div>
<div>{file.file?.mimeType || t('Unavailable')}</div>
</div>
<div>
<div className="py-2 text-xs font-medium uppercase opacity-80">Hashes</div>
<div className="py-2 text-xs font-medium uppercase opacity-80">{t('Hashes')}</div>
<table className="block w-full overflow-scroll whitespace-nowrap text-sm md:table">
<tbody>
<tr className="border-y bg-white dark:border-gray-700 dark:bg-gray-900">
+4 -1
View File
@@ -2,6 +2,7 @@ import type { OdFileObject } from '../../types'
import { FC, useEffect, useRef, useState } from 'react'
import { ReactReader } from 'react-reader'
import { useTranslation } from 'next-i18next'
import Loading from '../Loading'
import DownloadButtonGroup from '../DownloadBtnGtoup'
@@ -18,6 +19,8 @@ const EPUBPreview: FC<{ file: OdFileObject }> = ({ file }) => {
const [location, setLocation] = useState<string>()
const onLocationChange = (cfiStr: string) => setLocation(cfiStr)
const { t } = useTranslation()
// Fix for not valid epub files according to
// https://github.com/gerhardsletten/react-reader/issues/33#issuecomment-673964947
const fixEpub = rendition => {
@@ -50,7 +53,7 @@ const EPUBPreview: FC<{ file: OdFileObject }> = ({ file }) => {
<ReactReader
url={file['@microsoft.graph.downloadUrl']}
getRendition={rendition => fixEpub(rendition)}
loadingView={<Loading loadingText="Loading EPUB ..." />}
loadingView={<Loading loadingText={t('Loading EPUB ...')} />}
location={location}
locationChanged={onLocationChange}
epubInitOptions={{ openAs: 'epub' }}
+4 -1
View File
@@ -5,6 +5,7 @@ import gfm from 'remark-gfm'
import remarkMath from 'remark-math'
import rehypeKatex from 'rehype-katex'
import rehypeRaw from 'rehype-raw'
import { useTranslation } from 'next-i18next'
import 'katex/dist/katex.min.css'
@@ -21,6 +22,8 @@ const MarkdownPreview: FC<{
}> = ({ file, path, standalone = true }) => {
const { response: content, error, validating } = useAxiosGet(file['@microsoft.graph.downloadUrl'])
const { t } = useTranslation()
// The parent folder of the markdown file, which is also the relative image folder
const parentPath = path.substring(0, path.lastIndexOf('/'))
// Check if the image is relative path instead of a absolute url
@@ -76,7 +79,7 @@ const MarkdownPreview: FC<{
if (validating) {
return (
<PreviewContainer>
<Loading loadingText="Loading file content..." />
<Loading loadingText={t('Loading file content...')} />
</PreviewContainer>
)
}
+6 -2
View File
@@ -1,3 +1,5 @@
import { useTranslation } from 'next-i18next'
import FourOhFour from '../FourOhFour'
import Loading from '../Loading'
import DownloadButtonGroup from '../DownloadBtnGtoup'
@@ -5,6 +7,8 @@ import useAxiosGet from '../../utils/fetchOnMount'
import { DownloadBtnContainer, PreviewContainer } from './Containers'
const TextPreview = ({ file }) => {
const { t } = useTranslation()
const { response: content, error, validating } = useAxiosGet(file['@microsoft.graph.downloadUrl'])
if (error) {
return (
@@ -17,7 +21,7 @@ const TextPreview = ({ file }) => {
if (validating) {
return (
<PreviewContainer>
<Loading loadingText="Loading file content..." />
<Loading loadingText={t('Loading file content...')} />
</PreviewContainer>
)
}
@@ -25,7 +29,7 @@ const TextPreview = ({ file }) => {
if (!content) {
return (
<PreviewContainer>
<FourOhFour errorMsg="File is empty." />
<FourOhFour errorMsg={t('File is empty.')} />
</PreviewContainer>
)
}
+8 -4
View File
@@ -1,3 +1,5 @@
import { useTranslation } from 'next-i18next'
import FourOhFour from '../FourOhFour'
import Loading from '../Loading'
import { DownloadButton } from '../DownloadBtnGtoup'
@@ -12,6 +14,8 @@ const parseDotUrl = (content: string): string | undefined => {
}
const TextPreview = ({ file }) => {
const { t } = useTranslation()
const { response: content, error, validating } = useAxiosGet(file['@microsoft.graph.downloadUrl'])
if (error) {
return (
@@ -24,7 +28,7 @@ const TextPreview = ({ file }) => {
if (validating) {
return (
<PreviewContainer>
<Loading loadingText="Loading file content..." />
<Loading loadingText={t('Loading file content...')} />
</PreviewContainer>
)
}
@@ -32,7 +36,7 @@ const TextPreview = ({ file }) => {
if (!content) {
return (
<PreviewContainer>
<FourOhFour errorMsg="File is empty." />
<FourOhFour errorMsg={t('File is empty.')} />
</PreviewContainer>
)
}
@@ -47,9 +51,9 @@ const TextPreview = ({ file }) => {
<DownloadButton
onClickCallback={() => window.open(parseDotUrl(content) || '')}
btnColor="blue"
btnText="Open URL"
btnText={t('Open URL')}
btnIcon="external-link-alt"
btnTitle={`Open URL ${parseDotUrl(content) || ''}`}
btnTitle={t('Open URL{{url}}', { url: ' ' + parseDotUrl(content) || '' })}
/>
</div>
</DownloadBtnContainer>
+8 -5
View File
@@ -3,6 +3,7 @@ import { useRouter } from 'next/router'
import { useClipboard } from 'use-clipboard-copy'
import DPlayer from 'react-dplayer'
import toast from 'react-hot-toast'
import { useTranslation } from 'next-i18next'
import { useAsync } from 'react-async-hook'
import { getBaseUrl } from '../../utils/getBaseUrl'
@@ -17,6 +18,8 @@ const VideoPreview: React.FC<{ file: OdFileObject }> = ({ file }) => {
const { asPath } = useRouter()
const clipboard = useClipboard()
const { t } = useTranslation()
// OneDrive generates thumbnails for its video files, we pick the thumbnail with the highest resolution
const thumbnail = file.thumbnails && file.thumbnails.length > 0 ? file.thumbnails[0].large.url : ''
@@ -40,7 +43,7 @@ const VideoPreview: React.FC<{ file: OdFileObject }> = ({ file }) => {
{error ? (
<FourOhFour errorMsg={error.message} />
) : loading && isFlv ? (
<Loading loadingText="Loading FLV extension..." />
<Loading loadingText={t('Loading FLV extension...')} />
) : (
<DPlayer
className="aspect-video"
@@ -73,7 +76,7 @@ const VideoPreview: React.FC<{ file: OdFileObject }> = ({ file }) => {
<DownloadButton
onClickCallback={() => window.open(file['@microsoft.graph.downloadUrl'])}
btnColor="blue"
btnText="Download"
btnText={t('Download')}
btnIcon="file-download"
/>
{/* <DownloadButton
@@ -81,16 +84,16 @@ const VideoPreview: React.FC<{ file: OdFileObject }> = ({ file }) => {
window.open(`/api/proxy?url=${encodeURIComponent(file['@microsoft.graph.downloadUrl'])}`)
}
btnColor="teal"
btnText="Proxy download"
btnText={t('Proxy download')}
btnIcon="download"
/> */}
<DownloadButton
onClickCallback={() => {
clipboard.copy(`${getBaseUrl()}/api?path=${getReadablePath(asPath)}&raw=true`)
toast.success('Copied direct link to clipboard.')
toast.success(t('Copied direct link to clipboard.'))
}}
btnColor="pink"
btnText="Copy direct link"
btnText={t('Copy direct link')}
btnIcon="copy"
/>
+18
View File
@@ -0,0 +1,18 @@
const path = require('path')
const { i18n, localePath } = require('./next-i18next.config')
module.exports = {
createOldCatalogs: false,
defaultNamespace: 'common',
defaultValue: (lng, _ns, key) => (lng === i18n.defaultLocale ? key : ''),
keySeparator: false,
namespaceSeparator: false,
pluralSeparator: '——',
contextSeparator: '——',
lineEnding: 'lf',
locales: i18n.locales,
output: path.join(localePath, '$LOCALE/$NAMESPACE.json'),
input: ['**/*.{ts,tsx}', '!**/node_modules/**'],
sort: true
}
+14
View File
@@ -0,0 +1,14 @@
const path = require('path')
module.exports = {
i18n: {
defaultLocale: 'en',
locales: ['en', 'zh-CN']
},
localePath: path.resolve('public/locales'),
reloadOnPrerender: process.env.NODE_ENV === 'development',
keySeparator: false,
namespaceSeparator: false,
pluralSeparator: '——',
contextSeparator: '——'
}
+5
View File
@@ -1,3 +1,8 @@
const { i18n } = require('./next-i18next.config')
module.exports = {
i18n,
reactStrictMode: true,
// Required by Next i18n with API routes, otherwise API routes 404 when fetching without trailing slash
trailingSlash: true
}
+4 -1
View File
@@ -7,7 +7,8 @@
"build": "next build",
"start": "next start",
"lint": "next lint",
"format": "prettier components/**/*.tsx config/*.js pages/**/*.tsx {types,utils}/**/*.ts --write"
"format": "prettier components/**/*.tsx config/*.js pages/**/*.{ts,tsx} {types,utils}/**/*.ts --write",
"extract": "i18next"
},
"dependencies": {
"@fortawesome/fontawesome-svg-core": "^1.2.35",
@@ -27,6 +28,7 @@
"ioredis": "^4.28.2",
"jszip": "^3.7.1",
"next": "^12.0.10",
"next-i18next": "^10.2.0",
"nextjs-progressbar": "^0.0.13",
"preview-office-docs": "^1.0.2",
"prismjs": "^1.23.0",
@@ -62,6 +64,7 @@
"eslint": "8.8.0",
"eslint-config-next": "12.0.10",
"eslint-config-prettier": "^8.3.0",
"i18next-parser": "^5.4.0",
"postcss": "^8.4.5",
"prettier": "^2.5.1",
"prettier-plugin-tailwindcss": "^0.1.4",
+9
View File
@@ -1,5 +1,6 @@
import Head from 'next/head'
import { useRouter } from 'next/router'
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
import siteConfig from '../config/site.config'
import Navbar from '../components/Navbar'
@@ -32,3 +33,11 @@ export default function Folders() {
</div>
)
}
export async function getServerSideProps({ locale }) {
return {
props: {
...(await serverSideTranslations(locale, ['common']))
}
}
}
+2 -1
View File
@@ -55,6 +55,7 @@ import * as Icons from '@fortawesome/free-brands-svg-icons'
import type { AppProps } from 'next/app'
import NextNProgress from 'nextjs-progressbar'
import { appWithTranslation } from 'next-i18next'
// import all brand icons with tree-shaking so all icons can be referenced in the app
const iconList = Object.keys(Icons)
@@ -117,4 +118,4 @@ function MyApp({ Component, pageProps }: AppProps) {
</>
)
}
export default MyApp
export default appWithTranslation(MyApp)
+9
View File
@@ -1,4 +1,5 @@
import Head from 'next/head'
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
import siteConfig from '../config/site.config'
import Navbar from '../components/Navbar'
@@ -29,3 +30,11 @@ export default function Home() {
</div>
)
}
export async function getServerSideProps({ locale }) {
return {
props: {
...(await serverSideTranslations(locale, ['common']))
}
}
}
+47 -27
View File
@@ -1,6 +1,8 @@
import Head from 'next/head'
import Image from 'next/image'
import { useRouter } from 'next/router'
import { useTranslation, Trans } from 'next-i18next'
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
import siteConfig from '../../config/site.config'
import apiConfig from '../../config/api.config'
@@ -11,10 +13,12 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
export default function OAuthStep1() {
const router = useRouter()
const { t } = useTranslation()
return (
<div className="flex min-h-screen flex-col items-center justify-center bg-white dark:bg-gray-900">
<Head>
<title>{`OAuth Step 1 - ${siteConfig.title}`}</title>
<title>{t('OAuth Step 1 - {{title}}', { title: siteConfig.title })}</title>
</Head>
<main className="flex w-full flex-1 flex-col bg-gray-50 dark:bg-gray-800">
@@ -25,34 +29,40 @@ export default function OAuthStep1() {
<div className="mx-auto w-52">
<Image src="/images/fabulous-fireworks.png" width={912} height={912} alt="fabulous fireworks" priority />
</div>
<h3 className="mb-4 text-center text-xl font-medium">Welcome to your new onedrive-vercel-index 🎉</h3>
<h3 className="mb-4 text-center text-xl font-medium">
{t('Welcome to your new onedrive-vercel-index 🎉')}
</h3>
<h3 className="mt-4 mb-2 text-lg font-medium">Step 1/3: Preparations</h3>
<h3 className="mt-4 mb-2 text-lg font-medium">{t('Step 1/3: Preparations')}</h3>
<p className="py-1 text-sm font-medium text-yellow-400">
<FontAwesomeIcon icon="exclamation-triangle" className="mr-1" /> If you have not specified a REDIS_URL
inside your Vercel env variable, go initialise one at{' '}
<a href="https://upstash.com/" target="_blank" rel="noopener noreferrer" className="underline">
Upstash
</a>
. Docs:{' '}
<a
href="https://docs.upstash.com/redis/howto/vercelintegration"
target="_blank"
rel="noopener noreferrer"
className="underline"
>
Vercel Integration - Upstash
</a>
.
<Trans>
<FontAwesomeIcon icon="exclamation-triangle" className="mr-1" /> If you have not specified a REDIS_URL
inside your Vercel env variable, go initialise one at{' '}
<a href="https://upstash.com/" target="_blank" rel="noopener noreferrer" className="underline">
Upstash
</a>
. Docs:{' '}
<a
href="https://docs.upstash.com/redis/howto/vercelintegration"
target="_blank"
rel="noopener noreferrer"
className="underline"
>
Vercel Integration - Upstash
</a>
.
</Trans>
</p>
<p className="py-1">
Authorisation is required as no valid{' '}
<code className="font-mono text-sm underline decoration-pink-600 decoration-wavy">access_token</code> or{' '}
<code className="font-mono text-sm underline decoration-green-600 decoration-wavy">refresh_token</code> is
present on this deployed instance. Check the following configurations before proceeding with authorising
onedrive-vercel-index with your own Microsoft account.
<Trans>
Authorisation is required as no valid{' '}
<code className="font-mono text-sm underline decoration-pink-600 decoration-wavy">access_token</code> or{' '}
<code className="font-mono text-sm underline decoration-green-600 decoration-wavy">refresh_token</code>{' '}
is present on this deployed instance. Check the following configurations before proceeding with
authorising onedrive-vercel-index with your own Microsoft account.
</Trans>
</p>
<div className="my-4 overflow-hidden">
@@ -111,9 +121,11 @@ export default function OAuthStep1() {
</div>
<p className="py-1 text-sm font-medium">
<FontAwesomeIcon icon="exclamation-triangle" className="mr-1 text-yellow-400" /> If you see anything
missing or incorrect, you need to reconfigure <code className="font-mono text-xs">/config/api.json</code>{' '}
and redeploy this instance.
<Trans>
<FontAwesomeIcon icon="exclamation-triangle" className="mr-1 text-yellow-400" /> If you see anything
missing or incorrect, you need to reconfigure{' '}
<code className="font-mono text-xs">/config/api.config.js</code> and redeploy this instance.
</Trans>
</p>
<div className="mb-2 mt-6 text-right">
@@ -123,7 +135,7 @@ export default function OAuthStep1() {
router.push('/onedrive-vercel-index-oauth/step-2')
}}
>
<span>Proceed to OAuth</span> <FontAwesomeIcon icon="arrow-right" />
<span>{t('Proceed to OAuth')}</span> <FontAwesomeIcon icon="arrow-right" />
</button>
</div>
</div>
@@ -134,3 +146,11 @@ export default function OAuthStep1() {
</div>
)
}
export async function getServerSideProps({ locale }) {
return {
props: {
...(await serverSideTranslations(locale, ['common'])),
},
}
}
+35 -16
View File
@@ -3,6 +3,8 @@ import Image from 'next/image'
import { useRouter } from 'next/router'
import { useState } from 'react'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { useTranslation, Trans } from 'next-i18next'
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
import siteConfig from '../../config/site.config'
import Navbar from '../../components/Navbar'
@@ -17,12 +19,14 @@ export default function OAuthStep2() {
const [authCode, setAuthCode] = useState('')
const [buttonLoading, setButtonLoading] = useState(false)
const { t } = useTranslation()
const oAuthUrl = generateAuthorisationUrl()
return (
<div className="flex min-h-screen flex-col items-center justify-center bg-white dark:bg-gray-900">
<Head>
<title>{`OAuth Step 2 - ${siteConfig.title}`}</title>
<title>{t('OAuth Step 2 - {{title}}', { title: siteConfig.title })}</title>
</Head>
<main className="flex w-full flex-1 flex-col bg-gray-50 dark:bg-gray-800">
@@ -39,13 +43,17 @@ export default function OAuthStep2() {
priority
/>
</div>
<h3 className="mb-4 text-center text-xl font-medium">Welcome to your new onedrive-vercel-index 🎉</h3>
<h3 className="mb-4 text-center text-xl font-medium">
{t('Welcome to your new onedrive-vercel-index 🎉')}
</h3>
<h3 className="mt-4 mb-2 text-lg font-medium">Step 2/3: Get authorisation code</h3>
<h3 className="mt-4 mb-2 text-lg font-medium">{t('Step 2/3: Get authorisation code')}</h3>
<p className="py-1 text-sm font-medium text-red-400">
<FontAwesomeIcon icon="exclamation-circle" className="mr-1" /> If you are not the owner of this website,
stop now, as continuing with this process may expose your personal files in OneDrive.
<Trans>
<FontAwesomeIcon icon="exclamation-circle" className="mr-1" /> If you are not the owner of this website,
stop now, as continuing with this process may expose your personal files in OneDrive.
</Trans>
</p>
<div
@@ -63,11 +71,14 @@ export default function OAuthStep2() {
</div>
<p className="py-1">
The OAuth link for getting the authorisation code has been created. Click on the link above to get the{' '}
<b className="underline decoration-yellow-400 decoration-wavy">authorisation code</b>. Your browser will
open a new tab to Microsoft&apos;s account login page. After logging in and authenticating with your
Microsoft account, you will be redirected to a blank page on localhost. Paste{' '}
<b className="underline decoration-teal-500 decoration-wavy">the entire redirected URL</b> down below.
<Trans>
The OAuth link for getting the authorisation code has been created. Click on the link above to get the{' '}
<b className="underline decoration-yellow-400 decoration-wavy">authorisation code</b>. Your browser will
{/* eslint-disable-next-line react/no-unescaped-entities */}
open a new tab to Microsoft's account login page. After logging in and authenticating with your
Microsoft account, you will be redirected to a blank page on localhost. Paste{' '}
<b className="underline decoration-teal-500 decoration-wavy">the entire redirected URL</b> down below.
</Trans>
</p>
<div className="my-4 mx-auto w-2/3 overflow-hidden rounded">
@@ -90,15 +101,15 @@ export default function OAuthStep2() {
}}
/>
<p className="py-1">The authorisation code extracted is:</p>
<p className="py-1">{t('The authorisation code extracted is:')}</p>
<p className="my-2 overflow-hidden truncate rounded border border-gray-400/20 bg-gray-50 p-2 font-mono text-sm opacity-80 dark:bg-gray-800">
{authCode || <span className="animate-pulse">Waiting for code...</span>}
{authCode || <span className="animate-pulse">{t('Waiting for code...')}</span>}
</p>
<p>
{authCode
? '✅ You can now proceed onto the next step: requesting your access token and refresh token.'
: '❌ No valid code extracted.'}
? t(' You can now proceed onto the next step: requesting your access token and refresh token.')
: t(' No valid code extracted.')}
</p>
<div className="mb-2 mt-6 text-right">
@@ -112,11 +123,11 @@ export default function OAuthStep2() {
>
{buttonLoading ? (
<>
<span>Requesting tokens</span> <LoadingIcon className="ml-1 inline h-4 w-4 animate-spin" />
<span>{t('Requesting tokens')}</span> <LoadingIcon className="ml-1 inline h-4 w-4 animate-spin" />
</>
) : (
<>
<span>Get tokens</span> <FontAwesomeIcon icon="arrow-right" />
<span>{t('Get tokens')}</span> <FontAwesomeIcon icon="arrow-right" />
</>
)}
</button>
@@ -129,3 +140,11 @@ export default function OAuthStep2() {
</div>
)
}
export async function getServerSideProps({ locale }) {
return {
props: {
...(await serverSideTranslations(locale, ['common'])),
},
}
}
+60 -33
View File
@@ -3,6 +3,8 @@ import Image from 'next/image'
import { useRouter } from 'next/router'
import { useEffect, useState } from 'react'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { useTranslation, Trans } from 'next-i18next'
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
import siteConfig from '../../config/site.config'
import Navbar from '../../components/Navbar'
@@ -15,6 +17,8 @@ export default function OAuthStep3({ accessToken, expiryTime, refreshToken, erro
const router = useRouter()
const [expiryTimeLeft, setExpiryTimeLeft] = useState(expiryTime)
const { t } = useTranslation()
useEffect(() => {
if (!expiryTimeLeft) return
@@ -27,7 +31,7 @@ export default function OAuthStep3({ accessToken, expiryTime, refreshToken, erro
const [buttonContent, setButtonContent] = useState(
<div>
<span>Store tokens</span> <FontAwesomeIcon icon="key" />
<span>{t('Store tokens')}</span> <FontAwesomeIcon icon="key" />
</div>
)
const [buttonError, setButtonError] = useState(false)
@@ -36,7 +40,7 @@ export default function OAuthStep3({ accessToken, expiryTime, refreshToken, erro
setButtonError(false)
setButtonContent(
<div>
<span>Storing tokens</span> <LoadingIcon className="ml-1 inline h-4 w-4 animate-spin" />
<span>{t('Storing tokens')}</span> <LoadingIcon className="ml-1 inline h-4 w-4 animate-spin" />
</div>
)
@@ -46,7 +50,7 @@ export default function OAuthStep3({ accessToken, expiryTime, refreshToken, erro
setButtonError(true)
setButtonContent(
<div>
<span>Error validating identify, restart</span> <FontAwesomeIcon icon="exclamation-circle" />
<span>{t('Error validating identify, restart')}</span> <FontAwesomeIcon icon="exclamation-circle" />
</div>
)
return
@@ -55,7 +59,7 @@ export default function OAuthStep3({ accessToken, expiryTime, refreshToken, erro
setButtonError(true)
setButtonContent(
<div>
<span>Do not pretend to be the site owner</span> <FontAwesomeIcon icon="exclamation-circle" />
<span>{t('Do not pretend to be the site owner')}</span> <FontAwesomeIcon icon="exclamation-circle" />
</div>
)
return
@@ -66,7 +70,7 @@ export default function OAuthStep3({ accessToken, expiryTime, refreshToken, erro
setButtonError(false)
setButtonContent(
<div>
<span>Stored! Going home...</span> <FontAwesomeIcon icon="check" />
<span>{t('Stored! Going home...')}</span> <FontAwesomeIcon icon="check" />
</div>
)
setTimeout(() => {
@@ -77,7 +81,7 @@ export default function OAuthStep3({ accessToken, expiryTime, refreshToken, erro
setButtonError(true)
setButtonContent(
<div>
<span>Error storing the token</span> <FontAwesomeIcon icon="exclamation-circle" />
<span>{t('Error storing the token')}</span> <FontAwesomeIcon icon="exclamation-circle" />
</div>
)
})
@@ -86,7 +90,7 @@ export default function OAuthStep3({ accessToken, expiryTime, refreshToken, erro
return (
<div className="flex min-h-screen flex-col items-center justify-center bg-white dark:bg-gray-900">
<Head>
<title>{`OAuth Step 3 - ${siteConfig.title}`}</title>
<title>{t('OAuth Step 3 - {{title}}', { title: siteConfig.title })}</title>
</Head>
<main className="flex w-full flex-1 flex-col bg-gray-50 dark:bg-gray-800">
@@ -103,30 +107,43 @@ export default function OAuthStep3({ accessToken, expiryTime, refreshToken, erro
priority
/>
</div>
<h3 className="mb-4 text-center text-xl font-medium">Welcome to your new onedrive-vercel-index 🎉</h3>
<h3 className="mb-4 text-center text-xl font-medium">
{t('Welcome to your new onedrive-vercel-index 🎉')}
</h3>
<h3 className="mt-4 mb-2 text-lg font-medium">Step 3/3: Get access and refresh tokens</h3>
<h3 className="mt-4 mb-2 text-lg font-medium">{t('Step 3/3: Get access and refresh tokens')}</h3>
{error ? (
<div>
<p className="py-1 font-medium text-red-500">
<FontAwesomeIcon icon="exclamation-circle" className="mr-2" />
<span>Whoops, looks like we got a problem: {error}.</span>
<span>
{t('Whoops, looks like we got a problem: {{error}}.', {
// t('No auth code present')
error: t(error),
})}
</span>
</p>
<p className="my-2 whitespace-pre-line rounded border border-gray-400/20 bg-gray-50 p-2 font-mono text-sm opacity-80 dark:bg-gray-800">
{description}
{
// t('Where is the auth code? Did you follow step 2 you silly donut?')
t(description)
}
</p>
{errorUri && (
<p>
Check out{' '}
<a
href={errorUri}
target="_blank"
rel="noopener noreferrer"
className="text-blue-600 hover:underline dark:text-blue-500"
>
Microsoft&apos;s official explanation
</a>{' '}
on the error message.
<Trans>
Check out{' '}
<a
href={errorUri}
target="_blank"
rel="noopener noreferrer"
className="text-blue-600 hover:underline dark:text-blue-500"
>
{/* eslint-disable-next-line react/no-unescaped-entities */}
Microsoft's official explanation
</a>{' '}
on the error message.
</Trans>
</p>
)}
<div className="mb-2 mt-6 text-right">
@@ -136,19 +153,19 @@ export default function OAuthStep3({ accessToken, expiryTime, refreshToken, erro
router.push('/onedrive-vercel-index-oauth/step-1')
}}
>
<FontAwesomeIcon icon="arrow-left" /> <span>Restart</span>
<FontAwesomeIcon icon="arrow-left" /> <span>{t('Restart')}</span>
</button>
</div>
</div>
) : (
<div>
<p className="py-1 font-medium">Success! The API returned what we needed.</p>
<p className="py-1 font-medium">{t('Success! The API returned what we needed.')}</p>
<ol className="py-1">
{accessToken && (
<li>
<FontAwesomeIcon icon={['far', 'check-circle']} className="text-green-500" />{' '}
<span>
Acquired access_token:{' '}
{t('Acquired access_token: ')}
<code className="font-mono text-sm opacity-80">{`${accessToken.substring(0, 60)}...`}</code>
</span>
</li>
@@ -157,7 +174,7 @@ export default function OAuthStep3({ accessToken, expiryTime, refreshToken, erro
<li>
<FontAwesomeIcon icon={['far', 'check-circle']} className="text-green-500" />{' '}
<span>
Acquired refresh_token:{' '}
{t('Acquired refresh_token: ')}
<code className="font-mono text-sm opacity-80">{`${refreshToken.substring(0, 60)}...`}</code>
</span>
</li>
@@ -165,15 +182,22 @@ export default function OAuthStep3({ accessToken, expiryTime, refreshToken, erro
</ol>
<p className="py-1 text-sm font-medium text-teal-500">
<FontAwesomeIcon icon="exclamation-circle" className="mr-1" /> These tokens may take a few seconds to
populate after you click the button below. If you go back home and still see the welcome page telling
you to re-authenticate, revisit home and do a hard refresh.
<FontAwesomeIcon icon="exclamation-circle" className="mr-1" />{' '}
{t('These tokens may take a few seconds to populate after you click the button below. ') +
t('If you go back home and still see the welcome page telling you to re-authenticate, ') +
t('revisit home and do a hard refresh.')}
</p>
<p className="py-1">
Final step, click the button below to store these tokens persistently before they expire after{' '}
{Math.floor(expiryTimeLeft / 60)} minutes {expiryTimeLeft - Math.floor(expiryTimeLeft / 60) * 60}{' '}
seconds. Don&apos;t worry, after storing them, onedrive-vercel-index will take care of token refreshes
and updates after your site goes live.
{t(
'Final step, click the button below to store these tokens persistently before they expire after {{minutes}} minutes {{seconds}} seconds. ',
{
minutes: Math.floor(expiryTimeLeft / 60),
seconds: expiryTimeLeft - Math.floor(expiryTimeLeft / 60) * 60,
}
) +
t(
"Don't worry, after storing them, onedrive-vercel-index will take care of token refreshes and updates after your site goes live."
)}
</p>
<div className="mb-2 mt-6 text-right">
@@ -199,7 +223,7 @@ export default function OAuthStep3({ accessToken, expiryTime, refreshToken, erro
)
}
export async function getServerSideProps({ query }) {
export async function getServerSideProps({ query, locale }) {
const { authCode } = query
// Return if no auth code is present
@@ -208,6 +232,7 @@ export async function getServerSideProps({ query }) {
props: {
error: 'No auth code present',
description: 'Where is the auth code? Did you follow step 2 you silly donut?',
...(await serverSideTranslations(locale, ['common'])),
},
}
}
@@ -221,6 +246,7 @@ export async function getServerSideProps({ query }) {
error: response.error,
description: response.errorDescription,
errorUri: response.errorUri,
...(await serverSideTranslations(locale, ['common'])),
},
}
}
@@ -233,6 +259,7 @@ export async function getServerSideProps({ query }) {
expiryTime,
accessToken,
refreshToken,
...(await serverSideTranslations(locale, ['common'])),
},
}
}
+802 -14
View File
File diff suppressed because it is too large Load Diff
+111
View File
@@ -0,0 +1,111 @@
{
"- showing {{count}} page(s) ——one": "- showing {{count}} page ",
"- showing {{count}} page(s) ——other": "- showing {{count}} pages ",
"{{count}} item(s)——one": "{{count}} item",
"{{count}} item(s)——other": "{{count}} items",
"<0></0> If you are not the owner of this website, stop now, as continuing with this process may expose your personal files in OneDrive.": "<0></0> If you are not the owner of this website, stop now, as continuing with this process may expose your personal files in OneDrive.",
"<0></0> If you have not specified a REDIS_URL inside your Vercel env variable, go initialise one at <3>Upstash</3>. Docs: <6>Vercel Integration - Upstash</6>.": "<0></0> If you have not specified a REDIS_URL inside your Vercel env variable, go initialise one at <3>Upstash</3>. Docs: <6>Vercel Integration - Upstash</6>.",
"<0></0> If you see anything missing or incorrect, you need to reconfigure <3>/config/api.config.js</3> and redeploy this instance.": "<0></0> If you see anything missing or incorrect, you need to reconfigure <3>/config/api.config.js</3> and redeploy this instance.",
"✅ You can now proceed onto the next step: requesting your access token and refresh token.": "✅ You can now proceed onto the next step: requesting your access token and refresh token.",
"❌ No valid code extracted.": "❌ No valid code extracted.",
"Acquired access_token: ": "Acquired access_token: ",
"Acquired refresh_token: ": "Acquired refresh_token: ",
"Actions": "Actions",
"Authorisation is required as no valid <2>access_token</2> or <5>refresh_token</5> is present on this deployed instance. Check the following configurations before proceeding with authorising onedrive-vercel-index with your own Microsoft account.": "Authorisation is required as no valid <2>access_token</2> or <5>refresh_token</5> is present on this deployed instance. Check the following configurations before proceeding with authorising onedrive-vercel-index with your own Microsoft account.",
"Cancel": "Cancel",
"Cannot preview {{path}}": "Cannot preview {{path}}",
"Check out <2>Microsoft's official explanation</2> on the error message.": "Check out <2>Microsoft's official explanation</2> on the error message.",
"Clear all": "Clear all",
"Clear all tokens?": "Clear all tokens?",
"Cleared all tokens": "Cleared all tokens",
"clearing them means that you will need to re-enter the passwords again.": "clearing them means that you will need to re-enter the passwords again.",
"Copied direct link to clipboard.": "Copied direct link to clipboard.",
"Copied folder permalink.": "Copied folder permalink.",
"Copied raw file permalink.": "Copied raw file permalink.",
"Copy direct link": "Copy direct link",
"Copy folder permalink": "Copy folder permalink",
"Copy raw file permalink": "Copy raw file permalink",
"Copy the permalink to the file to the clipboard": "Copy the permalink to the file to the clipboard",
"Do not pretend to be the site owner": "Do not pretend to be the site owner",
"Don't worry, after storing them, onedrive-vercel-index will take care of token refreshes and updates after your site goes live.": "Don't worry, after storing them, onedrive-vercel-index will take care of token refreshes and updates after your site goes live.",
"Download": "Download",
"Download file": "Download file",
"Download folder": "Download folder",
"Download selected files": "Download selected files",
"Download the file directly through OneDrive": "Download the file directly through OneDrive",
"Downloading {{progress}}%": "Downloading {{progress}}%",
"Downloading folder, refresh page to cancel": "Downloading folder, refresh page to cancel",
"Downloading selected files, refresh page to cancel": "Downloading selected files, refresh page to cancel",
"Downloading selected files...": "Downloading selected files...",
"Email": "Email",
"Enter Password": "Enter Password",
"Error storing the token": "Error storing the token",
"Error validating identify, restart": "Error validating identify, restart",
"Error: {{message}}": "Error: {{message}}",
"Failed to download folder {{path}}: {{status}} {{message}} Skipped it to continue.": "Failed to download folder {{path}}: {{status}} {{message}} Skipped it to continue.",
"Failed to download folder.": "Failed to download folder.",
"Failed to download selected files.": "Failed to download selected files.",
"File is empty.": "File is empty.",
"File size": "File size",
"Final step, click the button below to store these tokens persistently before they expire after {{minutes}} minutes {{seconds}} seconds. ": "Final step, click the button below to store these tokens persistently before they expire after {{minutes}} minutes {{seconds}} seconds. ",
"Finished downloading folder.": "Finished downloading folder.",
"Finished downloading selected files.": "Finished downloading selected files.",
"Get tokens": "Get tokens",
"Grid": "Grid",
"Hashes": "Hashes",
"Home": "Home",
"If you go back home and still see the welcome page telling you to re-authenticate, ": "If you go back home and still see the welcome page telling you to re-authenticate, ",
"If you know the password, please enter it below.": "If you know the password, please enter it below.",
"Last modified": "Last modified",
"Last Modified": "Last Modified",
"Last modified:": "Last modified:",
"List": "List",
"Load more": "Load more",
"Loading ...": "Loading ...",
"Loading EPUB ...": "Loading EPUB ...",
"Loading file content...": "Loading file content...",
"Loading FLV extension...": "Loading FLV extension...",
"Logout": "Logout",
"MIME type": "MIME type",
"Name": "Name",
"No more files": "No more files",
"Nothing here.": "Nothing here.",
"OAuth Step 1 - {{title}}": "OAuth Step 1 - {{title}}",
"OAuth Step 2 - {{title}}": "OAuth Step 2 - {{title}}",
"OAuth Step 3 - {{title}}": "OAuth Step 3 - {{title}}",
"of {{count}} file(s) -——loaded——one": "of {{count}} file -",
"of {{count}} file(s) -——loaded——other": "of {{count}} files -",
"of {{count}} file(s) -——loading——one": "of ... file(s) -",
"of {{count}} file(s) -——loading——other": "of ... file(s) -",
"Oops, that's a <1>four-oh-four</1>.": "Oops, that's a <1>four-oh-four</1>.",
"Open URL": "Open URL",
"Open URL{{url}}": "Open URL{{url}}",
"Press <2>F12</2> and open devtools for more details, or seek help at <6>onedrive-vercel-index discussions</6>.": "Press <2>F12</2> and open devtools for more details, or seek help at <6>onedrive-vercel-index discussions</6>.",
"Proceed to OAuth": "Proceed to OAuth",
"Requesting tokens": "Requesting tokens",
"Restart": "Restart",
"revisit home and do a hard refresh.": "revisit home and do a hard refresh.",
"Search ...": "Search ...",
"Select all files": "Select all files",
"Select file": "Select file",
"Select files": "Select files",
"Size": "Size",
"Step 1/3: Preparations": "Step 1/3: Preparations",
"Step 2/3: Get authorisation code": "Step 2/3: Get authorisation code",
"Step 3/3: Get access and refresh tokens": "Step 3/3: Get access and refresh tokens",
"Store tokens": "Store tokens",
"Stored! Going home...": "Stored! Going home...",
"Storing tokens": "Storing tokens",
"Success! The API returned what we needed.": "Success! The API returned what we needed.",
"The authorisation code extracted is:": "The authorisation code extracted is:",
"The OAuth link for getting the authorisation code has been created. Click on the link above to get the <2>authorisation code</2>. Your browser willopen a new tab to Microsoft's account login page. After logging in and authenticating with your Microsoft account, you will be redirected to a blank page on localhost. Paste <6>the entire redirected URL</6> down below.": "The OAuth link for getting the authorisation code has been created. Click on the link above to get the <2>authorisation code</2>. Your browser willopen a new tab to Microsoft's account login page. After logging in and authenticating with your Microsoft account, you will be redirected to a blank page on localhost. Paste <6>the entire redirected URL</6> down below.",
"These tokens are used to authenticate yourself into password protected folders, ": "These tokens are used to authenticate yourself into password protected folders, ",
"These tokens may take a few seconds to populate after you click the button below. ": "These tokens may take a few seconds to populate after you click the button below. ",
"This route (the folder itself and the files inside) is password protected. ": "This route (the folder itself and the files inside) is password protected. ",
"Unavailable": "Unavailable",
"Waiting for code...": "Waiting for code...",
"Weibo": "Weibo",
"Welcome to your new onedrive-vercel-index 🎉": "Welcome to your new onedrive-vercel-index 🎉",
"Where is the auth code? Did you follow step 2 you silly donut?": "Where is the auth code? Did you follow step 2 you silly donut?",
"Whoops, looks like we got a problem: {{error}}.": "Whoops, looks like we got a problem: {{error}}."
}
+107
View File
@@ -0,0 +1,107 @@
{
"- showing {{count}} page(s) ——other": "已显示 {{count}} 页",
"{{count}} item(s)——other": "{{count}} 个项目",
"<0></0> If you are not the owner of this website, stop now, as continuing with this process may expose your personal files in OneDrive.": "<0></0> 如果你不是这个网站的所有者,请立即停止操作,因为接下来的操作可能会暴露你的 OneDrive 私人文件。",
"<0></0> If you have not specified a REDIS_URL inside your Vercel env variable, go initialise one at <3>Upstash</3>. Docs: <6>Vercel Integration - Upstash</6>.": "<0></0> 如果你还没有在 Vercel 中设置环境变量 REDIS_URL,你可以从 <3>Upstash</3> 处获取一个来使用。文档:<6>Vercel 集成 - Upstash</6>。",
"<0></0> If you see anything missing or incorrect, you need to reconfigure <3>/config/api.config.js</3> and redeploy this instance.": "<0></0> 如果你看到有遗漏或错误的项目,你需要重新编辑 <3>/config/api.config.js</3> 并重新部署这个实例。",
"✅ You can now proceed onto the next step: requesting your access token and refresh token.": "✅ 你现在可以进行下一步了:获取你的 access token 和 refresh token。",
"❌ No valid code extracted.": "❌ 无法提取授权码。",
"Acquired access_token: ": "获取 access_token",
"Acquired refresh_token: ": "获取 refresh_token",
"Actions": "操作",
"Authorisation is required as no valid <2>access_token</2> or <5>refresh_token</5> is present on this deployed instance. Check the following configurations before proceeding with authorising onedrive-vercel-index with your own Microsoft account.": "本项目还没有设置有效的 <2>access_token</2> 和 <5>refresh_token</5>,需要进行授权。在继续对 onedrive-vercel-index 授权你的 Microsoft 帐号前,请检查一下下方的配置信息。",
"Cancel": "取消",
"Cannot preview {{path}}": "无法预览 {{path}}",
"Check out <2>Microsoft's official explanation</2> on the error message.": "请查阅 <2>Microsoft 官方解释</2> 以获取详细的错误信息。",
"Clear all": "清除所有密钥",
"Clear all tokens?": "清除所有密钥?",
"Cleared all tokens": "已清除所有密钥",
"clearing them means that you will need to re-enter the passwords again.": "清除它们意味着下次访问时你需要重新输入密钥。",
"Copied direct link to clipboard.": "已复制直链到剪贴板。",
"Copied folder permalink.": "已复制文件夹永久链接。",
"Copied raw file permalink.": "已复制文件永久链接。",
"Copy direct link": "复制文件直链",
"Copy folder permalink": "复制文件夹永久链接",
"Copy raw file permalink": "复制文件永久链接",
"Copy the permalink to the file to the clipboard": "复制文件永久链接到剪贴板",
"Do not pretend to be the site owner": "你不是网站所有者",
"Don't worry, after storing them, onedrive-vercel-index will take care of token refreshes and updates after your site goes live.": "别担心,存储它们之后,onedrive-vercel-index 会在帮助你定时更新 token",
"Download": "下载",
"Download file": "下载此文件",
"Download folder": "下载此文件夹",
"Download selected files": "下载选定文件",
"Download the file directly through OneDrive": "直接从 OneDrive 下载文件",
"Downloading {{progress}}%": "已下载 {{progress}}%",
"Downloading folder, refresh page to cancel": "下载文件夹中,刷新页面以取消",
"Downloading selected files, refresh page to cancel": "下载选定文件中,刷新页面以取消",
"Downloading selected files...": "下载选定文件中…",
"Email": "电子邮件",
"Enter Password": "输入密码",
"Error storing the token": "存储 token 时出错",
"Error validating identify, restart": "校验身份出错,需要重新开始",
"Error: {{message}}": "错误:{{message}}",
"Failed to download folder {{path}}: {{status}} {{message}} Skipped it to continue.": "下载文件夹 {{path}} 失败:{{status}} {{message}} 已忽略此错误并继续下载。",
"Failed to download folder.": "下载文件夹失败。",
"Failed to download selected files.": "下载选定文件失败。",
"File is empty.": "文件为空。",
"File size": "文件大小",
"Final step, click the button below to store these tokens persistently before they expire after {{minutes}} minutes {{seconds}} seconds. ": "最后一步,在这些 tokens 于 {{minutes}} 分钟 {{seconds}} 秒后失效前,点击下方按钮以永久存储这些 tokens",
"Finished downloading folder.": "下载文件夹成功。",
"Finished downloading selected files.": "下载选定文件成功。",
"Get tokens": "获取 tokens",
"Grid": "图格",
"Hashes": "哈希值",
"Home": "首页",
"If you go back home and still see the welcome page telling you to re-authenticate, ": "如果你回到首页却仍然发现欢迎界面在提示你重新认证,",
"If you know the password, please enter it below.": "如果你知晓密码,请在下面输入。",
"Last modified": "最后修改时间",
"Last Modified": "最后修改时间",
"Last modified:": "最后修改时间:",
"List": "列表",
"Load more": "加载更多",
"Loading ...": "加载中…",
"Loading EPUB ...": "加载 EPUB 中…",
"Loading file content...": "加载文件内容中…",
"Loading FLV extension...": "加载 FLV 扩展中…",
"Logout": "注销",
"MIME type": "MIME 类型",
"Name": "文件名",
"No more files": "加载完毕",
"Nothing here.": "无内容。",
"OAuth Step 1 - {{title}}": "OAuth 第 1 步 - {{title}}",
"OAuth Step 2 - {{title}}": "OAuth 第 2 步 - {{title}}",
"OAuth Step 3 - {{title}}": "OAuth 第 3 步 - {{title}}",
"of {{count}} file(s) -——loaded——other": "共 {{count}} 个文件",
"of {{count}} file(s) -——loading——other": "共…个文件",
"Oops, that's a <1>four-oh-four</1>.": "Oops,这里是 <1>404</1> 页面。",
"Open URL": "打开 URL",
"Open URL{{url}}": "打开 URL{{url}}",
"Press <2>F12</2> and open devtools for more details, or seek help at <6>onedrive-vercel-index discussions</6>.": "请按下 <2>F12</2> 来打开开发者工具窗口以获取详细信息,或是到 <6>onedrive-vercel-index 社区讨论</6> 处寻求帮助。",
"Proceed to OAuth": "继续进行 OAuth",
"Requesting tokens": "正在获取 token",
"Restart": "重新开始",
"revisit home and do a hard refresh.": "重新访问首页并刷新浏览器",
"Search ...": "搜索…",
"Select all files": "选择所有文件",
"Select file": "选择此文件",
"Select files": "选择以下文件",
"Size": "文件大小",
"Step 1/3: Preparations": "步骤 1/3:准备",
"Step 2/3: Get authorisation code": "步骤 2/3:获取授权码",
"Step 3/3: Get access and refresh tokens": "步骤 3/3:获取 access token 和 refresh token",
"Store tokens": "储存 tokens",
"Stored! Going home...": "已存储!正在返回首页…",
"Storing tokens": "正在存储 token…",
"Success! The API returned what we needed.": "成功!需要的 token 已被返回。",
"The authorisation code extracted is:": "提取出的授权码为:",
"The OAuth link for getting the authorisation code has been created. Click on the link above to get the <2>authorisation code</2>. Your browser willopen a new tab to Microsoft's account login page. After logging in and authenticating with your Microsoft account, you will be redirected to a blank page on localhost. Paste <6>the entire redirected URL</6> down below.": "创建出的这个 OAuth 链接是用来获取授权码的。点击上方链接以获取所需的 <2>授权码</2>。你的浏览器将在新的标签页打开 Microsoft 帐号登录页面。在登录并验证你的 Microsoft 帐号之后,你将被重定向到一个域名为 localhost 的空白页面。请将<6>完整的重定向后的 URL</6> 整体复制粘贴到下方。",
"These tokens are used to authenticate yourself into password protected folders, ": "这些密钥是用来验证你的身份以访问密钥保护下的文件夹的,",
"These tokens may take a few seconds to populate after you click the button below. ": "当你点击下方按钮之后,这些 tokens 可能需要几秒钟来生成出现。",
"This route (the folder itself and the files inside) is password protected. ": "此路由(此文件夹和其中的文件)是受密钥保护的。",
"Unavailable": "无",
"Waiting for code...": "等待授权码…",
"Weibo": "微博",
"Welcome to your new onedrive-vercel-index 🎉": "欢迎来到你崭新的 onedrive-vercel-index 🎉",
"Where is the auth code? Did you follow step 2 you silly donut?": "授权码呢?你遵守了第 2 步吗?你这个傻瓜甜甜圈!o( ̄ヘ ̄o#)",
"Whoops, looks like we got a problem: {{error}}.": "Whoops,看来我们遇到了一个问题:{{error}}"
}