Localization support (#99)

* basic i18n poc

* translate home, filters, tabs

support dot notation in backend for namespaces

* i18n context menu, artist filters, list controls

also nothings here
fix backend not caching fallback

* i18n queue, artist view, search/results

* i18n settings and server view

* Added translation using Weblate (Norwegian Bokmål)

* Translated using Weblate (Norwegian Bokmål)

Currently translated at 100.0% (6 of 6 strings)

Translation: Subtracks/subtracks
Translate-URL: https://hosted.weblate.org/projects/subtracks/subtracks/nb_NO/

* Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Translation: Subtracks/subtracks
Translate-URL: https://hosted.weblate.org/projects/subtracks/subtracks/

* Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Translation: Subtracks/subtracks
Translate-URL: https://hosted.weblate.org/projects/subtracks/subtracks/

* Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Translation: Subtracks/subtracks
Translate-URL: https://hosted.weblate.org/projects/subtracks/subtracks/

* fix url escaping

* added some mostly naive text overflow fixes

rewrote filter context menu as a slide in because the old one apparently can't handle dynamic width

* Added translation using Weblate (French)

* Translated using Weblate (French)

Currently translated at 17.4% (11 of 63 strings)

Translation: Subtracks/subtracks
Translate-URL: https://hosted.weblate.org/projects/subtracks/subtracks/fr/

* Translated using Weblate (French)

Currently translated at 19.0% (12 of 63 strings)

Translation: Subtracks/subtracks
Translate-URL: https://hosted.weblate.org/projects/subtracks/subtracks/fr/

* Translated using Weblate (French)

Currently translated at 40.0% (26 of 65 strings)

Translation: Subtracks/subtracks
Translate-URL: https://hosted.weblate.org/projects/subtracks/subtracks/fr/

* add weblate and some pretty badges to readme

* fix link

* Translated using Weblate (French)

Currently translated at 50.7% (33 of 65 strings)

Translation: Subtracks/subtracks
Translate-URL: https://hosted.weblate.org/projects/subtracks/subtracks/fr/

* Translated using Weblate (English)

Currently translated at 100.0% (65 of 65 strings)

Translation: Subtracks/subtracks
Translate-URL: https://hosted.weblate.org/projects/subtracks/subtracks/en/

* Translated using Weblate (French)

Currently translated at 90.7% (59 of 65 strings)

Translation: Subtracks/subtracks
Translate-URL: https://hosted.weblate.org/projects/subtracks/subtracks/fr/

* i18n now playing context type

fix overscroll on new filter menu
fix getting default namespace from the i18n backend

* Translated using Weblate (French)

Currently translated at 96.9% (63 of 65 strings)

Translation: Subtracks/subtracks
Translate-URL: https://hosted.weblate.org/projects/subtracks/subtracks/fr/

* Translated using Weblate (French)

Currently translated at 100.0% (66 of 66 strings)

Translation: Subtracks/subtracks
Translate-URL: https://hosted.weblate.org/projects/subtracks/subtracks/fr/

* Translated using Weblate (Japanese) (#98)

Currently translated at 7.5% (5 of 66 strings)

Translation: Subtracks/subtracks
Translate-URL: https://hosted.weblate.org/projects/subtracks/subtracks/ja/

Co-authored-by: Austin Riedhammer <4966622+austinried@users.noreply.github.com>

* little note to remind me why that's there

* update licenses

Co-authored-by: Allan Nordhøy <epost@anotheragency.no>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Clyhtsuriva <aimeric@adjutor.xyz>
This commit is contained in:
austinried
2022-04-15 12:11:00 +09:00
committed by GitHub
co-authored by Allan Nordhøy Hosted Weblate Clyhtsuriva
parent abc5050acd
commit d253704c13
35 changed files with 1186 additions and 316 deletions
+46 -47
View File
@@ -5,13 +5,15 @@ import PressableOpacity from '@app/components/PressableOpacity'
import SettingsItem from '@app/components/SettingsItem'
import SettingsSwitch from '@app/components/SettingsSwitch'
import TextInput from '@app/components/TextInput'
import { useSwitchActiveServer, useResetImageCache } from '@app/hooks/settings'
import { withSuspenseMemo } from '@app/components/withSuspense'
import { useResetImageCache, useSwitchActiveServer } from '@app/hooks/settings'
import { Server } from '@app/models/settings'
import { useStore, useStoreDeep } from '@app/state/store'
import colors from '@app/styles/colors'
import font from '@app/styles/font'
import { useNavigation } from '@react-navigation/core'
import React, { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { KeyboardTypeOptions, Linking, Modal, Pressable, StyleSheet, Text, View } from 'react-native'
import { ScrollView } from 'react-native-gesture-handler'
import { useSafeAreaInsets } from 'react-native-safe-area-context'
@@ -73,25 +75,23 @@ const ModalChoice = React.memo<{
)
})
function bitrateString(bitrate: number): string {
return bitrate === 0 ? 'Unlimited' : `${bitrate}kbps`
}
const BitrateModal = React.memo<{
const BitrateModal = withSuspenseMemo<{
title: string
bitrate: number
setBitrate: (bitrate: number) => void
}>(({ title, bitrate, setBitrate }) => {
const { t } = useTranslation('settings.network.values')
const [visible, setVisible] = useState(false)
const toggleModal = useCallback(() => setVisible(!visible), [visible])
const bitrateText = useCallback((value: number) => (value === 0 ? t('unlimitedKbps') : t('kbps', { value })), [t])
const BitrateChoice: React.FC<{ value: number }> = useCallback(
({ value }) => {
const text = bitrateString(value)
return (
<ModalChoice
text={text}
text={bitrateText(value)}
value={value}
setValue={setBitrate}
closeModal={toggleModal}
@@ -99,12 +99,12 @@ const BitrateModal = React.memo<{
/>
)
},
[bitrate, toggleModal, setBitrate],
[bitrate, toggleModal, setBitrate, bitrateText],
)
return (
<>
<SettingsItem title={title} subtitle={bitrateString(bitrate)} onPress={toggleModal} />
<SettingsItem title={title} subtitle={bitrateText(bitrate)} onPress={toggleModal} />
<Modal animationType="fade" transparent={true} visible={visible} onRequestClose={toggleModal}>
<Pressable style={styles.modalBackdrop} onPress={toggleModal}>
<View style={styles.centeredView}>
@@ -135,9 +135,9 @@ const SettingsTextModal = React.memo<{
title: string
value: string
setValue: (text: string) => void
getUnit?: (text: string) => string
subtitle: (value: string) => string
keyboardType?: KeyboardTypeOptions
}>(({ title, value, setValue, getUnit, keyboardType }) => {
}>(({ title, value, setValue, subtitle, keyboardType }) => {
const [visible, setVisible] = useState(false)
const [inputText, setInputText] = useState(value)
@@ -148,16 +148,9 @@ const SettingsTextModal = React.memo<{
toggleModal()
}, [inputText, setValue, toggleModal])
const getSubtitle = useCallback(() => {
if (!getUnit) {
return value
}
return value + ' ' + getUnit(value)
}, [getUnit, value])
return (
<>
<SettingsItem title={title} subtitle={getSubtitle()} onPress={toggleModal} />
<SettingsItem title={title} subtitle={subtitle(value)} onPress={toggleModal} />
<Modal animationType="fade" transparent={true} visible={visible} onRequestClose={toggleModal}>
<Pressable style={styles.modalBackdrop} onPress={toggleModal}>
<View style={styles.centeredView}>
@@ -183,15 +176,9 @@ const SettingsTextModal = React.memo<{
)
})
function secondsUnit(seconds: string): string {
const numberValue = parseFloat(seconds)
if (Math.abs(numberValue) !== 1) {
return 'seconds'
}
return 'second'
}
const SettingsContent = withSuspenseMemo(() => {
const { t } = useTranslation('settings')
const SettingsContent = React.memo(() => {
const servers = useStoreDeep(store => store.settings.servers)
const scrobble = useStore(store => store.settings.scrobble)
const setScrobble = useStore(store => store.setScrobble)
@@ -221,66 +208,78 @@ const SettingsContent = React.memo(() => {
const setMinBufferText = useCallback((text: string) => setMinBuffer(parseFloat(text)), [setMinBuffer])
const setMaxBufferText = useCallback((text: string) => setMaxBuffer(parseFloat(text)), [setMaxBuffer])
const secondsText = useCallback((value: string) => t('network.values.seconds', { value }), [t])
return (
<View style={styles.content}>
<Header>Servers</Header>
<Header>{t('servers.name')}</Header>
{Object.values(servers).map(s => (
<ServerItem key={s.id} server={s} />
))}
<Button
style={styles.button}
title="Add Server"
title={t('servers.actions.add')}
onPress={() => navigation.navigate('server')}
buttonStyle="hollow"
/>
<Header style={styles.header}>Network</Header>
<BitrateModal title="Maximum bitrate (Wi-Fi)" bitrate={maxBitrateWifi} setBitrate={setMaxBitrateWifi} />
<BitrateModal title="Maximum bitrate (mobile)" bitrate={maxBitrateMobile} setBitrate={setMaxBitrateMobile} />
<Header style={styles.header}>{t('network.name')}</Header>
<BitrateModal
title={t('network.options.maxBitrateWifi.title')}
bitrate={maxBitrateWifi}
setBitrate={setMaxBitrateWifi}
/>
<BitrateModal
title={t('network.options.maxBitrateMobile.title')}
bitrate={maxBitrateMobile}
setBitrate={setMaxBitrateMobile}
/>
<SettingsTextModal
title="Minimum buffer time"
title={t('network.options.minBuffer.title')}
value={minBuffer.toString()}
setValue={setMinBufferText}
getUnit={secondsUnit}
subtitle={secondsText}
keyboardType="numeric"
/>
<SettingsTextModal
title="Maximum buffer time"
title={t('network.options.maxBuffer.title')}
value={maxBuffer.toString()}
setValue={setMaxBufferText}
getUnit={secondsUnit}
subtitle={secondsText}
keyboardType="numeric"
/>
<Header style={styles.header}>Music</Header>
<Header style={styles.header}>{t('music.name')}</Header>
<SettingsSwitch
title="Scrobble plays"
subtitle={scrobble ? 'Scrobble play history' : "Don't scrobble play history"}
title={t('music.options.scrobble.title')}
subtitle={scrobble ? t('music.options.scrobble.descriptionOn') : t('music.options.scrobble.descriptionOff')}
value={scrobble}
setValue={setScrobble}
/>
<Header style={styles.header}>Reset</Header>
<Header style={styles.header}>{t('reset.name')}</Header>
<Button
disabled={clearing}
style={styles.button}
title="Clear Image Cache"
title={t('reset.actions.clearImageCache')}
onPress={clear}
buttonStyle="hollow"
/>
<Header style={styles.header}>About</Header>
<Header style={styles.header}>{t('about.name')}</Header>
<Text style={styles.text}>
<Text style={styles.bold}>Subtracks</Text> version {version}
<Text style={styles.bold}>Subtracks</Text> {t('about.version', { version })}
</Text>
<Button
disabled={clearing}
style={styles.button}
title="Project Homepage"
title={t('about.actions.projectHomepage')}
onPress={() => Linking.openURL('https://github.com/austinried/subtracks')}
buttonStyle="hollow"
/>
<Button
disabled={clearing}
style={styles.button}
title="Licenses"
onPress={() => navigation.navigate('web', { uri: 'file:///android_asset/licenses.html' })}
title={t('about.actions.licenses')}
onPress={() =>
navigation.navigate('web', { uri: 'file:///android_asset/licenses.html', title: t('about.actions.licenses') })
}
buttonStyle="hollow"
/>
</View>