english translation

This commit is contained in:
刘浩远 2020-06-11 21:24:18 +08:00
parent 34b281f59a
commit a0d5e41bf2
16 changed files with 310 additions and 99 deletions

View File

@ -1,4 +1,5 @@
import * as React from "react" import * as React from "react"
import intl = require("react-intl-universal")
import { renderToString } from "react-dom/server" import { renderToString } from "react-dom/server"
import { RSSItem } from "../scripts/models/item" import { RSSItem } from "../scripts/models/item"
import { openExternal } from "../scripts/utils" import { openExternal } from "../scripts/utils"
@ -11,6 +12,7 @@ const FONT_SIZE_OPTIONS = [12, 13, 14, 15, 16, 17, 18, 19, 20]
type ArticleProps = { type ArticleProps = {
item: RSSItem item: RSSItem
source: RSSSource source: RSSSource
locale: string
dismiss: () => void dismiss: () => void
toggleHasRead: (item: RSSItem) => void toggleHasRead: (item: RSSItem) => void
toggleStarred: (item: RSSItem) => void toggleStarred: (item: RSSItem) => void
@ -57,13 +59,13 @@ class Article extends React.Component<ArticleProps, ArticleState> {
items: [ items: [
{ {
key: "openInBrowser", key: "openInBrowser",
text: "在浏览器中打开", text: intl.get("openExternal"),
iconProps: {iconName: "NavigateExternalInline"}, iconProps: {iconName: "NavigateExternalInline"},
onClick: this.openInBrowser onClick: this.openInBrowser
}, },
{ {
key: "toggleHidden", key: "toggleHidden",
text: this.props.item.hidden ? "取消隐藏" : "隐藏文章", text: this.props.item.hidden ? intl.get("article.unhide") : intl.get("article.hide"),
onClick: () => { this.props.toggleHidden(this.props.item) } onClick: () => { this.props.toggleHidden(this.props.item) }
} }
] ]
@ -128,7 +130,7 @@ class Article extends React.Component<ArticleProps, ArticleState> {
articleView = () => "article/article.html?h=" + window.btoa(encodeURIComponent(renderToString(<> articleView = () => "article/article.html?h=" + window.btoa(encodeURIComponent(renderToString(<>
<p className="title">{this.props.item.title}</p> <p className="title">{this.props.item.title}</p>
<p className="date">{this.props.item.date.toLocaleString("zh-cn", {hour12: false})}</p> <p className="date">{this.props.item.date.toLocaleString(this.props.locale, {hour12: !this.props.locale.startsWith("zh")})}</p>
<article dangerouslySetInnerHTML={{__html: this.props.item.content}}></article> <article dangerouslySetInnerHTML={{__html: this.props.item.content}}></article>
</>))) + `&s=${this.state.fontSize}&u=${this.props.item.link}&d=${Number(document.body.classList.contains("dark"))}` </>))) + `&s=${this.state.fontSize}&u=${this.props.item.link}&d=${Number(document.body.classList.contains("dark"))}`
@ -144,35 +146,35 @@ class Article extends React.Component<ArticleProps, ArticleState> {
</span> </span>
</Stack.Item> </Stack.Item>
<CommandBarButton <CommandBarButton
title={this.props.item.hasRead ? "标为未读" : "标为已读"} title={this.props.item.hasRead ? intl.get("article.markUnread") : intl.get("article.markRead")}
iconProps={this.props.item.hasRead iconProps={this.props.item.hasRead
? {iconName: "StatusCircleRing"} ? {iconName: "StatusCircleRing"}
: {iconName: "RadioBtnOn", style: {fontSize: 14, textAlign: "center"}}} : {iconName: "RadioBtnOn", style: {fontSize: 14, textAlign: "center"}}}
onClick={() => this.props.toggleHasRead(this.props.item)} /> onClick={() => this.props.toggleHasRead(this.props.item)} />
<CommandBarButton <CommandBarButton
title={this.props.item.starred ? "取消星标" : "标为星标"} title={this.props.item.starred ? intl.get("article.unstar") : intl.get("article.star")}
iconProps={{iconName: this.props.item.starred ? "FavoriteStarFill" : "FavoriteStar"}} iconProps={{iconName: this.props.item.starred ? "FavoriteStarFill" : "FavoriteStar"}}
onClick={() => this.props.toggleStarred(this.props.item)} /> onClick={() => this.props.toggleStarred(this.props.item)} />
<CommandBarButton <CommandBarButton
title="字体大小" title={intl.get("article.fontSize")}
disabled={this.state.loadWebpage} disabled={this.state.loadWebpage}
iconProps={{iconName: "FontSize"}} iconProps={{iconName: "FontSize"}}
menuIconProps={{style: {display: "none"}}} menuIconProps={{style: {display: "none"}}}
menuProps={this.fontMenuProps()} /> menuProps={this.fontMenuProps()} />
<CommandBarButton <CommandBarButton
title="加载网页" title={intl.get("article.loadWebpage")}
className={this.state.loadWebpage ? "active" : ""} className={this.state.loadWebpage ? "active" : ""}
iconProps={{iconName: "Globe"}} iconProps={{iconName: "Globe"}}
onClick={this.toggleWebpage} /> onClick={this.toggleWebpage} />
<CommandBarButton <CommandBarButton
title="更多" title={intl.get("more")}
iconProps={{iconName: "More"}} iconProps={{iconName: "More"}}
menuIconProps={{style: {display: "none"}}} menuIconProps={{style: {display: "none"}}}
menuProps={this.moreMenuProps()} /> menuProps={this.moreMenuProps()} />
</Stack> </Stack>
<Stack horizontal horizontalAlign="end" style={{width: 112}}> <Stack horizontal horizontalAlign="end" style={{width: 112}}>
<CommandBarButton <CommandBarButton
title="关闭" title={intl.get("close")}
iconProps={{iconName: "BackToWindow"}} iconProps={{iconName: "BackToWindow"}}
onClick={this.props.dismiss} /> onClick={this.props.dismiss} />
</Stack> </Stack>

View File

@ -1,4 +1,5 @@
import * as React from "react" import * as React from "react"
import intl = require("react-intl-universal")
import { clipboard } from "electron" import { clipboard } from "electron"
import { openExternal, cutText, googleSearch } from "../scripts/utils" import { openExternal, cutText, googleSearch } from "../scripts/utils"
import { ContextualMenu, IContextualMenuItem, ContextualMenuItemType, DirectionalHint } from "office-ui-fabric-react/lib/ContextualMenu" import { ContextualMenu, IContextualMenuItem, ContextualMenuItemType, DirectionalHint } from "office-ui-fabric-react/lib/ContextualMenu"
@ -34,7 +35,7 @@ export class ContextMenu extends React.Component<ContextMenuProps> {
case ContextMenuType.Item: return [ case ContextMenuType.Item: return [
{ {
key: "showItem", key: "showItem",
text: "阅读", text: intl.get("context.read"),
iconProps: { iconName: "TextDocument" }, iconProps: { iconName: "TextDocument" },
onClick: () => { onClick: () => {
this.props.markRead(this.props.item) this.props.markRead(this.props.item)
@ -43,7 +44,7 @@ export class ContextMenu extends React.Component<ContextMenuProps> {
}, },
{ {
key: "openInBrowser", key: "openInBrowser",
text: "在浏览器中打开", text: intl.get("openExternal"),
iconProps: { iconName: "NavigateExternalInline" }, iconProps: { iconName: "NavigateExternalInline" },
onClick: () => { onClick: () => {
this.props.markRead(this.props.item) this.props.markRead(this.props.item)
@ -53,25 +54,25 @@ export class ContextMenu extends React.Component<ContextMenuProps> {
this.props.item.hasRead this.props.item.hasRead
? { ? {
key: "markAsUnread", key: "markAsUnread",
text: "标为未读", text: intl.get("article.markUnread"),
iconProps: { iconName: "RadioBtnOn", style: { fontSize: 14, textAlign: "center" } }, iconProps: { iconName: "RadioBtnOn", style: { fontSize: 14, textAlign: "center" } },
onClick: () => { this.props.markUnread(this.props.item) } onClick: () => { this.props.markUnread(this.props.item) }
} }
: { : {
key: "markAsRead", key: "markAsRead",
text: "标为已读", text: intl.get("article.markRead"),
iconProps: { iconName: "StatusCircleRing" }, iconProps: { iconName: "StatusCircleRing" },
onClick: () => { this.props.markRead(this.props.item) } onClick: () => { this.props.markRead(this.props.item) }
}, },
{ {
key: "toggleStarred", key: "toggleStarred",
text: this.props.item.starred ? "取消星标" : "标为星标", text: this.props.item.starred ? intl.get("article.unstar") : intl.get("article.star"),
iconProps: { iconName: this.props.item.starred ? "FavoriteStar" : "FavoriteStarFill" }, iconProps: { iconName: this.props.item.starred ? "FavoriteStar" : "FavoriteStarFill" },
onClick: () => { this.props.toggleStarred(this.props.item) } onClick: () => { this.props.toggleStarred(this.props.item) }
}, },
{ {
key: "toggleHidden", key: "toggleHidden",
text: this.props.item.hidden ? "取消隐藏" : "隐藏文章", text: this.props.item.hidden ? intl.get("article.unhide") : intl.get("article.hide"),
onClick: () => { this.props.toggleHidden(this.props.item) } onClick: () => { this.props.toggleHidden(this.props.item) }
}, },
{ {
@ -80,25 +81,25 @@ export class ContextMenu extends React.Component<ContextMenuProps> {
}, },
{ {
key: "copyTitle", key: "copyTitle",
text: "复制标题", text: intl.get("context.copyTitle"),
onClick: () => { clipboard.writeText(this.props.item.title) } onClick: () => { clipboard.writeText(this.props.item.title) }
}, },
{ {
key: "copyURL", key: "copyURL",
text: "复制链接", text: intl.get("context.copyURL"),
onClick: () => { clipboard.writeText(this.props.item.link) } onClick: () => { clipboard.writeText(this.props.item.link) }
} }
] ]
case ContextMenuType.Text: return [ case ContextMenuType.Text: return [
{ {
key: "copyText", key: "copyText",
text: "复制", text: intl.get("context.copy"),
iconProps: { iconName: "Copy" }, iconProps: { iconName: "Copy" },
onClick: () => { clipboard.writeText(this.props.text) } onClick: () => { clipboard.writeText(this.props.text) }
}, },
{ {
key: "searchText", key: "searchText",
text: `使用Google搜索“${cutText(this.props.text, 15)}`, text: intl.get("context.search", { text: cutText(this.props.text, 15) }),
iconProps: { iconName: "Search" }, iconProps: { iconName: "Search" },
onClick: () => { googleSearch(this.props.text) } onClick: () => { googleSearch(this.props.text) }
} }
@ -108,12 +109,12 @@ export class ContextMenu extends React.Component<ContextMenuProps> {
key: "section_1", key: "section_1",
itemType: ContextualMenuItemType.Section, itemType: ContextualMenuItemType.Section,
sectionProps: { sectionProps: {
title: "视图", title: intl.get("context.view"),
bottomDivider: true, bottomDivider: true,
items: [ items: [
{ {
key: "cardView", key: "cardView",
text: "卡片视图", text: intl.get("context.cardView"),
iconProps: { iconName: "GridViewMedium" }, iconProps: { iconName: "GridViewMedium" },
canCheck: true, canCheck: true,
checked: this.props.viewType === ViewType.Cards, checked: this.props.viewType === ViewType.Cards,
@ -121,7 +122,7 @@ export class ContextMenu extends React.Component<ContextMenuProps> {
}, },
{ {
key: "listView", key: "listView",
text: "列表视图", text: intl.get("context.listView"),
iconProps: { iconName: "BacklogList" }, iconProps: { iconName: "BacklogList" },
canCheck: true, canCheck: true,
checked: this.props.viewType === ViewType.List, checked: this.props.viewType === ViewType.List,
@ -134,12 +135,12 @@ export class ContextMenu extends React.Component<ContextMenuProps> {
key: "section_2", key: "section_2",
itemType: ContextualMenuItemType.Section, itemType: ContextualMenuItemType.Section,
sectionProps: { sectionProps: {
title: "筛选", title: intl.get("context.filter"),
bottomDivider: true, bottomDivider: true,
items: [ items: [
{ {
key: "allArticles", key: "allArticles",
text: "全部文章", text: intl.get("allArticles"),
iconProps: { iconName: "ClearFilter" }, iconProps: { iconName: "ClearFilter" },
canCheck: true, canCheck: true,
checked: (this.props.filter & ~FeedFilter.ShowHidden) == FeedFilter.Default, checked: (this.props.filter & ~FeedFilter.ShowHidden) == FeedFilter.Default,
@ -147,7 +148,7 @@ export class ContextMenu extends React.Component<ContextMenuProps> {
}, },
{ {
key: "unreadOnly", key: "unreadOnly",
text: "仅未读文章", text: intl.get("context.unreadOnly"),
iconProps: { iconName: "RadioBtnOn", style: { fontSize: 14, textAlign: "center" } }, iconProps: { iconName: "RadioBtnOn", style: { fontSize: 14, textAlign: "center" } },
canCheck: true, canCheck: true,
checked: (this.props.filter & ~FeedFilter.ShowHidden) == FeedFilter.UnreadOnly, checked: (this.props.filter & ~FeedFilter.ShowHidden) == FeedFilter.UnreadOnly,
@ -155,7 +156,7 @@ export class ContextMenu extends React.Component<ContextMenuProps> {
}, },
{ {
key: "starredOnly", key: "starredOnly",
text: "仅星标文章", text: intl.get("context.starredOnly"),
iconProps: { iconName: "FavoriteStarFill" }, iconProps: { iconName: "FavoriteStarFill" },
canCheck: true, canCheck: true,
checked: (this.props.filter & ~FeedFilter.ShowHidden) == FeedFilter.StarredOnly, checked: (this.props.filter & ~FeedFilter.ShowHidden) == FeedFilter.StarredOnly,
@ -166,7 +167,7 @@ export class ContextMenu extends React.Component<ContextMenuProps> {
}, },
{ {
key: "showHidden", key: "showHidden",
text: "显示隐藏文章", text: intl.get("context.showHidden"),
canCheck: true, canCheck: true,
checked: Boolean(this.props.filter & FeedFilter.ShowHidden), checked: Boolean(this.props.filter & FeedFilter.ShowHidden),
onClick: () => this.props.toggleFilter(FeedFilter.ShowHidden) onClick: () => this.props.toggleFilter(FeedFilter.ShowHidden)

View File

@ -1,5 +1,6 @@
import * as React from "react" import * as React from "react"
import { Feed, FeedProps } from "./feed" import intl = require("react-intl-universal")
import { FeedProps } from "./feed"
import DefaultCard from "../cards/default-card" import DefaultCard from "../cards/default-card"
import { PrimaryButton } from 'office-ui-fabric-react'; import { PrimaryButton } from 'office-ui-fabric-react';
@ -47,7 +48,7 @@ class CardsFeed extends React.Component<FeedProps> {
{ {
(this.props.feed.loaded && !this.props.feed.allLoaded) (this.props.feed.loaded && !this.props.feed.allLoaded)
? <div className="load-more-wrapper"><PrimaryButton ? <div className="load-more-wrapper"><PrimaryButton
text="加载更多" text={intl.get("loadMore")}
disabled={this.props.feed.loading} disabled={this.props.feed.loading}
onClick={() => this.props.loadMore(this.props.feed)} /></div> onClick={() => this.props.loadMore(this.props.feed)} /></div>
: null : null

View File

@ -1,4 +1,5 @@
import * as React from "react" import * as React from "react"
import intl = require("react-intl-universal")
import { FeedProps } from "./feed" import { FeedProps } from "./feed"
import { DefaultButton } from 'office-ui-fabric-react'; import { DefaultButton } from 'office-ui-fabric-react';
import ListCard from "../cards/list-card"; import ListCard from "../cards/list-card";
@ -22,7 +23,7 @@ class ListFeed extends React.Component<FeedProps> {
{ {
(this.props.feed.loaded && !this.props.feed.allLoaded) (this.props.feed.loaded && !this.props.feed.allLoaded)
? <div className="load-more-wrapper"><DefaultButton ? <div className="load-more-wrapper"><DefaultButton
text="加载更多" text={intl.get("loadMore")}
disabled={this.props.feed.loading} disabled={this.props.feed.loading}
onClick={() => this.props.loadMore(this.props.feed)} /></div> onClick={() => this.props.loadMore(this.props.feed)} /></div>
: null : null

View File

@ -1,4 +1,5 @@
import * as React from "react" import * as React from "react"
import intl = require("react-intl-universal")
import { Callout, ActivityItem, Icon, DirectionalHint } from "@fluentui/react" import { Callout, ActivityItem, Icon, DirectionalHint } from "@fluentui/react"
import { AppLog, AppLogType } from "../scripts/models/app" import { AppLog, AppLogType } from "../scripts/models/app"
import Time from "./utils/time" import Time from "./utils/time"
@ -29,7 +30,7 @@ class LogMenu extends React.Component<LogMenuProps> {
onDismiss={() => this.props.close()} onDismiss={() => this.props.close()}
> >
{ this.props.logs.length == 0 { this.props.logs.length == 0
? <p style={{ textAlign: "center" }}></p> ? <p style={{ textAlign: "center" }}>{intl.get("log.empty")}</p>
: this.activityItems().map((item => ( : this.activityItems().map((item => (
<ActivityItem {...item} key={item.key} style={{ margin: 12 }} /> <ActivityItem {...item} key={item.key} style={{ margin: 12 }} />
))) } ))) }

View File

@ -1,4 +1,5 @@
import * as React from "react" import * as React from "react"
import intl = require("react-intl-universal")
import { Icon } from "@fluentui/react/lib/Icon" import { Icon } from "@fluentui/react/lib/Icon"
import { Nav, INavLink, INavLinkGroup } from "office-ui-fabric-react/lib/Nav" import { Nav, INavLink, INavLinkGroup } from "office-ui-fabric-react/lib/Nav"
import { SourceGroup } from "../scripts/models/group" import { SourceGroup } from "../scripts/models/group"
@ -23,13 +24,13 @@ export class Menu extends React.Component<MenuProps> {
{ {
links: [ links: [
{ {
name: "搜索", name: intl.get("search"),
key: "search", key: "search",
icon: "Search", icon: "Search",
url: null url: null
}, },
{ {
name: "全部文章", name: intl.get("allArticles"),
key: ALL, key: ALL,
icon: "TextDocument", icon: "TextDocument",
onClick: this.props.allArticles, onClick: this.props.allArticles,
@ -78,14 +79,14 @@ export class Menu extends React.Component<MenuProps> {
<div className="menu-container" onClick={this.props.toggleMenu} style={{display: this.props.display ? "block" : "none"}}> <div className="menu-container" onClick={this.props.toggleMenu} style={{display: this.props.display ? "block" : "none"}}>
<div className="menu" onClick={(e) => e.stopPropagation()}> <div className="menu" onClick={(e) => e.stopPropagation()}>
<div className="btn-group"> <div className="btn-group">
<a className="btn hide-wide" title="关闭菜单" onClick={this.props.toggleMenu}><Icon iconName="Back" /></a> <a className="btn hide-wide" title={intl.get("menu.close")} onClick={this.props.toggleMenu}><Icon iconName="Back" /></a>
<a className="btn inline-block-wide" title="关闭菜单" onClick={this.props.toggleMenu}><Icon iconName="GlobalNavButton" /></a> <a className="btn inline-block-wide" title={intl.get("menu.close")} onClick={this.props.toggleMenu}><Icon iconName="GlobalNavButton" /></a>
</div> </div>
<div className="nav-wrapper"> <div className="nav-wrapper">
<Nav <Nav
groups={this.getItems()} groups={this.getItems()}
selectedKey={this.props.selected} /> selectedKey={this.props.selected} />
<p className={"subs-header " + AnimationClassNames.slideDownIn10}></p> <p className={"subs-header " + AnimationClassNames.slideDownIn10}>{intl.get("menu.subscriptions")}</p>
<Nav <Nav
selectedKey={this.props.selected} selectedKey={this.props.selected}
groups={this.getGroups()} /> groups={this.getGroups()} />

View File

@ -1,4 +1,5 @@
import * as React from "react" import * as React from "react"
import intl = require("react-intl-universal")
import { remote } from "electron" import { remote } from "electron"
import { Icon } from "@fluentui/react/lib/Icon" import { Icon } from "@fluentui/react/lib/Icon"
import { AppState } from "../scripts/models/app" import { AppState } from "../scripts/models/app"
@ -76,25 +77,59 @@ class Nav extends React.Component<NavProps, NavState> {
return ( return (
<nav className={this.hideButtons() + this.menuOn() + this.itemOn()}> <nav className={this.hideButtons() + this.menuOn() + this.itemOn()}>
<div className="btn-group"> <div className="btn-group">
<a className="btn hide-wide" title="菜单" onClick={this.props.menu}><Icon iconName="GlobalNavButton" /></a> <a className="btn hide-wide"
title={intl.get("nav.menu")}
onClick={this.props.menu}>
<Icon iconName="GlobalNavButton" />
</a>
</div> </div>
<span className="title">{this.props.state.title}</span> <span className="title">{this.props.state.title}</span>
<div className="btn-group" style={{float:"right"}}> <div className="btn-group" style={{float:"right"}}>
<a className={"btn"+this.fetching()} onClick={this.fetch} title="刷新"><Icon iconName="Refresh" /></a> <a className={"btn"+this.fetching()}
<a className="btn" title="全部标为已读"><Icon iconName="InboxCheck" /></a> onClick={this.fetch}
<a className="btn" id="log-toggle" title="消息" onClick={this.props.logs}> title={intl.get("nav.refresh")}>
<Icon iconName="Refresh" />
</a>
<a className="btn" title={intl.get("nav.markAllRead")}>
<Icon iconName="InboxCheck" />
</a>
<a className="btn"
id="log-toggle"
title={intl.get("nav.notifications")}
onClick={this.props.logs}>
{this.props.state.logMenu.notify ? <Icon iconName="RingerSolid" /> : <Icon iconName="Ringer" />} {this.props.state.logMenu.notify ? <Icon iconName="RingerSolid" /> : <Icon iconName="Ringer" />}
</a> </a>
<a className="btn" id="view-toggle" title="视图" onClick={this.props.views} <a className="btn"
onMouseDown={e => {if (this.props.state.contextMenu.event === "#view-toggle") e.stopPropagation()}}> id="view-toggle"
title={intl.get("nav.view")}
onClick={this.props.views}
onMouseDown={e => {
if (this.props.state.contextMenu.event === "#view-toggle") e.stopPropagation()}}>
<Icon iconName="View" /></a> <Icon iconName="View" /></a>
<a className="btn" title="选项" onClick={this.props.settings}><Icon iconName="Settings" /></a> <a className="btn"
<span className="seperator"></span> title={intl.get("nav.settings")}
<a className="btn system" title="最小化" onClick={this.minimize} style={{fontSize: 12}}><Icon iconName="Remove" /></a> onClick={this.props.settings}>
<a className="btn system" title="最大化" onClick={this.maximize}> <Icon iconName="Settings" />
{this.state.maximized ? <Icon iconName="ChromeRestore" style={{fontSize: 11}} /> :<Icon iconName="Checkbox" style={{fontSize: 10}} />} </a>
<span className="seperator"></span>
<a className="btn system"
title={intl.get("nav.minimize")}
onClick={this.minimize}
style={{fontSize: 12}}>
<Icon iconName="Remove" />
</a>
<a className="btn system"
title={intl.get("nav.maximize")}
onClick={this.maximize}>
{this.state.maximized
? <Icon iconName="ChromeRestore" style={{fontSize: 11}} />
: <Icon iconName="Checkbox" style={{fontSize: 10}} />}
</a>
<a className="btn system close"
title={intl.get("close")}
onClick={this.close}>
<Icon iconName="Cancel" />
</a> </a>
<a className="btn system close" title="关闭" onClick={this.close}><Icon iconName="Cancel" /></a>
</div> </div>
{!this.canFetch() && {!this.canFetch() &&
<ProgressIndicator <ProgressIndicator

View File

@ -1,4 +1,5 @@
import * as React from "react" import * as React from "react"
import intl = require("react-intl-universal")
import { Icon } from "@fluentui/react/lib/Icon" import { Icon } from "@fluentui/react/lib/Icon"
import { AnimationClassNames } from "@fluentui/react/lib/Styling" import { AnimationClassNames } from "@fluentui/react/lib/Styling"
import AboutTab from "./settings/about" import AboutTab from "./settings/about"
@ -23,24 +24,24 @@ class Settings extends React.Component<SettingsProps> {
<div className="settings-container"> <div className="settings-container">
<div className={"settings " + AnimationClassNames.slideUpIn20}> <div className={"settings " + AnimationClassNames.slideUpIn20}>
{this.props.blocked && <div className="loading"> {this.props.blocked && <div className="loading">
<Spinner label="正在更新订阅源,请稍候…" /> <Spinner label={intl.get("settings.fetching")} />
</div>} </div>}
<div className="btn-group" style={{position: "absolute", top: 6, left: -64}}> <div className="btn-group" style={{position: "absolute", top: 6, left: -64}}>
<a className={"btn" + (this.props.exitting ? " disabled" : "")} title="退出设置" onClick={this.props.close}> <a className={"btn" + (this.props.exitting ? " disabled" : "")} title={intl.get("settings.exit")} onClick={this.props.close}>
<Icon iconName="Back" /> <Icon iconName="Back" />
</a> </a>
</div> </div>
<Pivot> <Pivot>
<PivotItem headerText="订阅源" itemIcon="Source"> <PivotItem headerText={intl.get("settings.sources")} itemIcon="Source">
<SourcesTabContainer /> <SourcesTabContainer />
</PivotItem> </PivotItem>
<PivotItem headerText="分组与排序" itemIcon="GroupList"> <PivotItem headerText={intl.get("settings.grouping")} itemIcon="GroupList">
<GroupsTabContainer /> <GroupsTabContainer />
</PivotItem> </PivotItem>
<PivotItem headerText="应用选项" itemIcon="Settings"> <PivotItem headerText={intl.get("settings.app")} itemIcon="Settings">
<AppTabContainer /> <AppTabContainer />
</PivotItem> </PivotItem>
<PivotItem headerText="关于" itemIcon="Info"> <PivotItem headerText={intl.get("settings.about")} itemIcon="Info">
<AboutTab /> <AboutTab />
</PivotItem> </PivotItem>
</Pivot> </Pivot>

View File

@ -1,4 +1,5 @@
import * as React from "react" import * as React from "react"
import intl = require("react-intl-universal")
import { Stack, Link } from "@fluentui/react" import { Stack, Link } from "@fluentui/react"
import { openExternal } from "../../scripts/utils" import { openExternal } from "../../scripts/utils"
@ -8,11 +9,11 @@ class AboutTab extends React.Component {
<Stack className="settings-about" horizontalAlign="center"> <Stack className="settings-about" horizontalAlign="center">
<img src="logo.svg" style={{width: 120, height: 120}} /> <img src="logo.svg" style={{width: 120, height: 120}} />
<h3>Fluent Reader</h3> <h3>Fluent Reader</h3>
<small> 0.1.0</small> <small>{intl.get("settings.version")} 0.1.0</small>
<p className="settings-hint">Copyright © 2020 Haoyuan Liu. All rights reserved.</p> <p className="settings-hint">Copyright © 2020 Haoyuan Liu. All rights reserved.</p>
<Stack horizontal horizontalAlign="center" tokens={{childrenGap: 8}}> <Stack horizontal horizontalAlign="center" tokens={{childrenGap: 12}}>
<small><Link onClick={() => openExternal("https://github.com/yang991178/fluent-reader")}></Link></small> <small><Link onClick={() => openExternal("https://github.com/yang991178/fluent-reader")}>{intl.get("settings.openSource")}</Link></small>
<small><Link onClick={() => openExternal("https://github.com/yang991178/fluent-reader/issues")}></Link></small> <small><Link onClick={() => openExternal("https://github.com/yang991178/fluent-reader/issues")}>{intl.get("settings.feedback")}</Link></small>
</Stack> </Stack>
</Stack> </Stack>
</div> </div>

View File

@ -1,4 +1,5 @@
import * as React from "react" import * as React from "react"
import intl = require("react-intl-universal")
import { SourceGroup } from "../../scripts/models/group" import { SourceGroup } from "../../scripts/models/group"
import { SourceState, RSSSource } from "../../scripts/models/source" import { SourceState, RSSSource } from "../../scripts/models/source"
import { IColumn, Selection, SelectionMode, DetailsList, Label, Stack, import { IColumn, Selection, SelectionMode, DetailsList, Label, Stack,
@ -69,22 +70,22 @@ class GroupsTab extends React.Component<GroupsTabProps, GroupsTabState> {
}) })
} }
groupColumns: IColumn[] = [ groupColumns = (): IColumn[] => [
{ {
key: "type", key: "type",
name: "类型", name: intl.get("groups.type"),
minWidth: 40, minWidth: 40,
maxWidth: 40, maxWidth: 40,
data: "string", data: "string",
onRender: (g: SourceGroup) => <> onRender: (g: SourceGroup) => <>
{g.isMultiple ? "分组" : "订阅源"} {g.isMultiple ? intl.get("groups.group") : intl.get("groups.source")}
</> </>
}, },
{ {
key: "capacity", key: "capacity",
name: "容量", name: intl.get("groups.capacity"),
minWidth: 40, minWidth: 40,
maxWidth: 40, maxWidth: 60,
data: "string", data: "string",
onRender: (g: SourceGroup) => <> onRender: (g: SourceGroup) => <>
{g.isMultiple ? g.sids.length : ""} {g.isMultiple ? g.sids.length : ""}
@ -92,7 +93,7 @@ class GroupsTab extends React.Component<GroupsTabProps, GroupsTabState> {
}, },
{ {
key: "name", key: "name",
name: "名称", name: intl.get("name"),
minWidth: 200, minWidth: 200,
data: "string", data: "string",
isRowHeader: true, isRowHeader: true,
@ -105,7 +106,7 @@ class GroupsTab extends React.Component<GroupsTabProps, GroupsTabState> {
sourceColumns: IColumn[] = [ sourceColumns: IColumn[] = [
{ {
key: "favicon", key: "favicon",
name: "图标", name: intl.get("icon"),
fieldName: "name", fieldName: "name",
isIconOnly: true, isIconOnly: true,
iconName: "ImagePixel", iconName: "ImagePixel",
@ -117,7 +118,7 @@ class GroupsTab extends React.Component<GroupsTabProps, GroupsTabState> {
}, },
{ {
key: "name", key: "name",
name: "名称", name: intl.get("name"),
fieldName: "name", fieldName: "name",
minWidth: 200, minWidth: 200,
data: 'string', data: 'string',
@ -252,11 +253,11 @@ class GroupsTab extends React.Component<GroupsTabProps, GroupsTabState> {
<> <>
<Stack horizontal horizontalAlign="space-between" style={{height: 40}}> <Stack horizontal horizontalAlign="space-between" style={{height: 40}}>
<CommandBarButton <CommandBarButton
text="退出分组" text={intl.get("groups.exitGroup")}
iconProps={{iconName: "BackToWindow"}} iconProps={{iconName: "BackToWindow"}}
onClick={() => this.setState({manageGroup: false})} /> onClick={() => this.setState({manageGroup: false})} />
{this.state.selectedSources != null && <CommandBarButton {this.state.selectedSources != null && <CommandBarButton
text="从分组删除订阅源" text={intl.get("groups.deleteSource")}
onClick={this.removeFromGroup} onClick={this.removeFromGroup}
iconProps={{iconName: "RemoveFromShoppingList", style: {color: "#d13438"}}} />} iconProps={{iconName: "RemoveFromShoppingList", style: {color: "#d13438"}}} />}
</Stack> </Stack>
@ -272,18 +273,18 @@ class GroupsTab extends React.Component<GroupsTabProps, GroupsTabState> {
selectionMode={SelectionMode.multiple} /> selectionMode={SelectionMode.multiple} />
</MarqueeSelection> </MarqueeSelection>
<span className="settings-hint"></span> <span className="settings-hint">{intl.get("groups.sourceHint")}</span>
</>} </>}
{(!this.state.manageGroup || !this.state.selectedGroup) {(!this.state.manageGroup || !this.state.selectedGroup)
?<> ?<>
<form onSubmit={this.createGroup}> <form onSubmit={this.createGroup}>
<Label htmlFor="newGroupName"></Label> <Label htmlFor="newGroupName">{intl.get("groups.create")}</Label>
<Stack horizontal> <Stack horizontal>
<Stack.Item grow> <Stack.Item grow>
<TextField <TextField
onGetErrorMessage={v => v.trim().length == 0 ? "名称不得为空" : ""} onGetErrorMessage={v => v.trim().length == 0 ? intl.get("emptyName") : ""}
validateOnLoad={false} validateOnLoad={false}
placeholder="输入名称" placeholder={intl.get("groups.enterName")}
value={this.state.newGroupName} value={this.state.newGroupName}
id="newGroupName" id="newGroupName"
name="newGroupName" name="newGroupName"
@ -293,7 +294,7 @@ class GroupsTab extends React.Component<GroupsTabProps, GroupsTabState> {
<PrimaryButton <PrimaryButton
disabled={this.state.newGroupName.length == 0} disabled={this.state.newGroupName.length == 0}
type="sumbit" type="sumbit"
text="新建" /> text={intl.get("create")} />
</Stack.Item> </Stack.Item>
</Stack> </Stack>
</form> </form>
@ -301,7 +302,7 @@ class GroupsTab extends React.Component<GroupsTabProps, GroupsTabState> {
<DetailsList <DetailsList
compact={true} compact={true}
items={this.props.groups} items={this.props.groups}
columns={this.groupColumns} columns={this.groupColumns()}
setKey="selected" setKey="selected"
onItemInvoked={this.manageGroup} onItemInvoked={this.manageGroup}
dragDropEvents={this.groupDragDropEvents} dragDropEvents={this.groupDragDropEvents}
@ -311,13 +312,13 @@ class GroupsTab extends React.Component<GroupsTabProps, GroupsTabState> {
{this.state.selectedGroup {this.state.selectedGroup
? ( this.state.selectedGroup.isMultiple ? ( this.state.selectedGroup.isMultiple
?<> ?<>
<Label></Label> <Label>{intl.get("groups.selectedGroup")}</Label>
<Stack horizontal> <Stack horizontal>
<Stack.Item grow> <Stack.Item grow>
<TextField <TextField
onGetErrorMessage={v => v.trim().length == 0 ? "名称不得为空" : ""} onGetErrorMessage={v => v.trim().length == 0 ? intl.get("emptyName") : ""}
validateOnLoad={false} validateOnLoad={false}
placeholder="分组名称" placeholder={intl.get("groups.enterName")}
value={this.state.editGroupName} value={this.state.editGroupName}
name="editGroupName" name="editGroupName"
onChange={this.handleInputChange} /> onChange={this.handleInputChange} />
@ -326,22 +327,22 @@ class GroupsTab extends React.Component<GroupsTabProps, GroupsTabState> {
<DefaultButton <DefaultButton
disabled={this.state.editGroupName.length == 0} disabled={this.state.editGroupName.length == 0}
onClick={this.updateGroupName} onClick={this.updateGroupName}
text="修改名称" /> text={intl.get("groups.editName")} />
</Stack.Item> </Stack.Item>
<Stack.Item> <Stack.Item>
<DangerButton <DangerButton
key={this.state.selectedGroup.index} key={this.state.selectedGroup.index}
onClick={this.deleteGroup} onClick={this.deleteGroup}
text={`删除分组`} /> text={intl.get("groups.deleteGroup")} />
</Stack.Item> </Stack.Item>
</Stack> </Stack>
</> </>
:<> :<>
<Label></Label> <Label>{intl.get("groups.selectedSource")}</Label>
<Stack horizontal> <Stack horizontal>
<Stack.Item grow> <Stack.Item grow>
<Dropdown <Dropdown
placeholder="选择分组" placeholder={intl.get("groups.chooseGroup")}
selectedKey={this.state.dropdownIndex} selectedKey={this.state.dropdownIndex}
options={this.dropdownOptions()} options={this.dropdownOptions()}
onChange={this.dropdownChange} /> onChange={this.dropdownChange} />
@ -350,12 +351,12 @@ class GroupsTab extends React.Component<GroupsTabProps, GroupsTabState> {
<DefaultButton <DefaultButton
disabled={this.state.dropdownIndex === null} disabled={this.state.dropdownIndex === null}
onClick={this.addToGroup} onClick={this.addToGroup}
text="添加至分组" /> text={intl.get("groups.addToGroup")} />
</Stack.Item> </Stack.Item>
</Stack> </Stack>
</> </>
) )
: <span className="settings-hint"></span> : <span className="settings-hint">{intl.get("groups.groupHint")}</span>
} }
</> : null} </> : null}
</div> </div>

View File

@ -1,4 +1,5 @@
import * as React from "react" import * as React from "react"
import intl = require("react-intl-universal")
import { PrimaryButton } from "@fluentui/react"; import { PrimaryButton } from "@fluentui/react";
class DangerButton extends PrimaryButton { class DangerButton extends PrimaryButton {
@ -35,7 +36,7 @@ class DangerButton extends PrimaryButton {
{...this.props} {...this.props}
className={this.props.className + " danger"} className={this.props.className + " danger"}
onClick={this.onClick} onClick={this.onClick}
text={this.state.confirming ? `确认${this.props.text}?` : this.props.text} text={this.state.confirming ? intl.get("dangerButton", { action: this.props.text.toLowerCase() }) : this.props.text}
> >
{this.props.children} {this.props.children}
</PrimaryButton> </PrimaryButton>

View File

@ -13,13 +13,15 @@ type ArticleContainerProps = {
const getItem = (state: RootState, props: ArticleContainerProps) => state.items[props.itemId] const getItem = (state: RootState, props: ArticleContainerProps) => state.items[props.itemId]
const getSource = (state: RootState, props: ArticleContainerProps) => state.sources[state.items[props.itemId].source] const getSource = (state: RootState, props: ArticleContainerProps) => state.sources[state.items[props.itemId].source]
const getLocale = (state: RootState) => state.app.locale
const makeMapStateToProps = () => { const makeMapStateToProps = () => {
return createSelector( return createSelector(
[getItem, getSource], [getItem, getSource, getLocale],
(item, source) => ({ (item, source, locale) => ({
item: item, item: item,
source: source source: source,
locale: locale
}) })
) )
} }

View File

@ -1,3 +1,4 @@
import intl = require("react-intl-universal")
import { remote } from "electron" import { remote } from "electron"
import { connect } from "react-redux" import { connect } from "react-redux"
import { createSelector } from "reselect" import { createSelector } from "reselect"
@ -30,7 +31,7 @@ const mapDispatchToProps = (dispatch: AppDispatch) => {
let path = remote.dialog.showOpenDialogSync( let path = remote.dialog.showOpenDialogSync(
remote.getCurrentWindow(), remote.getCurrentWindow(),
{ {
filters: [{ name: "OPML文件", extensions: ["xml", "opml"] }], filters: [{ name: intl.get("sources.opmlFile"), extensions: ["xml", "opml"] }],
properties: ["openFile"] properties: ["openFile"]
} }
) )

View File

@ -5,8 +5,68 @@
"icon": "Icon", "icon": "Icon",
"name": "Name", "name": "Name",
"openExternal": "Open externally", "openExternal": "Open externally",
"emptyName": "This field cannot be empty", "emptyName": "This field cannot be empty.",
"followSystem": "Follow system", "followSystem": "Follow system",
"more": "More",
"close": "Close",
"search": "Search",
"loadMore": "Load more",
"dangerButton": "Confirm {action}?",
"log": {
"empty": "No notifications",
"fetchFailure": "Failed to load source \"{name}\".",
"fetchSuccess": "Successfully fetched {count, plural, =1 {# article} other {# articles}}."
},
"nav": {
"menu": "Menu",
"refresh": "Refresh",
"markAllRead": "Mark all as read",
"notifications": "Notifications",
"view": "View",
"settings": "Settings",
"minimize": "Minimize",
"maximize": "Maximize"
},
"menu": {
"close": "Close menu",
"subscriptions": "Subscriptions"
},
"article": {
"hide": "Hide article",
"unhide": "Unhide article",
"markRead": "Mark as read",
"markUnread": "Mark as unread",
"star": "Star",
"unstar": "Remove star",
"fontSize": "Font size",
"loadWebpage": "Load webpage"
},
"context": {
"read": "Read",
"copyTitle": "Copy title",
"copyURL": "Copy link",
"copy": "Copy",
"search": "Search \"{text}\" on Google",
"view": "View",
"cardView": "Card view",
"listView": "List view",
"filter": "Filtering",
"unreadOnly": "Unread only",
"starredOnly": "Starred only",
"showHidden": "Show hidden articles"
},
"settings": {
"name": "Settings",
"fetching": "Updating sources, please wait …",
"exit": "Exit settings",
"sources": "Sources",
"grouping": "Grouping",
"app": "Preferences",
"about": "About",
"version": "Version",
"openSource": "Open source",
"feedback": "Feedback"
},
"sources": { "sources": {
"opmlFile": "OPML File", "opmlFile": "OPML File",
"name": "Source name", "name": "Source name",
@ -20,9 +80,27 @@
"loadWebpage": "Load webpage", "loadWebpage": "Load webpage",
"inputUrl": "Enter URL", "inputUrl": "Enter URL",
"badUrl": "Invalid URL", "badUrl": "Invalid URL",
"deleteWarning": "The source and all saved articles will be removed", "deleteWarning": "The source and all saved articles will be removed.",
"selected": "Selected source" "selected": "Selected source"
}, },
"groups": {
"type": "Type",
"group": "Group",
"source": "Source",
"capacity": "Capacity",
"exitGroup": "Back to groups",
"deleteSource": "Delete from group",
"sourceHint": "Drag and drop sources to reorder.",
"create": "Create group",
"selectedGroup": "Selected group",
"selectedSource": "Selected source",
"enterName": "Enter name",
"editName": "Edit name",
"deleteGroup": "Delete group",
"chooseGroup": "Select a group",
"addToGroup": "Add to ...",
"groupHint": "Double click on group to edit sources. Drag and drop to reorder."
},
"app": { "app": {
"language": "Display language", "language": "Display language",
"theme": "Theme", "theme": "Theme",

View File

@ -7,6 +7,66 @@
"openExternal": "在浏览器中打开", "openExternal": "在浏览器中打开",
"emptyName": "名称不得为空", "emptyName": "名称不得为空",
"followSystem": "跟随系统", "followSystem": "跟随系统",
"more": "更多",
"close": "关闭",
"search": "搜索",
"loadMore": "加载更多",
"dangerButton": "确认{action}",
"log": {
"empty": "无消息",
"fetchFailure": "无法加载订阅源“{name}”",
"fetchSuccess": "成功加载 {count} 篇文章"
},
"nav": {
"menu": "菜单",
"refresh": "刷新",
"markAllRead": "全部标为已读",
"notifications": "消息",
"view": "视图",
"settings": "选项",
"minimize": "最小化",
"maximize": "最大化"
},
"menu": {
"close": "关闭菜单",
"subscriptions": "订阅源"
},
"article": {
"hide": "隐藏文章",
"unhide": "取消隐藏",
"markRead": "标为已读",
"markUnread": "标为未读",
"star": "标为星标",
"unstar": "取消星标",
"fontSize": "字体大小",
"loadWebpage": "加载网页"
},
"context": {
"read": "阅读",
"copyTitle": "复制标题",
"copyURL": "复制链接",
"copy": "复制",
"search": "使用Google搜索“{text}”",
"view": "视图",
"cardView": "卡片视图",
"listView": "列表视图",
"filter": "筛选",
"unreadOnly": "仅未读文章",
"starredOnly": "仅星标文章",
"showHidden": "显示隐藏文章"
},
"settings": {
"name": "选项",
"fetching": "正在更新订阅源,请稍候…",
"exit": "退出选项",
"sources": "订阅源",
"grouping": "分组与排序",
"app": "应用偏好",
"about": "关于",
"version": "版本",
"openSource": "开源项目",
"feedback": "反馈"
},
"sources": { "sources": {
"opmlFile": "OPML文件", "opmlFile": "OPML文件",
"name": "订阅源名称", "name": "订阅源名称",
@ -23,6 +83,24 @@
"deleteWarning": "这将移除此订阅源与所有已保存的文章", "deleteWarning": "这将移除此订阅源与所有已保存的文章",
"selected": "选中订阅源" "selected": "选中订阅源"
}, },
"groups": {
"type": "类型",
"group": "分组",
"source": "订阅源",
"capacity": "容量",
"exitGroup": "退出分组",
"deleteSource": "从分组删除订阅源",
"sourceHint": "拖拽订阅源以排序",
"create": "新建分组",
"selectedGroup": "选中分组",
"selectedSource": "选中订阅源",
"enterName": "输入名称",
"editName": "修改名称",
"deleteGroup": "删除分组",
"chooseGroup": "选择分组",
"addToGroup": "添加至分组",
"groupHint": "双击分组以修改订阅源,可通过拖拽排序"
},
"app": { "app": {
"language": "界面语言", "language": "界面语言",
"theme": "应用主题", "theme": "应用主题",

View File

@ -39,7 +39,7 @@ export class AppState {
fetchingTotal = 0 fetchingTotal = 0
menu = getWindowBreakpoint() menu = getWindowBreakpoint()
menuKey = ALL menuKey = ALL
title = "全部文章" title = ""
settings = { settings = {
display: false, display: false,
changed: false, changed: false,
@ -163,21 +163,27 @@ export const initIntlDone = (locale: string): InitIntlAction => ({
locale: locale locale: locale
}) })
export function initIntl(): AppThunk { export function initIntl(): AppThunk<Promise<void>> {
return (dispatch) => { return (dispatch) => {
let locale = getCurrentLocale() let locale = getCurrentLocale()
intl.init({ return intl.init({
currentLocale: locale, currentLocale: locale,
locales: locales, locales: locales,
fallbackLocale: "zh-CN" fallbackLocale: "zh-CN"
}).then(() => dispatch(initIntlDone(locale))) }).then(() => { dispatch(initIntlDone(locale)) })
} }
} }
export function initApp(): AppThunk { export function initApp(): AppThunk {
return (dispatch) => { return (dispatch) => {
dispatch(initSources()).then(() => dispatch(initFeeds())).then(() => dispatch(fetchItems())) dispatch(initIntl()).then(() =>
dispatch(initIntl()) dispatch(initSources())
).then(() =>
dispatch(initFeeds())
).then(() => {
dispatch(selectAllArticles())
dispatch(fetchItems())
})
} }
} }
@ -257,7 +263,7 @@ export function appReducer(
notify: !state.logMenu.display, notify: !state.logMenu.display,
logs: [...state.logMenu.logs, new AppLog( logs: [...state.logMenu.logs, new AppLog(
AppLogType.Failure, AppLogType.Failure,
`无法加载订阅源“${action.errSource.name}`, intl.get("log.fetchFailure", { name: action.errSource.name }),
String(action.err) String(action.err)
)] )]
} }
@ -270,7 +276,7 @@ export function appReducer(
...state.logMenu, ...state.logMenu,
logs: [...state.logMenu.logs, new AppLog( logs: [...state.logMenu.logs, new AppLog(
AppLogType.Info, AppLogType.Info,
`成功加载 ${action.items.length} 篇文章` intl.get("log.fetchSuccess", { count: action.items.length })
)] )]
} }
} }
@ -286,7 +292,7 @@ export function appReducer(
...state, ...state,
menu: state.menu && action.keepMenu, menu: state.menu && action.keepMenu,
menuKey: ALL, menuKey: ALL,
title: "全部文章" title: intl.get("allArticles")
} }
case PageType.Sources: return { case PageType.Sources: return {
...state, ...state,