diff --git a/App.tsx b/App.tsx
index dfe2d708..8ccbd27c 100644
--- a/App.tsx
+++ b/App.tsx
@@ -5,7 +5,8 @@ import { Provider } from 'react-redux'
import ThemeManager from 'src/utils/styles/ThemeManager'
import { Index } from 'src/Index'
-import { store } from 'src/store'
+import { persistor, store } from 'src/store'
+import { PersistGate } from 'redux-persist/integration/react'
const queryCache = new QueryCache()
@@ -30,7 +31,16 @@ const App: React.FC = () => {
-
+
+ {bootstrapped => {
+ if (bootstrapped) {
+ require('src/i18n/i18n')
+ return
+ } else {
+ return <>>
+ }
+ }}
+
diff --git a/app.config.ts b/app.config.ts
index 467aa7e4..b50bdd32 100644
--- a/app.config.ts
+++ b/app.config.ts
@@ -13,11 +13,22 @@ export default (): ExpoConfig => ({
developmentClient: { silentLaunch: true },
scheme: 'mastodonct',
ios: {
+ infoPlist: {
+ CFBundleAllowMixedLocalizations: true
+ },
splash: {
image: './assets/splash.png',
resizeMode: 'contain',
backgroundColor: '#ffffff'
}
},
- assetBundlePatterns: ['**/*']
+ locales: {
+ zh: {
+ CFBundleDisplayName: '我的嘟嘟'
+ },
+ en: {
+ CFBundleDisplayName: 'My Toots'
+ }
+ },
+ assetBundlePatterns: ['assets/*']
})
diff --git a/package.json b/package.json
index eaadcd04..e05e00a3 100644
--- a/package.json
+++ b/package.json
@@ -18,13 +18,16 @@
"expo-auth-session": "~2.0.0",
"expo-av": "~8.6.0",
"expo-image-picker": "~9.1.1",
+ "expo-localization": "^9.0.0",
"expo-secure-store": "~9.2.0",
"expo-splash-screen": "~0.6.1",
"expo-status-bar": "~1.0.2",
+ "i18next": "^19.8.4",
"ky": "^0.24.0",
"lodash": "^4.17.20",
"react": "16.13.1",
"react-dom": "16.13.1",
+ "react-i18next": "^11.7.3",
"react-native": "https://github.com/expo/react-native/archive/sdk-39.0.3.tar.gz",
"react-native-appearance": "~0.3.3",
"react-native-collapsible": "^1.5.3",
@@ -60,4 +63,4 @@
"typescript": "~3.9.2"
},
"private": true
-}
\ No newline at end of file
+}
diff --git a/src/components/Menu/Container.tsx b/src/components/Menu/Container.tsx
index 4a87f0d9..0caa5d78 100644
--- a/src/components/Menu/Container.tsx
+++ b/src/components/Menu/Container.tsx
@@ -4,11 +4,24 @@ import { useTheme } from 'src/utils/styles/ThemeManager'
import constants from 'src/utils/styles/constants'
-const MenuContainer: React.FC = ({ ...props }) => {
+export interface Props {
+ children: React.ReactNode
+ marginTop?: boolean
+}
+
+const MenuContainer: React.FC = ({ ...props }) => {
const { theme } = useTheme()
return (
-
+
{props.children}
)
@@ -17,7 +30,7 @@ const MenuContainer: React.FC = ({ ...props }) => {
const styles = StyleSheet.create({
base: {
borderTopWidth: 1,
- marginBottom: constants.SPACING_M
+ marginBottom: constants.GLOBAL_PAGE_PADDING
}
})
diff --git a/src/components/Menu/Item.tsx b/src/components/Menu/Item.tsx
index ba70c828..39f6b412 100644
--- a/src/components/Menu/Item.tsx
+++ b/src/components/Menu/Item.tsx
@@ -1,61 +1,82 @@
import React from 'react'
import { Pressable, StyleSheet, Text, View } from 'react-native'
-import { useNavigation } from '@react-navigation/native'
import { Feather } from '@expo/vector-icons'
import { useTheme } from 'src/utils/styles/ThemeManager'
import constants from 'src/utils/styles/constants'
+import { ColorDefinitions } from 'src/utils/styles/themes'
export interface Props {
- icon?: string
+ iconFront?: string
+ iconFrontColor?: ColorDefinitions
title: string
- navigateTo?: string
- navigateToParams?: {}
+ content?: string
+ iconBack?: 'chevron-right' | 'check'
+ iconBackColor?: ColorDefinitions
+ onPress?: () => void
}
-const Core: React.FC = ({ icon, title, navigateTo }) => {
+const Core: React.FC = ({
+ iconFront,
+ iconFrontColor,
+ title,
+ content,
+ iconBack,
+ iconBackColor
+}) => {
const { theme } = useTheme()
+ iconFrontColor = iconFrontColor || 'primary'
+ iconBackColor = iconBackColor || 'secondary'
return (
- {icon && (
-
- )}
-
- {title}
-
- {navigateTo && (
-
- )}
+
+ {iconFront && (
+
+ )}
+
+ {title}
+
+
+
+ {content && (
+
+ {content}
+
+ )}
+ {iconBack && (
+
+ )}
+
)
}
const MenuItem: React.FC = ({ ...props }) => {
const { theme } = useTheme()
- const navigation = useNavigation()
- return props.navigateTo ? (
+ return props.onPress ? (
{
- navigation.navigate(props.navigateTo!, props.navigateToParams)
- }}
+ onPress={props.onPress}
>
) : (
-
+
)
@@ -69,15 +90,31 @@ const styles = StyleSheet.create({
core: {
flex: 1,
flexDirection: 'row',
- alignItems: 'center',
paddingLeft: constants.GLOBAL_PAGE_PADDING,
paddingRight: constants.GLOBAL_PAGE_PADDING
},
- iconLeading: {
+ front: {
+ flex: 1,
+ flexDirection: 'row',
+ alignItems: 'center'
+ },
+ back: {
+ flex: 1,
+ flexDirection: 'row',
+ justifyContent: 'flex-end',
+ alignItems: 'center'
+ },
+ iconFront: {
marginRight: 8
},
- iconNavigation: {
- marginLeft: 'auto'
+ text: {
+ fontSize: constants.FONT_SIZE_M
+ },
+ content: {
+ fontSize: constants.FONT_SIZE_M
+ },
+ iconBack: {
+ marginLeft: 8
}
})
diff --git a/src/components/Timelines.tsx b/src/components/Timelines.tsx
index d7b0edcb..c6a84963 100644
--- a/src/components/Timelines.tsx
+++ b/src/components/Timelines.tsx
@@ -1,5 +1,5 @@
import React, { useCallback, useEffect, useRef, useState } from 'react'
-import { Dimensions, FlatList, StyleSheet, Text, View } from 'react-native'
+import { Dimensions, FlatList, StyleSheet, View } from 'react-native'
import SegmentedControl from '@react-native-community/segmented-control'
import { createNativeStackNavigator } from 'react-native-screens/native-stack'
import { useSelector } from 'react-redux'
@@ -7,8 +7,11 @@ import { Feather } from '@expo/vector-icons'
import Timeline from './Timelines/Timeline'
import sharedScreens from 'src/screens/Shared/sharedScreens'
-import { getRemoteUrl, InstancesState } from 'src/utils/slices/instancesSlice'
-import { RootState, store } from 'src/store'
+import {
+ getLocalUrl,
+ getRemoteUrl,
+ InstancesState
+} from 'src/utils/slices/instancesSlice'
import { useTheme } from 'src/utils/styles/ThemeManager'
import { useNavigation } from '@react-navigation/native'
import getCurrentTab from 'src/utils/getCurrentTab'
@@ -42,15 +45,10 @@ export interface Props {
const Timelines: React.FC = ({ name, content }) => {
const navigation = useNavigation()
const { theme } = useTheme()
- const localRegistered = useSelector(
- (state: RootState) => state.instances.local.url
- )
- const publicDomain = getRemoteUrl(store.getState())
+ const localRegistered = useSelector(getLocalUrl)
+ const publicDomain = useSelector(getRemoteUrl)
const [segment, setSegment] = useState(0)
const [renderHeader, setRenderHeader] = useState(false)
- const [segmentManuallyTriggered, setSegmentManuallyTriggered] = useState(
- false
- )
useEffect(() => {
const nbr = setTimeout(() => setRenderHeader(true), 50)
@@ -60,8 +58,6 @@ const Timelines: React.FC = ({ name, content }) => {
const horizontalPaging = useRef(null!)
const onChangeSegment = useCallback(({ nativeEvent }) => {
- setSegmentManuallyTriggered(true)
- setSegment(nativeEvent.selectedSegmentIndex)
horizontalPaging.current.scrollToIndex({
index: nativeEvent.selectedSegmentIndex
})
@@ -90,13 +86,8 @@ const Timelines: React.FC = ({ name, content }) => {
},
[localRegistered]
)
- const flOnMomentumScrollEnd = useCallback(
- () => setSegmentManuallyTriggered(false),
- []
- )
const flOnScroll = useCallback(
({ nativeEvent }) =>
- !segmentManuallyTriggered &&
setSegment(
nativeEvent.contentOffset.x <= Dimensions.get('window').width / 2
? 0
@@ -114,12 +105,13 @@ const Timelines: React.FC = ({ name, content }) => {
...(renderHeader &&
localRegistered && {
headerCenter: () => (
-
+
+
+
),
headerRight: () => (
= ({ name, content }) => {
keyExtractor={flKeyExtrator}
getItemLayout={flGetItemLayout}
showsHorizontalScrollIndicator={false}
- onMomentumScrollEnd={flOnMomentumScrollEnd}
/>
)
}}
@@ -159,6 +150,9 @@ const Timelines: React.FC = ({ name, content }) => {
}
const styles = StyleSheet.create({
+ segmentsContainer: {
+ flexBasis: '60%'
+ },
flatList: {
width: Dimensions.get('window').width,
height: '100%'
diff --git a/src/components/Timelines/Timeline/Shared/ActionsStatus.tsx b/src/components/Timelines/Timeline/Shared/ActionsStatus.tsx
index b3a7f1b3..bc7a5e22 100644
--- a/src/components/Timelines/Timeline/Shared/ActionsStatus.tsx
+++ b/src/components/Timelines/Timeline/Shared/ActionsStatus.tsx
@@ -8,17 +8,15 @@ import {
Text,
View
} from 'react-native'
-import Toast from 'react-native-toast-message'
import { useMutation, useQueryCache } from 'react-query'
import { Feather } from '@expo/vector-icons'
-import { findIndex } from 'lodash'
import client from 'src/api/client'
import { getLocalAccountId } from 'src/utils/slices/instancesSlice'
-import { store } from 'src/store'
import { useTheme } from 'src/utils/styles/ThemeManager'
import constants from 'src/utils/styles/constants'
import { toast } from 'src/components/toast'
+import { useSelector } from 'react-redux'
const fireMutation = async ({
id,
@@ -87,7 +85,7 @@ const ActionsStatus: React.FC = ({ queryKey, status }) => {
const iconColorAction = (state: boolean) =>
state ? theme.primary : theme.secondary
- const localAccountId = getLocalAccountId(store.getState())
+ const localAccountId = useSelector(getLocalAccountId)
const [bottomSheetVisible, setBottomSheetVisible] = useState(false)
const queryCache = useQueryCache()
diff --git a/src/components/Timelines/Timeline/Shared/Avatar.tsx b/src/components/Timelines/Timeline/Shared/Avatar.tsx
index 6f820cee..2aadf714 100644
--- a/src/components/Timelines/Timeline/Shared/Avatar.tsx
+++ b/src/components/Timelines/Timeline/Shared/Avatar.tsx
@@ -34,7 +34,7 @@ const styles = StyleSheet.create({
image: {
width: '100%',
height: '100%',
- borderRadius: 8
+ borderRadius: 6
}
})
diff --git a/src/components/Timelines/Timeline/Shared/HeaderDefault.tsx b/src/components/Timelines/Timeline/Shared/HeaderDefault.tsx
index 06c15be5..4f414491 100644
--- a/src/components/Timelines/Timeline/Shared/HeaderDefault.tsx
+++ b/src/components/Timelines/Timeline/Shared/HeaderDefault.tsx
@@ -2,19 +2,18 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react'
import { Pressable, StyleSheet, Text, View } from 'react-native'
import { useNavigation } from '@react-navigation/native'
import { Feather } from '@expo/vector-icons'
-import Toast from 'react-native-toast-message'
import { useMutation, useQueryCache } from 'react-query'
import Emojis from './Emojis'
import relativeTime from 'src/utils/relativeTime'
import client from 'src/api/client'
import { getLocalAccountId, getLocalUrl } from 'src/utils/slices/instancesSlice'
-import { store } from 'src/store'
import { useTheme } from 'src/utils/styles/ThemeManager'
import constants from 'src/utils/styles/constants'
import BottomSheet from 'src/components/BottomSheet'
import BottomSheetRow from 'src/components/BottomSheet/Row'
import { toast } from 'src/components/toast'
+import { useSelector } from 'react-redux'
const fireMutation = async ({
id,
@@ -115,8 +114,8 @@ const HeaderDefault: React.FC = ({
const { theme } = useTheme()
const navigation = useNavigation()
- const localAccountId = getLocalAccountId(store.getState())
- const localDomain = getLocalUrl(store.getState())
+ const localAccountId = useSelector(getLocalAccountId)
+ const localDomain = useSelector(getLocalUrl)
const [since, setSince] = useState(relativeTime(created_at))
const [modalVisible, setModalVisible] = useState(false)
diff --git a/src/i18n/en/_all.ts b/src/i18n/en/_all.ts
new file mode 100644
index 00000000..5f745620
--- /dev/null
+++ b/src/i18n/en/_all.ts
@@ -0,0 +1,3 @@
+export default {
+ common: require('./common').default
+}
diff --git a/src/i18n/en/common.ts b/src/i18n/en/common.ts
new file mode 100644
index 00000000..43602fbe
--- /dev/null
+++ b/src/i18n/en/common.ts
@@ -0,0 +1,31 @@
+export default {
+ headers: {
+ local: {
+ segments: {
+ left: 'Following',
+ right: 'Local'
+ }
+ },
+ public: {
+ segments: {
+ left: 'Federated',
+ right: 'Others'
+ }
+ },
+ notifications: 'Notifications',
+ me: {
+ root: 'My Mastodon',
+ conversations: 'Messages',
+ bookmarks: 'Booksmarks',
+ favourites: 'Favourites',
+ lists: {
+ root: 'Lists',
+ list: 'List {{list}}'
+ },
+ settings: {
+ root: 'Settings',
+ language: 'Language'
+ }
+ }
+ }
+}
diff --git a/src/i18n/i18n.ts b/src/i18n/i18n.ts
new file mode 100644
index 00000000..f138cd83
--- /dev/null
+++ b/src/i18n/i18n.ts
@@ -0,0 +1,50 @@
+import i18next from 'i18next'
+import { initReactI18next } from 'react-i18next'
+import * as Localization from 'expo-localization'
+
+import zh from 'src/i18n/zh/_all'
+import en from 'src/i18n/en/_all'
+import {
+ changeLanguage,
+ getSettingsLanguage
+} from 'src/utils/slices/settingsSlice'
+import { store } from 'src/store'
+ console.log(store.getState())
+if (!getSettingsLanguage(store.getState())) {
+ console.log('No default locale of app')
+ const deviceLocal = Localization.locale
+ if (deviceLocal.startsWith('zh')) {
+ store.dispatch(changeLanguage('zh'))
+ } else {
+ store.dispatch(changeLanguage('en'))
+ }
+}
+
+i18next.use(initReactI18next).init({
+ lng: getSettingsLanguage(store.getState()),
+ fallbackLng: 'en',
+ supportedLngs: ['zh', 'en'],
+ nonExplicitSupportedLngs: true,
+
+ ns: ['common'],
+ defaultNS: 'common',
+
+ resources: {
+ zh: zh,
+ en: en
+ },
+
+ saveMissing: true,
+ missingKeyHandler: (lng, ns, key, fallbackValue) => {
+ console.warn('i18n missing: ' + ns + ' : ' + key)
+ },
+
+ // react options
+ interpolation: {
+ escapeValue: false
+ },
+
+ debug: true
+})
+
+export default i18next
diff --git a/src/i18n/zh/_all.ts b/src/i18n/zh/_all.ts
new file mode 100644
index 00000000..e67184ea
--- /dev/null
+++ b/src/i18n/zh/_all.ts
@@ -0,0 +1,4 @@
+export default {
+ common: require('./common').default,
+ settings: require('./settings').default
+}
diff --git a/src/i18n/zh/common.ts b/src/i18n/zh/common.ts
new file mode 100644
index 00000000..e703cd6e
--- /dev/null
+++ b/src/i18n/zh/common.ts
@@ -0,0 +1,31 @@
+export default {
+ headers: {
+ local: {
+ segments: {
+ left: '我的关注',
+ right: '本站嘟嘟'
+ }
+ },
+ public: {
+ segments: {
+ left: '跨站关注',
+ right: '外站嘟嘟'
+ }
+ },
+ notifications: '我的通知',
+ me: {
+ root: '我的长毛象',
+ conversations: '私信',
+ bookmarks: '书签',
+ favourites: '喜欢',
+ lists: {
+ root: '列表',
+ list: '列表 {{list}}'
+ },
+ settings: {
+ root: '设置',
+ language: '语言'
+ }
+ }
+ }
+}
diff --git a/src/i18n/zh/settings.ts b/src/i18n/zh/settings.ts
new file mode 100644
index 00000000..12ff856e
--- /dev/null
+++ b/src/i18n/zh/settings.ts
@@ -0,0 +1,11 @@
+export default {
+ content: {
+ language: {
+ title: '切换语言',
+ options: {
+ zh: '简体中文',
+ en: 'English'
+ }
+ }
+ }
+}
diff --git a/src/screens/Local.tsx b/src/screens/Local.tsx
index daac16de..53822067 100644
--- a/src/screens/Local.tsx
+++ b/src/screens/Local.tsx
@@ -1,14 +1,17 @@
import React from 'react'
+import { useTranslation } from 'react-i18next'
import Timelines from 'src/components/Timelines'
const ScreenLocal: React.FC = () => {
+ const { t } = useTranslation()
+
return (
)
diff --git a/src/screens/Me.tsx b/src/screens/Me.tsx
index b028088c..12644965 100644
--- a/src/screens/Me.tsx
+++ b/src/screens/Me.tsx
@@ -1,5 +1,6 @@
import React from 'react'
import { createNativeStackNavigator } from 'react-native-screens/native-stack'
+import { useTranslation } from 'react-i18next'
import { useSelector } from 'react-redux'
import ScreenMeRoot from 'src/screens/Me/Root'
@@ -16,6 +17,7 @@ import { RootState } from 'src/store'
const Stack = createNativeStackNavigator()
const ScreenMe: React.FC = () => {
+ const { t } = useTranslation()
const localRegistered = useSelector(
(state: RootState) => state.instances.local.url
)
@@ -32,49 +34,49 @@ const ScreenMe: React.FC = () => {
headerStyle: { backgroundColor: 'rgba(255, 255, 255, 0)' },
headerCenter: () => <>>
}
- : { headerTitle: '我的长毛象' }
+ : { headerTitle: t('headers.me.root') }
}
/>
({
- headerTitle: `列表:${route.params.title}`
+ headerTitle: t('headers.me.lists.list', { list: route.params.title })
})}
/>
diff --git a/src/screens/Me/Lists.tsx b/src/screens/Me/Lists.tsx
index 63e5d5d5..58bc3f32 100644
--- a/src/screens/Me/Lists.tsx
+++ b/src/screens/Me/Lists.tsx
@@ -1,3 +1,4 @@
+import { useNavigation } from '@react-navigation/native'
import React from 'react'
import { ActivityIndicator, Text } from 'react-native'
import { useQuery } from 'react-query'
@@ -6,6 +7,7 @@ import { MenuContainer, MenuItem } from 'src/components/Menu'
import { listsFetch } from 'src/utils/fetches/listsFetch'
const ScreenMeLists: React.FC = () => {
+ const navigation = useNavigation()
const { status, data } = useQuery(['Lists'], listsFetch)
let lists
@@ -20,13 +22,14 @@ const ScreenMeLists: React.FC = () => {
lists = data?.map((d: Mastodon.List, i: number) => (