diff --git a/dist/article/article.css b/dist/article/article.css
index b0bf21a..015cd8b 100644
--- a/dist/article/article.css
+++ b/dist/article/article.css
@@ -13,8 +13,8 @@ body {
body.dark {
color: #f8f8f8;
--gray: #a19f9d;
- --primary: #3a96dd;
- --primary-alt: #4ba0e1;
+ --primary: #4ba0e1;
+ --primary-alt: #65aee6;
}
a {
diff --git a/dist/styles.css b/dist/styles.css
index c6671f2..a6d33e3 100644
--- a/dist/styles.css
+++ b/dist/styles.css
@@ -36,6 +36,7 @@ body.dark {
}
html, body {
+ background-color: transparent;
font-family: "Segoe UI Regular", "Source Han Sans SC Regular", "Microsoft YaHei", sans-serif;
height: 100%;
overflow: hidden;
@@ -94,7 +95,7 @@ i.ms-Nav-chevron {
.ms-Label, .ms-Spinner-label {
user-select: none;
}
-.ms-ActivityItem {
+.ms-ActivityItem, .ms-ActivityItem-commentText {
color: var(--neutralSecondary);
}
.ms-ActivityItem-timeStamp {
@@ -192,7 +193,7 @@ body.dark .btn-group .btn:active {
background-color: #fff2;
}
.btn-group .btn.disabled, .btn-group .btn.fetching {
- background-color: unset;
+ background-color: unset !important;
color: var(--neutralSecondaryAlt);
}
.btn-group .btn.fetching {
@@ -297,6 +298,9 @@ body.dark .settings .loading {
.tab-body .ms-StackItem:last-child {
margin-right: 0;
}
+.tab-body .ms-ChoiceFieldGroup {
+ margin-bottom: 20px;
+}
img.favicon {
width: 16px;
height: 16px;
diff --git a/package.json b/package.json
index f63fd3a..090adda 100644
--- a/package.json
+++ b/package.json
@@ -62,6 +62,7 @@
"pac-proxy-agent": "^4.1.0",
"react": "^16.13.1",
"react-dom": "^16.13.1",
+ "react-intl-universal": "^2.2.5",
"react-redux": "^7.2.0",
"redux": "^4.0.5",
"redux-devtools": "^3.5.0",
diff --git a/src/components/root.tsx b/src/components/root.tsx
index 7a511f1..5c40512 100644
--- a/src/components/root.tsx
+++ b/src/components/root.tsx
@@ -7,9 +7,11 @@ import MenuContainer from "../containers/menu-container"
import NavContainer from "../containers/nav-container"
import LogMenuContainer from "../containers/log-menu-container"
import SettingsContainer from "../containers/settings-container"
+import { RootState } from "../scripts/reducer"
-const Root = ({ dispatch }) => (
+const Root = ({ locale, dispatch }) => locale && (
dispatch(closeContextMenu())}
onContextMenu={event => {
let text = document.getSelection().toString()
@@ -24,4 +26,5 @@ const Root = ({ dispatch }) => (
)
-export default connect()(Root)
\ No newline at end of file
+const getLocale = (state: RootState) => ({ locale: state.app.locale })
+export default connect(getLocale)(Root)
\ No newline at end of file
diff --git a/src/components/settings.tsx b/src/components/settings.tsx
index 131d26d..04e1111 100644
--- a/src/components/settings.tsx
+++ b/src/components/settings.tsx
@@ -5,7 +5,7 @@ import AboutTab from "./settings/about"
import { Pivot, PivotItem, Spinner } from "@fluentui/react"
import SourcesTabContainer from "../containers/settings/sources-container"
import GroupsTabContainer from "../containers/settings/groups-container"
-import AppTab from "./settings/app"
+import AppTabContainer from "../containers/settings/app-container"
type SettingsProps = {
display: boolean,
@@ -38,7 +38,7 @@ class Settings extends React.Component {
-
+
diff --git a/src/components/settings/app.tsx b/src/components/settings/app.tsx
index 6d40fbf..6b8d0da 100644
--- a/src/components/settings/app.tsx
+++ b/src/components/settings/app.tsx
@@ -1,20 +1,31 @@
import * as React from "react"
+import intl = require("react-intl-universal")
import { urlTest } from "../../scripts/utils"
-import { getProxy, getProxyStatus, toggleProxyStatus, setProxy, getThemeSettings, setThemeSettings, ThemeSettings } from "../../scripts/settings"
-import { Stack, Label, Toggle, TextField, DefaultButton, ChoiceGroup, IChoiceGroupOption, loadTheme } from "@fluentui/react"
+import { getProxy, getProxyStatus, toggleProxyStatus, setProxy, getThemeSettings, setThemeSettings, ThemeSettings, getLocaleSettings } from "../../scripts/settings"
+import { Stack, Label, Toggle, TextField, DefaultButton, ChoiceGroup, IChoiceGroupOption, loadTheme, Dropdown, IDropdownOption } from "@fluentui/react"
-const themeChoices: IChoiceGroupOption[] = [
- { key: ThemeSettings.Default, text: "系统默认" },
- { key: ThemeSettings.Light, text: "浅色模式" },
- { key: ThemeSettings.Dark, text: "深色模式" }
-]
+type AppTabProps = {
+ setLanguage: (option: string) => void
+}
-class AppTab extends React.Component {
+class AppTab extends React.Component {
state = {
pacStatus: getProxyStatus(),
pacUrl: getProxy(),
themeSettings: getThemeSettings()
}
+
+ themeChoices = (): IChoiceGroupOption[] => [
+ { key: ThemeSettings.Default, text: intl.get("followSystem") },
+ { key: ThemeSettings.Light, text: intl.get("app.lightTheme") },
+ { key: ThemeSettings.Dark, text: intl.get("app.darkTheme") }
+ ]
+
+ languageOptions = (): IDropdownOption[] => [
+ { key: "default", text: intl.get("followSystem") },
+ { key: "en-US", text: "English" },
+ { key: "zh-CN", text: "中文(简体)"}
+ ]
toggleStatus = () => {
toggleProxyStatus()
@@ -41,9 +52,26 @@ class AppTab extends React.Component {
render = () => (
+
+
+
+ this.props.setLanguage(String(option.key))}
+ style={{width: 200}} />
+
+
+
+
+
-
+
@@ -54,8 +82,8 @@ class AppTab extends React.Component {
urlTest(v.trim()) ? "" : "请正确输入URL"}
- placeholder="PAC地址"
+ onGetErrorMessage={v => urlTest(v.trim()) ? "" : intl.get("app.badUrl")}
+ placeholder={intl.get("app.pac")}
name="pacUrl"
onChange={this.handleInputChange}
value={this.state.pacUrl} />
@@ -64,16 +92,10 @@ class AppTab extends React.Component {
+ text={intl.get("app.setPac")} />
}
-
-
)
}
diff --git a/src/components/settings/sources.tsx b/src/components/settings/sources.tsx
index f885572..a74fb1e 100644
--- a/src/components/settings/sources.tsx
+++ b/src/components/settings/sources.tsx
@@ -1,4 +1,5 @@
import * as React from "react"
+import intl = require("react-intl-universal")
import { Label, DefaultButton, TextField, Stack, PrimaryButton, DetailsList,
IColumn, SelectionMode, Selection, IChoiceGroupOption, ChoiceGroup } from "@fluentui/react"
import { SourceState, RSSSource, SourceOpenTarget } from "../../scripts/models/source"
@@ -20,42 +21,6 @@ type SourcesTabState = {
selectedSource: RSSSource
}
-const columns: IColumn[] = [
- {
- key: "favicon",
- name: "图标",
- fieldName: "name",
- isIconOnly: true,
- iconName: "ImagePixel",
- minWidth: 16,
- maxWidth: 16,
- onRender: (s: RSSSource) => s.iconurl && (
-
- )
- },
- {
- key: "name",
- name: "名称",
- fieldName: "name",
- minWidth: 200,
- data: 'string',
- isRowHeader: true
- },
- {
- key: "url",
- name: "URL",
- fieldName: "url",
- minWidth: 280,
- data: 'string'
- }
-]
-
-const sourceOpenTargetChoices: IChoiceGroupOption[] = [
- { key: String(SourceOpenTarget.Local), text: "RSS正文" },
- { key: String(SourceOpenTarget.Webpage), text: "加载网页" },
- { key: String(SourceOpenTarget.External), text: "在浏览器中打开" }
-]
-
class SourcesTab extends React.Component {
selection: Selection
@@ -78,6 +43,42 @@ class SourcesTab extends React.Component {
})
}
+ columns = (): IColumn[] => [
+ {
+ key: "favicon",
+ name: intl.get("icon"),
+ fieldName: "name",
+ isIconOnly: true,
+ iconName: "ImagePixel",
+ minWidth: 16,
+ maxWidth: 16,
+ onRender: (s: RSSSource) => s.iconurl && (
+
+ )
+ },
+ {
+ key: "name",
+ name: intl.get("name"),
+ fieldName: "name",
+ minWidth: 200,
+ data: 'string',
+ isRowHeader: true
+ },
+ {
+ key: "url",
+ name: "URL",
+ fieldName: "url",
+ minWidth: 280,
+ data: 'string'
+ }
+ ]
+
+ sourceOpenTargetChoices = (): IChoiceGroupOption[] => [
+ { key: String(SourceOpenTarget.Local), text: intl.get("sources.rssText") },
+ { key: String(SourceOpenTarget.Webpage), text: intl.get("sources.loadWebpage") },
+ { key: String(SourceOpenTarget.External), text: intl.get("openExternal") }
+ ]
+
handleInputChange = (event) => {
const name: string = event.target.name
this.setState({[name]: event.target.value.trim()})
@@ -96,24 +97,24 @@ class SourcesTab extends React.Component {
render = () => (
-
+
-
+
-
+
s.sid}
setKey="selected"
selection={this.selection}
selectionMode={SelectionMode.single} />
{this.state.selectedSource && <>
-
+
v.trim().length == 0 ? "名称不得为空" : ""}
+ onGetErrorMessage={v => v.trim().length == 0 ? intl.get("emptyName") : ""}
validateOnLoad={false}
- placeholder="订阅源名称"
+ placeholder={intl.get("sources.name")}
value={this.state.newSourceName}
name="newSourceName"
onChange={this.handleInputChange} />
@@ -152,23 +153,23 @@ class SourcesTab extends React.Component {
this.props.updateSourceName(this.state.selectedSource, this.state.newSourceName)}
- text="修改名称" />
+ text={intl.get("sources.editName")} />
-
+
this.props.deleteSource(this.state.selectedSource)}
key={this.state.selectedSource.sid}
- text={`删除订阅源`} />
+ text={intl.get("sources.delete")} />
- 这将移除此订阅源与所有已保存的文章
+ {intl.get("sources.deleteWarning")}
>}
diff --git a/src/containers/settings/app-container.tsx b/src/containers/settings/app-container.tsx
new file mode 100644
index 0000000..fb0064d
--- /dev/null
+++ b/src/containers/settings/app-container.tsx
@@ -0,0 +1,14 @@
+import { connect } from "react-redux"
+import { setLocaleSettings } from "../../scripts/settings"
+import { initIntl } from "../../scripts/models/app"
+import AppTab from "../../components/settings/app"
+
+const mapDispatchToProps = dispatch => ({
+ setLanguage: (option: string) => {
+ setLocaleSettings(option)
+ dispatch(initIntl())
+ }
+})
+
+const AppTabContainer = connect(null, mapDispatchToProps)(AppTab)
+export default AppTabContainer
\ No newline at end of file
diff --git a/src/electron.ts b/src/electron.ts
index 4aa00e2..e55728b 100644
--- a/src/electron.ts
+++ b/src/electron.ts
@@ -1,9 +1,10 @@
import { app, ipcMain, BrowserWindow, Menu, nativeTheme } from "electron"
import windowStateKeeper = require("electron-window-state")
-import Store = require('electron-store');
+import Store = require("electron-store")
let mainWindow: BrowserWindow
const store = new Store()
+nativeTheme.themeSource = store.get("theme", "system")
function createWindow() {
let mainWindowState = windowStateKeeper({
@@ -13,7 +14,7 @@ function createWindow() {
// Create the browser window.
mainWindow = new BrowserWindow({
title: "Fluent Reader",
- backgroundColor: shouldUseDarkColors() ? "#282828" : "#faf9f8",
+ backgroundColor: nativeTheme.shouldUseDarkColors ? "#282828" : "#faf9f8",
x: mainWindowState.x,
y: mainWindowState.y,
width: mainWindowState.width,
@@ -22,15 +23,20 @@ function createWindow() {
minHeight: 600,
frame: false,
fullscreenable: false,
+ show: false,
webPreferences: {
nodeIntegration: true,
webviewTag: true
}
})
mainWindowState.manage(mainWindow)
+ mainWindow.on('ready-to-show', () => {
+ mainWindow.show()
+ mainWindow.focus()
+ mainWindow.webContents.openDevTools()
+ });
// and load the index.html of the app.
mainWindow.loadFile((app.isPackaged ? "dist/" : "") + 'index.html')
- mainWindow.webContents.openDevTools()
}
Menu.setApplicationMenu(null)
@@ -54,10 +60,3 @@ ipcMain.on("set-theme", (_, theme) => {
store.set("theme", theme)
nativeTheme.themeSource = theme
})
-
-function shouldUseDarkColors() {
- let option = store.get("theme", "system")
- return option === "system"
- ? nativeTheme.shouldUseDarkColors
- : option === "dark"
-}
\ No newline at end of file
diff --git a/src/index.tsx b/src/index.tsx
index fa5029a..7844373 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -3,15 +3,12 @@ import * as ReactDOM from "react-dom"
import { Provider } from "react-redux"
import { createStore, applyMiddleware } from "redux"
import thunkMiddleware from "redux-thunk"
-import { loadTheme } from '@fluentui/react'
import { initializeIcons } from "@fluentui/react/lib/Icons"
import { rootReducer, RootState } from "./scripts/reducer"
-import { initSources } from "./scripts/models/source"
-import { fetchItems } from "./scripts/models/item"
import Root from "./components/root"
-import { initFeeds } from "./scripts/models/feed"
import { AppDispatch } from "./scripts/utils"
import { setProxy, applyThemeSettings } from "./scripts/settings"
+import { initApp } from "./scripts/models/app"
setProxy()
@@ -23,7 +20,7 @@ const store = createStore(
applyMiddleware(thunkMiddleware)
)
-store.dispatch(initSources()).then(() => store.dispatch(initFeeds())).then(() => store.dispatch(fetchItems()))
+store.dispatch(initApp())
ReactDOM.render(
diff --git a/src/scripts/i18n/_locales.ts b/src/scripts/i18n/_locales.ts
new file mode 100644
index 0000000..abcfbd4
--- /dev/null
+++ b/src/scripts/i18n/_locales.ts
@@ -0,0 +1,9 @@
+import en_US from "./en-US.json"
+import zh_CN from "./zh-CN.json"
+
+const locales = {
+ "en-US": en_US,
+ "zh-CN": zh_CN,
+}
+
+export default locales
\ No newline at end of file
diff --git a/src/scripts/i18n/en-US.json b/src/scripts/i18n/en-US.json
new file mode 100644
index 0000000..bb673ba
--- /dev/null
+++ b/src/scripts/i18n/en-US.json
@@ -0,0 +1,36 @@
+{
+ "allArticles": "All articles",
+ "add": "Add",
+ "create": "Create",
+ "icon": "Icon",
+ "name": "Name",
+ "openExternal": "Open externally",
+ "emptyName": "This field cannot be empty",
+ "followSystem": "Follow system",
+ "sources": {
+ "opmlFile": "OPML File",
+ "name": "Source name",
+ "editName": "Edit name",
+ "openTarget": "Default open target for articles",
+ "delete": "Delete source",
+ "add": "Add source",
+ "import": "Import",
+ "export": "Export",
+ "rssText": "RSS full text",
+ "loadWebpage": "Load webpage",
+ "inputUrl": "Enter URL",
+ "badUrl": "Invalid URL",
+ "deleteWarning": "The source and all saved articles will be removed",
+ "selected": "Selected source"
+ },
+ "app": {
+ "language": "Display language",
+ "theme": "Theme",
+ "lightTheme": "Light mode",
+ "darkTheme": "Dark mode",
+ "enableProxy": "Enable Proxy",
+ "badUrl": "Invalid URL",
+ "pac": "PAC Address",
+ "setPac": "Set PAC"
+ }
+}
\ No newline at end of file
diff --git a/src/scripts/i18n/zh-CN.json b/src/scripts/i18n/zh-CN.json
new file mode 100644
index 0000000..1440230
--- /dev/null
+++ b/src/scripts/i18n/zh-CN.json
@@ -0,0 +1,36 @@
+{
+ "allArticles": "全部文章",
+ "add": "添加",
+ "create": "新建",
+ "icon": "图标",
+ "name": "名称",
+ "openExternal": "在浏览器中打开",
+ "emptyName": "名称不得为空",
+ "followSystem": "跟随系统",
+ "sources": {
+ "opmlFile": "OPML文件",
+ "name": "订阅源名称",
+ "editName": "修改名称",
+ "openTarget": "订阅源文章打开方式",
+ "delete": "删除订阅源",
+ "add": "添加订阅源",
+ "import": "导入文件",
+ "export": "导出文件",
+ "rssText": "RSS正文",
+ "loadWebpage": "加载网页",
+ "inputUrl": "输入URL",
+ "badUrl": "请正确输入URL",
+ "deleteWarning": "这将移除此订阅源与所有已保存的文章",
+ "selected": "选中订阅源"
+ },
+ "app": {
+ "language": "界面语言",
+ "theme": "应用主题",
+ "lightTheme": "浅色模式",
+ "darkTheme": "深色模式",
+ "enableProxy": "启用代理",
+ "badUrl": "请正确输入URL",
+ "pac": "PAC地址",
+ "setPac": "设置PAC"
+ }
+}
\ No newline at end of file
diff --git a/src/scripts/models/app.ts b/src/scripts/models/app.ts
index 8ebf162..5186756 100644
--- a/src/scripts/models/app.ts
+++ b/src/scripts/models/app.ts
@@ -1,9 +1,12 @@
-import { RSSSource, INIT_SOURCES, SourceActionTypes, ADD_SOURCE, UPDATE_SOURCE, DELETE_SOURCE } from "./source"
-import { RSSItem, ItemActionTypes, FETCH_ITEMS } from "./item"
+import intl = require("react-intl-universal")
+import { RSSSource, INIT_SOURCES, SourceActionTypes, ADD_SOURCE, UPDATE_SOURCE, DELETE_SOURCE, initSources } from "./source"
+import { RSSItem, ItemActionTypes, FETCH_ITEMS, fetchItems } from "./item"
import { ActionStatus, AppThunk, getWindowBreakpoint } from "../utils"
import { INIT_FEEDS, FeedActionTypes, ALL, initFeeds } from "./feed"
import { SourceGroupActionTypes, UPDATE_SOURCE_GROUP, ADD_SOURCE_TO_GROUP, DELETE_SOURCE_GROUP, REMOVE_SOURCE_FROM_GROUP } from "./group"
import { PageActionTypes, SELECT_PAGE, PageType, selectAllArticles } from "./page"
+import { getCurrentLocale, setLocaleSettings } from "../settings"
+import locales from "../i18n/_locales"
export enum ContextMenuType {
Hidden, Item, Text, View
@@ -28,6 +31,7 @@ export class AppLog {
}
export class AppState {
+ locale = null as string
sourceInit = false
feedInit = false
fetchingItems = false
@@ -149,12 +153,44 @@ export function exitSettings(): AppThunk {
}
}
+export const INIT_INTL = "INIT_INTL"
+export interface InitIntlAction {
+ type: typeof INIT_INTL
+ locale: string
+}
+export const initIntlDone = (locale: string): InitIntlAction => ({
+ type: INIT_INTL,
+ locale: locale
+})
+
+export function initIntl(): AppThunk {
+ return (dispatch) => {
+ let locale = getCurrentLocale()
+ intl.init({
+ currentLocale: locale,
+ locales: locales,
+ fallbackLocale: "zh-CN"
+ }).then(() => dispatch(initIntlDone(locale)))
+ }
+}
+
+export function initApp(): AppThunk {
+ return (dispatch) => {
+ dispatch(initSources()).then(() => dispatch(initFeeds())).then(() => dispatch(fetchItems()))
+ dispatch(initIntl())
+ }
+}
+
export function appReducer(
state = new AppState(),
- action: SourceActionTypes | ItemActionTypes | ContextMenuActionTypes | SettingsActionTypes
+ action: SourceActionTypes | ItemActionTypes | ContextMenuActionTypes | SettingsActionTypes | InitIntlAction
| MenuActionTypes | LogMenuActionType | FeedActionTypes | PageActionTypes | SourceGroupActionTypes
): AppState {
switch (action.type) {
+ case INIT_INTL: return {
+ ...state,
+ locale: action.locale
+ }
case INIT_SOURCES:
switch (action.status) {
case ActionStatus.Success: return {
diff --git a/src/scripts/settings.ts b/src/scripts/settings.ts
index 7e43e70..86b586f 100644
--- a/src/scripts/settings.ts
+++ b/src/scripts/settings.ts
@@ -1,6 +1,7 @@
import { remote, ipcRenderer } from "electron"
import { ViewType } from "./models/page"
import { IPartialTheme, loadTheme } from "@fluentui/react"
+import locales from "./i18n/_locales"
const PAC_STORE_KEY = "PAC"
const PAC_STATUS_KEY = "PAC_ON"
@@ -97,6 +98,20 @@ export function applyThemeSettings() {
}
}
+const LOCALE_STORE_KEY = "locale"
+export function setLocaleSettings(option: string) {
+ localStorage.setItem(LOCALE_STORE_KEY, option)
+}
+export function getLocaleSettings() {
+ let stored = localStorage.getItem(LOCALE_STORE_KEY)
+ return stored === null ? "default" : stored
+}
+export function getCurrentLocale() {
+ let set = getLocaleSettings()
+ let locale = set === "default" ? remote.app.getLocale() : set
+ return (locale in locales) ? locale : "en-US"
+}
+
export const STORE_KEYS = [
PAC_STORE_KEY, PAC_STATUS_KEY, VIEW_STORE_KEY, THEME_STORE_KEY
]
\ No newline at end of file
diff --git a/tsconfig.json b/tsconfig.json
index e1f8555..adcb1ed 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -1,6 +1,8 @@
{
"compilerOptions": {
"jsx": "react",
+ "resolveJsonModule": true,
+ "esModuleInterop": true,
"target": "ES2019",
"module": "CommonJS"
}