├── .watchmanconfig ├── .eslintignore ├── android ├── app │ ├── src │ │ ├── main │ │ │ ├── play │ │ │ │ ├── contact-email.txt │ │ │ │ ├── listings │ │ │ │ │ ├── en-US │ │ │ │ │ │ ├── title.txt │ │ │ │ │ │ ├── short-description.txt │ │ │ │ │ │ ├── graphics │ │ │ │ │ │ │ ├── icon │ │ │ │ │ │ │ │ └── icon.png │ │ │ │ │ │ │ └── phone-screenshots │ │ │ │ │ │ │ │ ├── 1.png │ │ │ │ │ │ │ │ ├── 2.png │ │ │ │ │ │ │ │ ├── 3.png │ │ │ │ │ │ │ │ ├── 4.png │ │ │ │ │ │ │ │ ├── 5.png │ │ │ │ │ │ │ │ ├── 6.png │ │ │ │ │ │ │ │ ├── 7.png │ │ │ │ │ │ │ │ ├── 8.png │ │ │ │ │ │ │ │ ├── 9.png │ │ │ │ │ │ │ │ ├── 10.png │ │ │ │ │ │ │ │ └── 11.png │ │ │ │ │ │ └── full-description.txt │ │ │ │ │ └── zh-CN │ │ │ │ │ │ ├── title.txt │ │ │ │ │ │ ├── short-description.txt │ │ │ │ │ │ ├── graphics │ │ │ │ │ │ ├── icon │ │ │ │ │ │ │ └── icon.png │ │ │ │ │ │ └── phone-screenshots │ │ │ │ │ │ │ ├── 1.png │ │ │ │ │ │ │ ├── 2.png │ │ │ │ │ │ │ ├── 3.png │ │ │ │ │ │ │ ├── 4.png │ │ │ │ │ │ │ ├── 5.png │ │ │ │ │ │ │ ├── 6.png │ │ │ │ │ │ │ ├── 7.png │ │ │ │ │ │ │ ├── 8.png │ │ │ │ │ │ │ ├── 9.png │ │ │ │ │ │ │ └── 10.png │ │ │ │ │ │ └── full-description.txt │ │ │ │ ├── contact-website.txt │ │ │ │ └── release-notes │ │ │ │ │ └── en-US │ │ │ │ │ └── default.txt │ │ │ ├── res │ │ │ │ ├── values │ │ │ │ │ ├── strings.xml │ │ │ │ │ └── styles.xml │ │ │ │ ├── mipmap-hdpi │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ ├── ic_launcher_background.png │ │ │ │ │ ├── ic_launcher_foreground.png │ │ │ │ │ └── ic_launcher_monochrome.png │ │ │ │ ├── mipmap-mdpi │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ ├── ic_launcher_background.png │ │ │ │ │ ├── ic_launcher_foreground.png │ │ │ │ │ └── ic_launcher_monochrome.png │ │ │ │ ├── mipmap-xhdpi │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ ├── ic_launcher_background.png │ │ │ │ │ ├── ic_launcher_foreground.png │ │ │ │ │ └── ic_launcher_monochrome.png │ │ │ │ ├── mipmap-xxhdpi │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ ├── ic_launcher_background.png │ │ │ │ │ ├── ic_launcher_foreground.png │ │ │ │ │ └── ic_launcher_monochrome.png │ │ │ │ ├── mipmap-xxxhdpi │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ ├── ic_launcher_background.png │ │ │ │ │ ├── ic_launcher_foreground.png │ │ │ │ │ └── ic_launcher_monochrome.png │ │ │ │ ├── mipmap-anydpi-v26 │ │ │ │ │ └── ic_launcher.xml │ │ │ │ └── drawable │ │ │ │ │ └── rn_edit_text_material.xml │ │ │ ├── AndroidManifest.xml │ │ │ └── java │ │ │ │ └── com │ │ │ │ └── gmail │ │ │ │ └── blandilyte │ │ │ │ └── silverdict │ │ │ │ ├── MainApplication.kt │ │ │ │ └── MainActivity.kt │ │ └── debug │ │ │ └── AndroidManifest.xml │ ├── debug.keystore │ ├── proguard-rules.pro │ └── build.gradle ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── settings.gradle ├── build.gradle ├── gradle.properties └── gradlew.bat ├── .bundle └── config ├── app.json ├── tsconfig.json ├── ios ├── SilverDict │ ├── Images.xcassets │ │ ├── Contents.json │ │ └── AppIcon.appiconset │ │ │ └── Contents.json │ ├── AppDelegate.h │ ├── main.m │ ├── AppDelegate.mm │ ├── PrivacyInfo.xcprivacy │ ├── Info.plist │ └── LaunchScreen.storyboard ├── .xcode.env ├── SilverDictTests │ ├── Info.plist │ └── SilverDictTests.m ├── Podfile └── SilverDict.xcodeproj │ └── xcshareddata │ └── xcschemes │ └── SilverDict.xcscheme ├── babel.config.js ├── crowdin.yml ├── metro.config.js ├── Gemfile ├── src ├── translations │ ├── l10n.js │ ├── zh-CN.json │ ├── zh-TW.json │ ├── ar-SA.json │ ├── en-US.json │ ├── es-ES.json │ ├── de-DE.json │ ├── el-GR.json │ └── en-GB.json ├── components │ ├── LibraryScreen │ │ ├── GroupManager │ │ │ ├── GroupDictionaryEditorDialogueItem.js │ │ │ ├── ConfirmDeleteDialogue.js │ │ │ ├── GroupNameEditorDialogue.js │ │ │ ├── GroupLanguageEditorDialogue.js │ │ │ ├── GroupDictionaryEditorDialogue.js │ │ │ ├── AddGroupDialogue.js │ │ │ └── GroupCard.js │ │ ├── DictionaryManager │ │ │ ├── EditMenu.js │ │ │ ├── ConfirmDeleteDialogue.js │ │ │ ├── DictionaryNameEditorDialogue.js │ │ │ └── AddDictionaryDialogue.js │ │ ├── LibraryNavigationBar.js │ │ ├── SourceManager.js │ │ ├── GroupManager.js │ │ ├── SourceManager │ │ │ ├── ConfirmDeleteDialogue.js │ │ │ └── AddSourceDialogue.js │ │ ├── LibraryAppbar.js │ │ └── DictionaryManager.js │ ├── common │ │ ├── InProgressDialogue.js │ │ ├── ConfirmDialogue.js │ │ └── TextEditorDialogue.js │ ├── QueryScreen.js │ ├── QueryScreen │ │ ├── QueryBar.js │ │ ├── Input.js │ │ ├── QueryContent.js │ │ ├── DictionarySelection.js │ │ └── ArticleBottomBar.js │ ├── SettingsScreen │ │ ├── ChangeColourDialogue.js │ │ ├── ConfirmClearHistoryDialogue.js │ │ ├── ChangeFontDialogue.js │ │ ├── ChangeAddressDialogue.js │ │ ├── ConfirmRecreateNgramDialogue.js │ │ ├── ChangeSizeSuggestionDialogue.js │ │ ├── ChangeSizeHistoryDialogue.js │ │ ├── AdditionalFontsDialogue.js │ │ └── DarkReaderConfigDialogue.js │ ├── LibraryScreen.js │ ├── DrawerContent.js │ └── SettingsScreen.js ├── App.js ├── utils.js ├── config.js └── AppContext.js ├── .eslintrc.json ├── index.js ├── .gitignore ├── package.json └── README.md /.watchmanconfig: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | /src/components/QueryScreen/darkreader.js -------------------------------------------------------------------------------- /android/app/src/main/play/contact-email.txt: -------------------------------------------------------------------------------- 1 | blandilyte@gmail.com -------------------------------------------------------------------------------- /android/app/src/main/play/listings/en-US/title.txt: -------------------------------------------------------------------------------- 1 | SilverDict -------------------------------------------------------------------------------- /android/app/src/main/play/listings/zh-CN/title.txt: -------------------------------------------------------------------------------- 1 | SilverDict -------------------------------------------------------------------------------- /.bundle/config: -------------------------------------------------------------------------------- 1 | BUNDLE_PATH: "vendor/bundle" 2 | BUNDLE_FORCE_RUBY_PLATFORM: 1 3 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "SilverDict", 3 | "displayName": "SilverDict" 4 | } 5 | -------------------------------------------------------------------------------- /android/app/src/main/play/contact-website.txt: -------------------------------------------------------------------------------- 1 | https://silverdict.lecoteauverdoyant.co.uk/ -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@react-native/typescript-config/tsconfig.json" 3 | } -------------------------------------------------------------------------------- /android/app/src/main/play/listings/zh-CN/short-description.txt: -------------------------------------------------------------------------------- 1 | SilverDict 词典服务器 Android 客户端 -------------------------------------------------------------------------------- /android/app/src/main/play/listings/en-US/short-description.txt: -------------------------------------------------------------------------------- 1 | Android client of the SilverDict dictionary server -------------------------------------------------------------------------------- /android/app/debug.keystore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Crissium/SilverDict-mobile/HEAD/android/app/debug.keystore -------------------------------------------------------------------------------- /android/app/src/main/play/release-notes/en-US/default.txt: -------------------------------------------------------------------------------- 1 | Allow launching from the text selection menu in other apps. -------------------------------------------------------------------------------- /android/app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | SilverDict 3 | 4 | -------------------------------------------------------------------------------- /ios/SilverDict/Images.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Crissium/SilverDict-mobile/HEAD/android/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /ios/SilverDict/AppDelegate.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | 4 | @interface AppDelegate : RCTAppDelegate 5 | 6 | @end 7 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Crissium/SilverDict-mobile/HEAD/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Crissium/SilverDict-mobile/HEAD/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Crissium/SilverDict-mobile/HEAD/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Crissium/SilverDict-mobile/HEAD/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Crissium/SilverDict-mobile/HEAD/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/play/listings/en-US/graphics/icon/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Crissium/SilverDict-mobile/HEAD/android/app/src/main/play/listings/en-US/graphics/icon/icon.png -------------------------------------------------------------------------------- /android/app/src/main/play/listings/zh-CN/graphics/icon/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Crissium/SilverDict-mobile/HEAD/android/app/src/main/play/listings/zh-CN/graphics/icon/icon.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Crissium/SilverDict-mobile/HEAD/android/app/src/main/res/mipmap-hdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Crissium/SilverDict-mobile/HEAD/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher_monochrome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Crissium/SilverDict-mobile/HEAD/android/app/src/main/res/mipmap-hdpi/ic_launcher_monochrome.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Crissium/SilverDict-mobile/HEAD/android/app/src/main/res/mipmap-mdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Crissium/SilverDict-mobile/HEAD/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher_monochrome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Crissium/SilverDict-mobile/HEAD/android/app/src/main/res/mipmap-mdpi/ic_launcher_monochrome.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Crissium/SilverDict-mobile/HEAD/android/app/src/main/res/mipmap-xhdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Crissium/SilverDict-mobile/HEAD/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher_monochrome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Crissium/SilverDict-mobile/HEAD/android/app/src/main/res/mipmap-xhdpi/ic_launcher_monochrome.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Crissium/SilverDict-mobile/HEAD/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Crissium/SilverDict-mobile/HEAD/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher_monochrome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Crissium/SilverDict-mobile/HEAD/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_monochrome.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Crissium/SilverDict-mobile/HEAD/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Crissium/SilverDict-mobile/HEAD/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_monochrome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Crissium/SilverDict-mobile/HEAD/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_monochrome.png -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ['module:@react-native/babel-preset'], 3 | plugins: [ 4 | 'react-native-paper/babel', 5 | 'react-native-reanimated/plugin' 6 | ] 7 | }; 8 | -------------------------------------------------------------------------------- /android/app/src/main/play/listings/en-US/graphics/phone-screenshots/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Crissium/SilverDict-mobile/HEAD/android/app/src/main/play/listings/en-US/graphics/phone-screenshots/1.png -------------------------------------------------------------------------------- /android/app/src/main/play/listings/en-US/graphics/phone-screenshots/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Crissium/SilverDict-mobile/HEAD/android/app/src/main/play/listings/en-US/graphics/phone-screenshots/2.png -------------------------------------------------------------------------------- /android/app/src/main/play/listings/en-US/graphics/phone-screenshots/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Crissium/SilverDict-mobile/HEAD/android/app/src/main/play/listings/en-US/graphics/phone-screenshots/3.png -------------------------------------------------------------------------------- /android/app/src/main/play/listings/en-US/graphics/phone-screenshots/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Crissium/SilverDict-mobile/HEAD/android/app/src/main/play/listings/en-US/graphics/phone-screenshots/4.png -------------------------------------------------------------------------------- /android/app/src/main/play/listings/en-US/graphics/phone-screenshots/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Crissium/SilverDict-mobile/HEAD/android/app/src/main/play/listings/en-US/graphics/phone-screenshots/5.png -------------------------------------------------------------------------------- /android/app/src/main/play/listings/en-US/graphics/phone-screenshots/6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Crissium/SilverDict-mobile/HEAD/android/app/src/main/play/listings/en-US/graphics/phone-screenshots/6.png -------------------------------------------------------------------------------- /android/app/src/main/play/listings/en-US/graphics/phone-screenshots/7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Crissium/SilverDict-mobile/HEAD/android/app/src/main/play/listings/en-US/graphics/phone-screenshots/7.png -------------------------------------------------------------------------------- /android/app/src/main/play/listings/en-US/graphics/phone-screenshots/8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Crissium/SilverDict-mobile/HEAD/android/app/src/main/play/listings/en-US/graphics/phone-screenshots/8.png -------------------------------------------------------------------------------- /android/app/src/main/play/listings/en-US/graphics/phone-screenshots/9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Crissium/SilverDict-mobile/HEAD/android/app/src/main/play/listings/en-US/graphics/phone-screenshots/9.png -------------------------------------------------------------------------------- /android/app/src/main/play/listings/zh-CN/graphics/phone-screenshots/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Crissium/SilverDict-mobile/HEAD/android/app/src/main/play/listings/zh-CN/graphics/phone-screenshots/1.png -------------------------------------------------------------------------------- /android/app/src/main/play/listings/zh-CN/graphics/phone-screenshots/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Crissium/SilverDict-mobile/HEAD/android/app/src/main/play/listings/zh-CN/graphics/phone-screenshots/2.png -------------------------------------------------------------------------------- /android/app/src/main/play/listings/zh-CN/graphics/phone-screenshots/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Crissium/SilverDict-mobile/HEAD/android/app/src/main/play/listings/zh-CN/graphics/phone-screenshots/3.png -------------------------------------------------------------------------------- /android/app/src/main/play/listings/zh-CN/graphics/phone-screenshots/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Crissium/SilverDict-mobile/HEAD/android/app/src/main/play/listings/zh-CN/graphics/phone-screenshots/4.png -------------------------------------------------------------------------------- /android/app/src/main/play/listings/zh-CN/graphics/phone-screenshots/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Crissium/SilverDict-mobile/HEAD/android/app/src/main/play/listings/zh-CN/graphics/phone-screenshots/5.png -------------------------------------------------------------------------------- /android/app/src/main/play/listings/zh-CN/graphics/phone-screenshots/6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Crissium/SilverDict-mobile/HEAD/android/app/src/main/play/listings/zh-CN/graphics/phone-screenshots/6.png -------------------------------------------------------------------------------- /android/app/src/main/play/listings/zh-CN/graphics/phone-screenshots/7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Crissium/SilverDict-mobile/HEAD/android/app/src/main/play/listings/zh-CN/graphics/phone-screenshots/7.png -------------------------------------------------------------------------------- /android/app/src/main/play/listings/zh-CN/graphics/phone-screenshots/8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Crissium/SilverDict-mobile/HEAD/android/app/src/main/play/listings/zh-CN/graphics/phone-screenshots/8.png -------------------------------------------------------------------------------- /android/app/src/main/play/listings/zh-CN/graphics/phone-screenshots/9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Crissium/SilverDict-mobile/HEAD/android/app/src/main/play/listings/zh-CN/graphics/phone-screenshots/9.png -------------------------------------------------------------------------------- /android/app/src/main/play/listings/en-US/graphics/phone-screenshots/10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Crissium/SilverDict-mobile/HEAD/android/app/src/main/play/listings/en-US/graphics/phone-screenshots/10.png -------------------------------------------------------------------------------- /android/app/src/main/play/listings/en-US/graphics/phone-screenshots/11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Crissium/SilverDict-mobile/HEAD/android/app/src/main/play/listings/en-US/graphics/phone-screenshots/11.png -------------------------------------------------------------------------------- /android/app/src/main/play/listings/zh-CN/graphics/phone-screenshots/10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Crissium/SilverDict-mobile/HEAD/android/app/src/main/play/listings/zh-CN/graphics/phone-screenshots/10.png -------------------------------------------------------------------------------- /crowdin.yml: -------------------------------------------------------------------------------- 1 | files: 2 | - source: /src/translations/en-GB.json 3 | translation: /src/translations/%locale%.json 4 | pull_request_title: "l10n: new translations from Crowdin" 5 | commit_message: "l10n: new translations {languageName}" -------------------------------------------------------------------------------- /android/app/src/main/play/listings/zh-CN/full-description.txt: -------------------------------------------------------------------------------- 1 | 这是 SilverDict 词典服务器的 Android 客户端,是基于 Web 技术的 GoldenDict 替代品。 该移动客户端具有词典应用程序的所有预期功能,包括查找、历史记录、输入建议、词典管理等。 2 | 3 | 您可以在其 GitHub 仓库中阅读有关该服务器的更多信息:https://github.com/Crissium/SilverDict 4 | -------------------------------------------------------------------------------- /ios/SilverDict/main.m: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | #import "AppDelegate.h" 4 | 5 | int main(int argc, char *argv[]) 6 | { 7 | @autoreleasepool { 8 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'SilverDict' 2 | apply from: file("../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesSettingsGradle(settings) 3 | include ':app' 4 | includeBuild('../node_modules/@react-native/gradle-plugin') 5 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-all.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /metro.config.js: -------------------------------------------------------------------------------- 1 | const { getDefaultConfig, mergeConfig } = require('@react-native/metro-config'); 2 | 3 | /** 4 | * Metro configuration 5 | * https://reactnative.dev/docs/metro 6 | * 7 | * @type {import('metro-config').MetroConfig} 8 | */ 9 | const config = {}; 10 | 11 | module.exports = mergeConfig(getDefaultConfig(__dirname), config); 12 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 9 | 10 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # You may use http://rbenv.org/ or https://rvm.io/ to install and use this version 4 | ruby ">= 2.6.10" 5 | 6 | # Cocoapods 1.15 introduced a bug which break the build. We will remove the upper 7 | # bound in the template on Cocoapods with next React Native release. 8 | gem 'cocoapods', '>= 1.13', '< 1.15' 9 | gem 'activesupport', '>= 6.1.7.5', '< 7.1.0' 10 | -------------------------------------------------------------------------------- /android/app/src/main/play/listings/en-US/full-description.txt: -------------------------------------------------------------------------------- 1 | This is the Android client of the SilverDict dictionary server, a web-based alternative to GoldenDict. This mobile client has all the expected features of a dictionary app, including lookup, history, suggest-as-you-type, dictionary management, and more. 2 | 3 | You can read more about the server at its repository: https://github.com/Crissium/SilverDict -------------------------------------------------------------------------------- /src/translations/l10n.js: -------------------------------------------------------------------------------- 1 | import LocalizedStrings from 'react-native-localization'; 2 | import { RTL_LANGS } from '../config'; 3 | 4 | export const localisedStrings = new LocalizedStrings({ 5 | 'en-GB': require('./en-GB.json'), 6 | 'en-US': require('./en-US.json'), 7 | 'ru-RU': require('./ru-RU.json'), 8 | 'zh-CN': require('./zh-CN.json') 9 | }); 10 | 11 | export const interfaceLangIsRTL = RTL_LANGS.includes(localisedStrings.getLanguage()); -------------------------------------------------------------------------------- /src/components/LibraryScreen/GroupManager/GroupDictionaryEditorDialogueItem.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Checkbox } from 'react-native-paper'; 3 | 4 | export default function GroupDictionaryEditorDialogueItem(props) { 5 | const { name, displayName, included, handleToggle } = props; 6 | 7 | return ( 8 | handleToggle(name)} /> 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /android/app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /usr/local/Cellar/android-sdk/24.3.3/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | -------------------------------------------------------------------------------- /ios/.xcode.env: -------------------------------------------------------------------------------- 1 | # This `.xcode.env` file is versioned and is used to source the environment 2 | # used when running script phases inside Xcode. 3 | # To customize your local environment, you can create an `.xcode.env.local` 4 | # file that is not versioned. 5 | 6 | # NODE_BINARY variable contains the PATH to the node executable. 7 | # 8 | # Customize the NODE_BINARY variable here. 9 | # For example, to use nvm with brew, add the following line 10 | # . "$(brew --prefix nvm)/nvm.sh" --no-use 11 | export NODE_BINARY=$(command -v node) 12 | -------------------------------------------------------------------------------- /src/components/common/InProgressDialogue.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Dialog, Portal, ProgressBar } from 'react-native-paper'; 3 | 4 | export default function InProgressDialogue(props) { 5 | const { visible, title } = props; 6 | 7 | return ( 8 | 9 | 13 | {title} 14 | 15 | 16 | 17 | 18 | 19 | ); 20 | } -------------------------------------------------------------------------------- /src/components/QueryScreen.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { View } from 'react-native'; 3 | import { QueryProvider } from './QueryScreen/QueryContext'; 4 | import QueryBar from './QueryScreen/QueryBar'; 5 | import QueryContent from './QueryScreen/QueryContent'; 6 | 7 | export default function QueryScreen({ navigation }) { 8 | 9 | return ( 10 | 11 | 12 | 15 | 16 | 17 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext { 3 | buildToolsVersion = "34.0.0" 4 | minSdkVersion = 23 5 | compileSdkVersion = 34 6 | targetSdkVersion = 34 7 | ndkVersion = "26.1.10909125" 8 | kotlinVersion = "1.9.22" 9 | } 10 | repositories { 11 | google() 12 | mavenCentral() 13 | } 14 | dependencies { 15 | classpath("com.android.tools.build:gradle") 16 | classpath("com.facebook.react:react-native-gradle-plugin") 17 | classpath("org.jetbrains.kotlin:kotlin-gradle-plugin") 18 | } 19 | } 20 | 21 | apply plugin: "com.facebook.react.rootproject" 22 | -------------------------------------------------------------------------------- /src/components/LibraryScreen/DictionaryManager/EditMenu.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Menu } from 'react-native-paper'; 3 | import { localisedStrings } from '../../../translations/l10n'; 4 | 5 | export default function EditMenu(props) { 6 | const { visible, setVisible, anchor, onEdit, onDelete } = props; 7 | 8 | return ( 9 | setVisible(false)} 12 | anchor={anchor} 13 | > 14 | { onEdit(); setVisible(false); }} 16 | title={localisedStrings['generic-rename']} /> 17 | { onDelete(); setVisible(false); }} 19 | title={localisedStrings['generic-delete']} /> 20 | 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2020": true, 5 | "node": true 6 | }, 7 | "extends": "eslint:recommended", 8 | "globals": { 9 | "Atomics": "readonly", 10 | "SharedArrayBuffer": "readonly" 11 | }, 12 | "parserOptions": { 13 | "ecmaFeatures": { 14 | "jsx": true 15 | }, 16 | "ecmaVersion": 14, 17 | "sourceType": "module" 18 | }, 19 | "plugins": [ 20 | "react" 21 | ], 22 | "rules": { 23 | "indent": [ 24 | "error", 25 | "tab" 26 | ], 27 | "linebreak-style": [ 28 | "error", 29 | "unix" 30 | ], 31 | "quotes": [ 32 | "error", 33 | "single" 34 | ], 35 | "semi": [ 36 | "error", 37 | "always" 38 | ], 39 | "no-unused-vars": "off", 40 | "no-inner-declarations": "off", 41 | "no-empty": "off" 42 | } 43 | } -------------------------------------------------------------------------------- /ios/SilverDictTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/components/QueryScreen/QueryBar.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { Keyboard } from 'react-native'; 3 | import { Appbar } from 'react-native-paper'; 4 | import Input from './Input'; 5 | import DictionarySelection from './DictionarySelection'; 6 | 7 | export default function QueryBar(props) { 8 | const [dictionarySelectionVisible, setDictionarySelectionVisible] = useState(false); 9 | 10 | return ( 11 | 14 | { Keyboard.dismiss(); props.openDrawer(); }} /> 17 | 18 | { Keyboard.dismiss(); setDictionarySelectionVisible(true); }} /> 21 | 25 | 26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /src/components/common/ConfirmDialogue.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Button, Dialog, Portal, Text } from 'react-native-paper'; 3 | import { localisedStrings } from '../../translations/l10n'; 4 | 5 | export default function ConfirmDialogue(props) { 6 | const { visible, setVisible, title, content, onConfirm } = props; 7 | 8 | return ( 9 | 10 | setVisible(false)}> 13 | {title} 14 | 15 | {content} 16 | 17 | 18 | 23 | 26 | 27 | 28 | 29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /ios/SilverDict/AppDelegate.mm: -------------------------------------------------------------------------------- 1 | #import "AppDelegate.h" 2 | 3 | #import 4 | 5 | @implementation AppDelegate 6 | 7 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions 8 | { 9 | self.moduleName = @"SilverDict"; 10 | // You can add your custom initial props in the dictionary below. 11 | // They will be passed down to the ViewController used by React Native. 12 | self.initialProps = @{}; 13 | 14 | return [super application:application didFinishLaunchingWithOptions:launchOptions]; 15 | } 16 | 17 | - (NSURL *)sourceURLForBridge:(RCTBridge *)bridge 18 | { 19 | return [self bundleURL]; 20 | } 21 | 22 | - (NSURL *)bundleURL 23 | { 24 | #if DEBUG 25 | return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index"]; 26 | #else 27 | return [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"]; 28 | #endif 29 | } 30 | 31 | @end 32 | -------------------------------------------------------------------------------- /src/components/SettingsScreen/ChangeColourDialogue.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import TextEditorDialogue from '../common/TextEditorDialogue'; 3 | import { localisedStrings } from '../../translations/l10n'; 4 | 5 | export default function ChangeColourDialogue(props) { 6 | const { visible, setVisible, darkTextColour, setDarkTextColour } = props; 7 | 8 | function handleSubmit(colourBuffer) { 9 | if (colourBuffer.length > 0) { 10 | setDarkTextColour(colourBuffer); 11 | } else { 12 | setDarkTextColour('white'); 13 | } 14 | setVisible(false); 15 | } 16 | 17 | return ( 18 | 30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /src/components/SettingsScreen/ConfirmClearHistoryDialogue.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ConfirmDialogue from '../common/ConfirmDialogue'; 3 | import { loadDataFromJsonResponse } from '../../utils'; 4 | import { localisedStrings } from '../../translations/l10n'; 5 | 6 | export default function ConfirmClearHistoryDialogue(props) { 7 | const { visible, setVisible, apiPrefix, setHistory } = props; 8 | 9 | function handleConfirm() { 10 | fetch(`${apiPrefix}/management/history`, { 11 | method: 'DELETE' 12 | }) 13 | .then(loadDataFromJsonResponse) 14 | .then((data) => { 15 | setHistory(data); 16 | setVisible(false); 17 | }) 18 | .catch((error) => { 19 | alert(localisedStrings['clear-history-dialogue-message-failure']); 20 | }); 21 | } 22 | 23 | return ( 24 | 31 | ); 32 | } -------------------------------------------------------------------------------- /src/components/LibraryScreen/LibraryNavigationBar.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { View } from 'react-native'; 3 | import { Appbar, Button, useTheme } from 'react-native-paper'; 4 | import { localisedStrings } from '../../translations/l10n'; 5 | 6 | export default function LibraryNavigationBar(props) { 7 | const textColour = useTheme().colors.onSurface; 8 | return ( 9 | 10 | 11 | 17 | 23 | 29 | 30 | 31 | ); 32 | } -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import 'react-native-gesture-handler'; 2 | import { AppRegistry, useColorScheme } from 'react-native'; 3 | import { PaperProvider, MD2LightTheme, MD2DarkTheme } from 'react-native-paper'; 4 | import { name as appName } from './app.json'; 5 | import App from './src/App'; 6 | 7 | const lightTheme = { 8 | ...MD2LightTheme, 9 | colors: { 10 | ...MD2LightTheme.colors, 11 | primary: 'grey', 12 | accent: '#483d8b', 13 | background: '#ffffff', 14 | surface: '#eaeaea', 15 | error: '#ffb6c1', 16 | onSurface: '#1d1d1d', 17 | } 18 | }; 19 | 20 | const darkTheme = { 21 | ...MD2DarkTheme, 22 | colors: { 23 | ...MD2DarkTheme.colors, 24 | primary: 'slategrey', 25 | accent: '#008080', 26 | background: '#000000', 27 | surface: '#111111', 28 | error: '#ffb6c1', 29 | onSurface: '#cccccc', 30 | } 31 | }; 32 | 33 | function Main() { 34 | const isDarkMode = useColorScheme() === 'dark'; 35 | const theme = isDarkMode ? darkTheme : lightTheme; 36 | 37 | return ( 38 | 39 | 40 | 41 | ); 42 | } 43 | 44 | AppRegistry.registerComponent(appName, () => Main); 45 | -------------------------------------------------------------------------------- /ios/SilverDict/Images.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "scale" : "2x", 6 | "size" : "20x20" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "scale" : "3x", 11 | "size" : "20x20" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "scale" : "2x", 16 | "size" : "29x29" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "scale" : "3x", 21 | "size" : "29x29" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "scale" : "2x", 26 | "size" : "40x40" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "scale" : "3x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "scale" : "2x", 36 | "size" : "60x60" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "scale" : "3x", 41 | "size" : "60x60" 42 | }, 43 | { 44 | "idiom" : "ios-marketing", 45 | "scale" : "1x", 46 | "size" : "1024x1024" 47 | } 48 | ], 49 | "info" : { 50 | "author" : "xcode", 51 | "version" : 1 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/components/LibraryScreen/SourceManager.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { FlatList } from 'react-native'; 3 | import { IconButton, List } from 'react-native-paper'; 4 | import ConfirmDeleteDialogue from './SourceManager/ConfirmDeleteDialogue'; 5 | import { useAppContext } from '../../AppContext'; 6 | 7 | export default function SourceManager() { 8 | const { sources } = useAppContext(); 9 | const [sourceToDelete, setSourceToDelete] = useState(''); 10 | const [confirmDeleteVisible, setConfirmDeleteVisible] = useState(false); 11 | 12 | function handleDelete(source) { 13 | setSourceToDelete(source); 14 | setConfirmDeleteVisible(true); 15 | } 16 | 17 | return ( 18 | <> 19 | 22 | handleDelete(item)} />} 25 | /> 26 | } 27 | keyExtractor={(item) => item} 28 | /> 29 | 34 | 35 | ); 36 | } -------------------------------------------------------------------------------- /src/components/SettingsScreen/ChangeFontDialogue.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Button, Dialog, Portal, RadioButton } from 'react-native-paper'; 3 | import { localisedStrings } from '../../translations/l10n'; 4 | 5 | export default function ChangeFontDialogue(props) { 6 | const { visible, setVisible, fontFamily, setFontFamily } = props; 7 | 8 | return ( 9 | 10 | setVisible(false)}> 13 | {localisedStrings['change-font-dialogue-title']} 14 | 15 | setFontFamily(value)} value={fontFamily}> 16 | 17 | 18 | 19 | 20 | 21 | 26 | 27 | 28 | 29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /src/components/SettingsScreen/ChangeAddressDialogue.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import TextEditorDialogue from '../common/TextEditorDialogue'; 3 | import { loadDataFromJsonResponse } from '../../utils'; 4 | import { localisedStrings } from '../../translations/l10n'; 5 | 6 | export default function ChangeAddressDialogue(props) { 7 | const { visible, setVisible, setServerAddress } = props; 8 | 9 | function handleSubmit(addressBuffer) { 10 | fetch(`${addressBuffer}/api/validator/test_connection`) 11 | .then(loadDataFromJsonResponse) 12 | .then((data) => { 13 | if (data['success']) { 14 | setServerAddress(addressBuffer); 15 | setVisible(false); 16 | } 17 | }) 18 | .catch((error) => { 19 | alert(localisedStrings['change-address-dialogue-message-unable-to-connect']); 20 | }); 21 | } 22 | 23 | return ( 24 | 35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 13 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /ios/SilverDict/PrivacyInfo.xcprivacy: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSPrivacyCollectedDataTypes 6 | 7 | 8 | NSPrivacyAccessedAPITypes 9 | 10 | 11 | NSPrivacyAccessedAPIType 12 | NSPrivacyAccessedAPICategoryFileTimestamp 13 | NSPrivacyAccessedAPITypeReasons 14 | 15 | C617.1 16 | 17 | 18 | 19 | NSPrivacyAccessedAPIType 20 | NSPrivacyAccessedAPICategoryUserDefaults 21 | NSPrivacyAccessedAPITypeReasons 22 | 23 | CA92.1 24 | 25 | 26 | 27 | NSPrivacyAccessedAPIType 28 | NSPrivacyAccessedAPICategorySystemBootTime 29 | NSPrivacyAccessedAPITypeReasons 30 | 31 | 35F9.1 32 | 33 | 34 | 35 | NSPrivacyTracking 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /src/components/LibraryScreen.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { View } from 'react-native'; 3 | import { createMaterialTopTabNavigator } from '@react-navigation/material-top-tabs'; 4 | import { getFocusedRouteNameFromRoute } from '@react-navigation/native'; 5 | import LibraryAppbar from './LibraryScreen/LibraryAppbar'; 6 | import LibraryNavigationBar from './LibraryScreen/LibraryNavigationBar'; 7 | import DictionaryManager from './LibraryScreen/DictionaryManager'; 8 | import GroupManager from './LibraryScreen/GroupManager'; 9 | import SourceManager from './LibraryScreen/SourceManager'; 10 | 11 | const Tab = createMaterialTopTabNavigator(); 12 | 13 | export default function LibraryScreen({ navigation, route }) { 14 | const nameActiveTab = getFocusedRouteNameFromRoute(route) ?? 'Dictionaries'; 15 | return ( 16 | 17 | navigation.openDrawer()} nameActiveTab={nameActiveTab} /> 18 | }> 19 | 20 | 21 | 22 | 23 | 24 | ); 25 | } -------------------------------------------------------------------------------- /src/components/LibraryScreen/GroupManager.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import DraggableFlatList from 'react-native-draggable-flatlist'; 3 | import GroupCard from './GroupManager/GroupCard'; 4 | import { useAppContext } from '../../AppContext'; 5 | import { JSON_HEADER, loadDataFromJsonResponse } from '../../utils'; 6 | import { localisedStrings } from '../../translations/l10n'; 7 | 8 | export default function GroupManager() { 9 | const { serverAddress, groups, setGroups } = useAppContext(); 10 | 11 | function handleReorder(newGroups) { 12 | fetch(`${serverAddress}/api/management/groups`, { 13 | method: 'PUT', 14 | headers: JSON_HEADER, 15 | body: JSON.stringify(newGroups) 16 | }) 17 | .then(loadDataFromJsonResponse) 18 | .then((data) => { 19 | setGroups(data); 20 | }) 21 | .catch((error) => { 22 | alert(localisedStrings['group-manager-alert-failure-reordering-groups']); 23 | }); 24 | } 25 | 26 | return ( 27 | handleReorder(data)} 30 | renderItem={({ item, drag }) => 31 | 36 | } 37 | keyExtractor={(item) => item.name} 38 | /> 39 | ); 40 | } -------------------------------------------------------------------------------- /ios/Podfile: -------------------------------------------------------------------------------- 1 | # Resolve react_native_pods.rb with node to allow for hoisting 2 | require Pod::Executable.execute_command('node', ['-p', 3 | 'require.resolve( 4 | "react-native/scripts/react_native_pods.rb", 5 | {paths: [process.argv[1]]}, 6 | )', __dir__]).strip 7 | 8 | platform :ios, min_ios_version_supported 9 | prepare_react_native_project! 10 | 11 | linkage = ENV['USE_FRAMEWORKS'] 12 | if linkage != nil 13 | Pod::UI.puts "Configuring Pod with #{linkage}ally linked Frameworks".green 14 | use_frameworks! :linkage => linkage.to_sym 15 | end 16 | 17 | target 'SilverDict' do 18 | config = use_native_modules! 19 | 20 | use_react_native!( 21 | :path => config[:reactNativePath], 22 | # An absolute path to your application root. 23 | :app_path => "#{Pod::Config.instance.installation_root}/.." 24 | ) 25 | 26 | target 'SilverDictTests' do 27 | inherit! :complete 28 | # Pods for testing 29 | end 30 | 31 | post_install do |installer| 32 | # https://github.com/facebook/react-native/blob/main/packages/react-native/scripts/react_native_pods.rb#L197-L202 33 | react_native_post_install( 34 | installer, 35 | config[:reactNativePath], 36 | :mac_catalyst_enabled => false, 37 | # :ccache_enabled => true 38 | ) 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /src/components/LibraryScreen/SourceManager/ConfirmDeleteDialogue.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ConfirmDialogue from '../../common/ConfirmDialogue'; 3 | import { useAppContext } from '../../../AppContext'; 4 | import { JSON_HEADER, loadDataFromJsonResponse } from '../../../utils'; 5 | import { localisedStrings } from '../../../translations/l10n'; 6 | 7 | export default function ConfirmDeleteDialogue(props) { 8 | const { visible, setVisible, source } = props; 9 | const { serverAddress, setSources } = useAppContext(); 10 | 11 | function handleDelete() { 12 | fetch(`${serverAddress}/api/management/sources`, { 13 | method: 'DELETE', 14 | headers: JSON_HEADER, 15 | body: JSON.stringify({ source: source }) 16 | }) 17 | .then(loadDataFromJsonResponse) 18 | .then((data) => { 19 | setSources(data); 20 | }) 21 | .catch((error) => { 22 | alert(localisedStrings['source-manager-alert-failure-deleting-source']); 23 | }) 24 | .finally(() => { 25 | setVisible(false); 26 | }); 27 | } 28 | 29 | return ( 30 | 37 | ); 38 | } -------------------------------------------------------------------------------- /src/components/LibraryScreen/GroupManager/ConfirmDeleteDialogue.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ConfirmDialogue from '../../common/ConfirmDialogue'; 3 | import { useAppContext } from '../../../AppContext'; 4 | import { JSON_HEADER, loadDataFromJsonResponse } from '../../../utils'; 5 | import { localisedStrings } from '../../../translations/l10n'; 6 | 7 | export default function ConfirmDeleteDialogue(props) { 8 | const { visible, setVisible, name } = props; 9 | const { serverAddress, setGroups, setGroupings } = useAppContext(); 10 | 11 | function handleDelete() { 12 | fetch(`${serverAddress}/api/management/groups`, { 13 | method: 'DELETE', 14 | headers: JSON_HEADER, 15 | body: JSON.stringify({ name: name }) 16 | }) 17 | .then(loadDataFromJsonResponse) 18 | .then((data) => { 19 | setGroups(data['groups']); 20 | setGroupings(data['groupings']); 21 | }) 22 | .catch((error) => { 23 | alert(localisedStrings['group-manager-alert-failure-removing-group']); 24 | }) 25 | .finally(() => { 26 | setVisible(false); 27 | }); 28 | } 29 | 30 | return ( 31 | 37 | ); 38 | } 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # My scripts 2 | release.py 3 | 4 | # OSX 5 | # 6 | .DS_Store 7 | 8 | # Xcode 9 | # 10 | build/ 11 | *.pbxuser 12 | !default.pbxuser 13 | *.mode1v3 14 | !default.mode1v3 15 | *.mode2v3 16 | !default.mode2v3 17 | *.perspectivev3 18 | !default.perspectivev3 19 | xcuserdata 20 | *.xccheckout 21 | *.moved-aside 22 | DerivedData 23 | *.hmap 24 | *.ipa 25 | *.xcuserstate 26 | **/.xcode.env.local 27 | 28 | # Android/IntelliJ 29 | # 30 | build/ 31 | .idea 32 | .gradle 33 | local.properties 34 | *.iml 35 | *.hprof 36 | .cxx/ 37 | *.keystore 38 | !debug.keystore 39 | 40 | # node.js 41 | # 42 | node_modules/ 43 | npm-debug.log 44 | yarn-error.log 45 | 46 | # fastlane 47 | # 48 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 49 | # screenshots whenever they are needed. 50 | # For more information about the recommended setup visit: 51 | # https://docs.fastlane.tools/best-practices/source-control/ 52 | 53 | **/fastlane/report.xml 54 | **/fastlane/Preview.html 55 | **/fastlane/screenshots 56 | **/fastlane/test_output 57 | 58 | # Bundle artifact 59 | *.jsbundle 60 | 61 | # Ruby / CocoaPods 62 | **/Pods/ 63 | /vendor/bundle/ 64 | 65 | # Temporary files created by Metro to check the health of the file watcher 66 | .metro-health-check* 67 | 68 | # testing 69 | /coverage 70 | 71 | # Yarn 72 | .yarn/* 73 | !.yarn/patches 74 | !.yarn/plugins 75 | !.yarn/releases 76 | !.yarn/sdks 77 | !.yarn/versions 78 | -------------------------------------------------------------------------------- /src/components/DrawerContent.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { View } from 'react-native'; 3 | import { DrawerContentScrollView } from '@react-navigation/drawer'; 4 | import { Drawer } from 'react-native-paper'; 5 | import { useAppContext } from '../AppContext'; 6 | import { localisedStrings } from '../translations/l10n'; 7 | 8 | export default function DrawerContent(props) { 9 | const { fetchInitialData, serverAddress, setDictionaries, setGroups, setGroupings, setHistory, setSizeHistory, setSizeSuggestion } = useAppContext(); 10 | 11 | return ( 12 | 13 | 14 | props.navigation.navigate('Query')} 18 | /> 19 | props.navigation.navigate('Library')} 23 | /> 24 | props.navigation.navigate('Settings')} 28 | /> 29 | fetchInitialData(`${serverAddress}/api`, setDictionaries, setGroups, setGroupings, setHistory, setSizeHistory, setSizeSuggestion)} 33 | /> 34 | 35 | 36 | ); 37 | } -------------------------------------------------------------------------------- /src/components/QueryScreen/Input.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | import { TextInput, useTheme } from 'react-native-paper'; 3 | import { isRTL } from '../../utils'; 4 | import { useQueryContext } from './QueryContext'; 5 | import { localisedStrings } from '../../translations/l10n'; 6 | 7 | export default function Input() { 8 | const { query, setQuery, textInputRef, handleInputSubmit, clearArticle } = useQueryContext(); 9 | 10 | function handleInputFocus(e) { 11 | clearArticle(); 12 | } 13 | 14 | useEffect(function () { 15 | if (isRTL(query)) 16 | textInputRef.current.setNativeProps({ style: { direction: 'rtl', textAlign: 'right' } }); 17 | else 18 | textInputRef.current.setNativeProps({ style: { direction: 'ltr', textAlign: 'left' } }); 19 | }, [query]); 20 | 21 | return ( 22 | 42 | ); 43 | } 44 | -------------------------------------------------------------------------------- /src/components/QueryScreen/QueryContent.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { FlatList, Keyboard, View } from 'react-native'; 3 | import { List } from 'react-native-paper'; 4 | import ArticleView from './ArticleView'; 5 | import ArticleBottomBar from './ArticleBottomBar'; 6 | import { useAppContext } from '../../AppContext'; 7 | import { useQueryContext } from './QueryContext'; 8 | import { isRTL } from '../../utils'; 9 | 10 | function WordItem(props) { 11 | const { word } = props; 12 | const wordIsRTL = isRTL(word); 13 | const { search } = useQueryContext(); 14 | 15 | return ( 16 | { Keyboard.dismiss(); search(word); }} /> 20 | ); 21 | } 22 | 23 | export default function QueryContent() { 24 | const { history } = useAppContext(); 25 | const { query, article, suggestions } = useQueryContext(); 26 | const wordsToDisplay = query.length === 0 ? history : suggestions; 27 | 28 | if (article.length === 0) 29 | return ( 30 | 31 | } 35 | keyExtractor={(item) => item} 36 | /> 37 | 38 | ); 39 | else 40 | return ( 41 | 44 | 45 | 46 | 47 | ); 48 | } 49 | -------------------------------------------------------------------------------- /src/components/SettingsScreen/ConfirmRecreateNgramDialogue.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import ConfirmDialogue from '../common/ConfirmDialogue'; 3 | import InProgressDialogue from '../common/InProgressDialogue'; 4 | import { loadDataFromJsonResponse } from '../../utils'; 5 | import { localisedStrings } from '../../translations/l10n'; 6 | 7 | export default function ConfirmRecreateNgramDialogue(props) { 8 | const { visible, setVisible, apiPrefix } = props; 9 | const [inProgress, setInProgress] = useState(false); 10 | 11 | function handleConfirm() { 12 | setInProgress(true); 13 | fetch(`${apiPrefix}/management/create_ngram_table`) 14 | .then(loadDataFromJsonResponse) 15 | .then((data) => { 16 | if (data['success']) { 17 | alert(localisedStrings['confirm-recreate-ngram-dialogue-message-success']); 18 | } 19 | }) 20 | .catch((error) => { 21 | alert(localisedStrings['confirm-recreate-ngram-dialogue-message-failure']); 22 | }) 23 | .finally(() => { 24 | setInProgress(false); 25 | setVisible(false); 26 | }); 27 | } 28 | 29 | return ( 30 | <> 31 | 35 | 42 | 43 | ); 44 | } -------------------------------------------------------------------------------- /src/components/SettingsScreen/ChangeSizeSuggestionDialogue.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import TextEditorDialogue from '../common/TextEditorDialogue'; 3 | import { JSON_HEADER, loadDataFromJsonResponse } from '../../utils'; 4 | import { localisedStrings } from '../../translations/l10n'; 5 | 6 | export default function ChangeSizeSuggestionDialogue(props) { 7 | const { visible, setVisible, apiPrefix, sizeSuggestion, setSizeSuggestion } = props; 8 | 9 | function handleSubmit(sizeBuffer) { 10 | let newSize; 11 | try { 12 | newSize = parseInt(sizeBuffer); 13 | newSize = Math.max(newSize, 1); 14 | } catch (error) { 15 | alert(localisedStrings['generic-alert-invalid-number']); 16 | return; 17 | } 18 | 19 | fetch(`${apiPrefix}/management/num_suggestions`, { 20 | method: 'PUT', 21 | headers: JSON_HEADER, 22 | body: JSON.stringify({ size: newSize }) 23 | }) 24 | .then(loadDataFromJsonResponse) 25 | .then((data) => { 26 | setSizeSuggestion(data['size']); 27 | setVisible(false); 28 | }) 29 | .catch((error) => { 30 | alert(localisedStrings['change-size-suggestion-dialogue-message-failure']); 31 | }); 32 | } 33 | 34 | return ( 35 | 46 | ); 47 | } 48 | -------------------------------------------------------------------------------- /src/components/SettingsScreen/ChangeSizeHistoryDialogue.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import TextEditorDialogue from '../common/TextEditorDialogue'; 3 | import { JSON_HEADER, loadDataFromJsonResponse } from '../../utils'; 4 | import { localisedStrings } from '../../translations/l10n'; 5 | 6 | export default function ChangeSizeHistoryDialogue(props) { 7 | const { visible, setVisible, apiPrefix, setHistory, sizeHistory, setSizeHistory } = props; 8 | 9 | function handleSubmit(sizeBuffer) { 10 | let newSize; 11 | try { 12 | newSize = parseInt(sizeBuffer); 13 | newSize = Math.max(newSize, 0); 14 | } catch (error) { 15 | alert(localisedStrings['generic-alert-invalid-number']); 16 | return; 17 | } 18 | 19 | fetch(`${apiPrefix}/management/history_size`, { 20 | method: 'PUT', 21 | headers: JSON_HEADER, 22 | body: JSON.stringify({ size: newSize }) 23 | }) 24 | .then(loadDataFromJsonResponse) 25 | .then((data) => { 26 | setHistory(data); 27 | setSizeHistory(newSize); 28 | setVisible(false); 29 | }) 30 | .catch((error) => { 31 | alert(localisedStrings['change-size-history-dialogue-message-failure']); 32 | }); 33 | } 34 | 35 | return ( 36 | 47 | ); 48 | } 49 | -------------------------------------------------------------------------------- /src/components/common/TextEditorDialogue.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { Button, Dialog, Text, TextInput, Portal } from 'react-native-paper'; 3 | import { localisedStrings } from '../../translations/l10n'; 4 | 5 | export default function TextEditorDialogue(props) { 6 | const { visible, setVisible, title, content, originalValue, handleSubmit, inputMode, placeholder, autoCapitalize, autoCorrect } = props; 7 | const [buffer, setBuffer] = useState(originalValue); 8 | 9 | return ( 10 | 11 | setVisible(false)}> 14 | {title} 15 | 16 | {content && 17 | {content}} 18 | setBuffer(text)} 31 | onSubmitEditing={(e) => { 32 | handleSubmit(buffer); 33 | }} /> 34 | 35 | 36 | 41 | 46 | 47 | 48 | 49 | ); 50 | } -------------------------------------------------------------------------------- /android/app/src/main/java/com/gmail/blandilyte/silverdict/MainApplication.kt: -------------------------------------------------------------------------------- 1 | package com.gmail.blandilyte.silverdict 2 | 3 | import android.app.Application 4 | import com.facebook.react.PackageList 5 | import com.facebook.react.ReactApplication 6 | import com.facebook.react.ReactHost 7 | import com.facebook.react.ReactNativeHost 8 | import com.facebook.react.ReactPackage 9 | import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.load 10 | import com.facebook.react.defaults.DefaultReactHost.getDefaultReactHost 11 | import com.facebook.react.defaults.DefaultReactNativeHost 12 | import com.facebook.soloader.SoLoader 13 | 14 | class MainApplication : Application(), ReactApplication { 15 | 16 | override val reactNativeHost: ReactNativeHost = object : DefaultReactNativeHost(this) { 17 | override fun getPackages(): List = PackageList(this).packages.apply { 18 | // Packages that cannot be autolinked yet can be added manually here, for example: 19 | // add(MyReactNativePackage()) 20 | } 21 | 22 | override fun getJSMainModuleName(): String = "index" 23 | 24 | override fun getUseDeveloperSupport(): Boolean = BuildConfig.DEBUG 25 | 26 | override val isNewArchEnabled: Boolean = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED 27 | override val isHermesEnabled: Boolean = BuildConfig.IS_HERMES_ENABLED 28 | } 29 | 30 | override val reactHost: ReactHost 31 | get() = getDefaultReactHost(applicationContext, reactNativeHost) 32 | 33 | override fun onCreate() { 34 | super.onCreate() 35 | SoLoader.init(this, false) 36 | if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) { 37 | // If you opted-in for the New Architecture, we load the native entry point for this app. 38 | load() 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/components/SettingsScreen/AdditionalFontsDialogue.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { FlatList } from 'react-native'; 3 | import { Button, Checkbox, Dialog, Portal, Text } from 'react-native-paper'; 4 | import { localisedStrings } from '../../translations/l10n'; 5 | 6 | function Item(props) { 7 | const { scriptName, enabled, setEnabled } = props; 8 | 9 | return ( 10 | setEnabled(!enabled)} 14 | /> 15 | ); 16 | } 17 | 18 | export default function AdditionalFontsDialogue(props) { 19 | const { visible, setVisible, scriptsWithAdditionalFonts, setScriptsWithAdditionalFonts } = props; 20 | 21 | return ( 22 | 23 | setVisible(false)}> 26 | {localisedStrings['additional-fonts-dialogue-title']} 27 | 28 | {localisedStrings['additional-fonts-dialogue-content']} 29 | setScriptsWithAdditionalFonts({ ...scriptsWithAdditionalFonts, [item]: value })} />} 36 | keyExtractor={(item) => item} /> 37 | 38 | 39 | 44 | 45 | 46 | 47 | ); 48 | } -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createDrawerNavigator } from '@react-navigation/drawer'; 3 | import { NavigationContainer, DefaultTheme, DarkTheme } from '@react-navigation/native'; 4 | import { I18nManager } from 'react-native'; 5 | import { useTheme } from 'react-native-paper'; 6 | import { AppProvider } from './AppContext'; 7 | import { interfaceLangIsRTL } from './translations/l10n'; 8 | import QueryScreen from './components/QueryScreen'; 9 | import LibraryScreen from './components/LibraryScreen'; 10 | import SettingsScreen from './components/SettingsScreen'; 11 | import DrawerContent from './components/DrawerContent'; 12 | 13 | I18nManager.forceRTL(interfaceLangIsRTL); 14 | 15 | const Drawer = createDrawerNavigator(); 16 | 17 | export default function App() { 18 | return ( 19 | 20 | 33 | } 36 | > 37 | 41 | 45 | 49 | 50 | 51 | 52 | ); 53 | } 54 | -------------------------------------------------------------------------------- /src/components/LibraryScreen/GroupManager/GroupNameEditorDialogue.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import TextEditorDialogue from '../../common/TextEditorDialogue'; 3 | import { useAppContext } from '../../../AppContext'; 4 | import { JSON_HEADER, loadDataFromJsonResponse } from '../../../utils'; 5 | import { localisedStrings } from '../../../translations/l10n'; 6 | 7 | export default function GroupNameEditorDialogue(props) { 8 | const { visible, setVisible, name } = props; 9 | const { serverAddress, setGroups, groupings, setGroupings } = useAppContext(); 10 | 11 | function handleSubmit(nameBuffer) { 12 | if (nameBuffer.length === 0) { 13 | alert(localisedStrings['group-manager-alert-empty-name']); 14 | return; 15 | } 16 | 17 | if (nameBuffer === name) { 18 | setVisible(false); 19 | return; 20 | } 21 | 22 | if (groupings[nameBuffer]) { 23 | alert(localisedStrings['group-manager-alert-name-exists']); 24 | return; 25 | } 26 | 27 | fetch(`${serverAddress}/api/management/group_name`, { 28 | method: 'PUT', 29 | headers: JSON_HEADER, 30 | body: JSON.stringify({ old: name, new: nameBuffer }) 31 | }) 32 | .then(loadDataFromJsonResponse) 33 | .then((data) => { 34 | setGroups(data['groups']); 35 | setGroupings(data['groupings']); 36 | setVisible(false); 37 | }) 38 | .catch((error) => { 39 | alert(localisedStrings['group-manager-alert-failure-renaming-group']); 40 | }); 41 | } 42 | 43 | return ( 44 | 54 | ); 55 | } 56 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "SilverDict", 3 | "version": "1.5.0", 4 | "private": true, 5 | "scripts": { 6 | "android": "react-native run-android", 7 | "ios": "react-native run-ios", 8 | "start": "react-native start" 9 | }, 10 | "dependencies": { 11 | "@react-native-async-storage/async-storage": "^1.23.1", 12 | "@react-native-community/slider": "^4.5.2", 13 | "@react-native-picker/picker": "^2.7.7", 14 | "@react-navigation/drawer": "^6.6.15", 15 | "@react-navigation/material-top-tabs": "^6.6.13", 16 | "@react-navigation/native": "^6.1.17", 17 | "iso-639-1": "^3.1.2", 18 | "react": "18.2.0", 19 | "react-native": "0.74.2", 20 | "react-native-autoheight-webview": "^1.6.5", 21 | "react-native-dialog": "^9.3.0", 22 | "react-native-draggable-flatlist": "^4.0.1", 23 | "react-native-gesture-handler": "^2.16.2", 24 | "react-native-localization": "^2.3.2", 25 | "react-native-pager-view": "^6.3.1", 26 | "react-native-paper": "^5.12.3", 27 | "react-native-picker-select": "^9.1.3", 28 | "react-native-reanimated": "^3.12.0", 29 | "react-native-safe-area-context": "^4.10.5", 30 | "react-native-screens": "^3.31.1", 31 | "react-native-tab-view": "^3.5.2", 32 | "react-native-vector-icons": "^10.1.0", 33 | "react-native-webview": "^13.10.3" 34 | }, 35 | "devDependencies": { 36 | "@babel/core": "^7.20.0", 37 | "@babel/preset-env": "^7.20.0", 38 | "@babel/runtime": "^7.20.0", 39 | "@react-native/babel-preset": "0.74.84", 40 | "@react-native/eslint-config": "0.74.84", 41 | "@react-native/metro-config": "0.74.84", 42 | "@react-native/typescript-config": "0.74.84", 43 | "@types/react": "^18.2.6", 44 | "@types/react-test-renderer": "^18.0.0", 45 | "prettier": "2.8.8", 46 | "react-test-renderer": "18.2.0", 47 | "typescript": "5.0.4" 48 | }, 49 | "engines": { 50 | "node": ">=18" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /ios/SilverDict/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleDisplayName 8 | SilverDict 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | $(MARKETING_VERSION) 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | $(CURRENT_PROJECT_VERSION) 25 | LSRequiresIPhoneOS 26 | 27 | NSAppTransportSecurity 28 | 29 | 30 | NSAllowsArbitraryLoads 31 | 32 | NSAllowsLocalNetworking 33 | 34 | 35 | NSLocationWhenInUseUsageDescription 36 | 37 | UILaunchStoryboardName 38 | LaunchScreen 39 | UIRequiredDeviceCapabilities 40 | 41 | arm64 42 | 43 | UISupportedInterfaceOrientations 44 | 45 | UIInterfaceOrientationPortrait 46 | UIInterfaceOrientationLandscapeLeft 47 | UIInterfaceOrientationLandscapeRight 48 | 49 | UIViewControllerBasedStatusBarAppearance 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /src/components/LibraryScreen/DictionaryManager/ConfirmDeleteDialogue.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useAppContext } from '../../../AppContext'; 3 | import { JSON_HEADER, convertDictionarySnakeCaseToCamelCase, loadDataFromJsonResponse } from '../../../utils'; 4 | import ConfirmDialogue from '../../common/ConfirmDialogue'; 5 | import { localisedStrings } from '../../../translations/l10n'; 6 | 7 | export default function ConfirmDeleteDialogue(props) { 8 | const { visible, setVisible, editedDictionaryIndex, setEditedDictionaryIndex } = props; 9 | const { serverAddress, dictionaries, setDictionaries, setGroupings } = useAppContext(); 10 | 11 | function onConfirm() { 12 | fetch(`${serverAddress}/api/management/dictionaries`, { 13 | method: 'DELETE', 14 | headers: JSON_HEADER, 15 | body: JSON.stringify({ name: dictionaries[editedDictionaryIndex].name }) 16 | }) 17 | .then(loadDataFromJsonResponse) 18 | .then((data) => { 19 | setEditedDictionaryIndex(-1); // This has to happen here, otherwise the edited dictionary would be deleted before the dialogue is closed. 20 | setVisible(false); 21 | setDictionaries(data['dictionaries'].map(convertDictionarySnakeCaseToCamelCase)); 22 | setGroupings(data['groupings']); 23 | }) 24 | .catch((error) => { 25 | setEditedDictionaryIndex(-1); 26 | setVisible(false); 27 | alert(localisedStrings['dictionary-manager-alert-delete-dictionary-failure']); 28 | }); 29 | } 30 | 31 | return ( 32 | 38 | ); 39 | } 40 | -------------------------------------------------------------------------------- /src/components/LibraryScreen/GroupManager/GroupLanguageEditorDialogue.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ISO6391 from 'iso-639-1'; 3 | import TextEditorDialogue from '../../common/TextEditorDialogue'; 4 | import { useAppContext } from '../../../AppContext'; 5 | import { getSetFromLangString, JSON_HEADER, loadDataFromJsonResponse } from '../../../utils'; 6 | import { localisedStrings } from '../../../translations/l10n'; 7 | 8 | export default function GroupLanguageEditorDialogue(props) { 9 | const { visible, setVisible, name, languagesString } = props; 10 | const { serverAddress, setGroups } = useAppContext(); 11 | 12 | function handleSubmit(languagesBufferString) { 13 | const newLangs = getSetFromLangString(languagesBufferString); 14 | for (const lang of newLangs) { 15 | if (!ISO6391.validate(lang)) { 16 | alert(localisedStrings['group-manager-alert-invalid-language-code']); 17 | return; 18 | } 19 | } 20 | 21 | fetch(`${serverAddress}/api/management/group_lang`, { 22 | method: 'PUT', 23 | headers: JSON_HEADER, 24 | body: JSON.stringify({ name: name, lang: Array.from(newLangs) }) 25 | }) 26 | .then(loadDataFromJsonResponse) 27 | .then((data) => { 28 | setGroups(data); 29 | setVisible(false); 30 | }) 31 | .catch((error) => { 32 | alert(localisedStrings['group-manager-alert-failure-changing-languages']); 33 | }); 34 | } 35 | 36 | return ( 37 | 48 | ); 49 | } 50 | -------------------------------------------------------------------------------- /src/components/LibraryScreen/DictionaryManager/DictionaryNameEditorDialogue.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useAppContext } from '../../../AppContext'; 3 | import { JSON_HEADER, loadDataFromJsonResponse } from '../../../utils'; 4 | import TextEditorDialogue from '../../common/TextEditorDialogue'; 5 | import { localisedStrings } from '../../../translations/l10n'; 6 | 7 | export default function DictionaryNameEditorDialogue(props) { 8 | const { visible, setVisible, editedDictionaryIndex, setEditedDictionaryIndex } = props; 9 | const { serverAddress, dictionaries, setDictionaries } = useAppContext(); 10 | 11 | function handleSubmit(nameBuffer) { 12 | const newDisplayName = nameBuffer.length > 0 ? nameBuffer : dictionaries[editedDictionaryIndex].displayName; 13 | fetch(`${serverAddress}/api/management/dictionary_name`, { 14 | method: 'PUT', 15 | headers: JSON_HEADER, 16 | body: JSON.stringify({ name: dictionaries[editedDictionaryIndex].name, display: newDisplayName }) 17 | }) 18 | .then(loadDataFromJsonResponse) 19 | .then((data) => { 20 | if (data['success']) { 21 | let newDictionaries = [...dictionaries]; 22 | newDictionaries[editedDictionaryIndex].displayName = newDisplayName; 23 | setDictionaries(newDictionaries); 24 | } else { 25 | alert(localisedStrings['dictionary-manager-alert-edit-name-failure']); 26 | } 27 | setEditedDictionaryIndex(-1); 28 | setVisible(false); 29 | }); 30 | } 31 | 32 | return ( 33 | 43 | ); 44 | } 45 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | # Default value: -Xmx512m -XX:MaxMetaspaceSize=256m 13 | org.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=512m 14 | 15 | # When configured, Gradle will run in incubating parallel mode. 16 | # This option should only be used with decoupled projects. More details, visit 17 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 18 | # org.gradle.parallel=true 19 | 20 | # AndroidX package structure to make it clearer which packages are bundled with the 21 | # Android operating system, and which are packaged with your app's APK 22 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 23 | android.useAndroidX=true 24 | # Automatically convert third-party libraries to use AndroidX 25 | android.enableJetifier=true 26 | 27 | # Use this property to specify which architecture you want to build. 28 | # You can also override it from the CLI using 29 | # ./gradlew -PreactNativeArchitectures=x86_64 30 | reactNativeArchitectures=armeabi-v7a,arm64-v8a,x86,x86_64 31 | 32 | # Use this property to enable support to the new architecture. 33 | # This will allow you to use TurboModules and the Fabric render in 34 | # your application. You should enable this flag either if you want 35 | # to write custom TurboModules/Fabric components OR use libraries that 36 | # are providing them. 37 | newArchEnabled=false 38 | 39 | # Use this property to enable or disable the Hermes JS engine. 40 | # If set to false, you will be using JSC instead. 41 | hermesEnabled=true 42 | -------------------------------------------------------------------------------- /src/components/LibraryScreen/SourceManager/AddSourceDialogue.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import TextEditorDialogue from '../../common/TextEditorDialogue'; 3 | import { useAppContext } from '../../../AppContext'; 4 | import { JSON_HEADER, loadDataFromJsonResponse } from '../../../utils'; 5 | import { localisedStrings } from '../../../translations/l10n'; 6 | 7 | export default function AddSourceDialogue(props) { 8 | const { serverAddress, sources, setSources } = useAppContext(); 9 | const { visible, setVisible } = props; 10 | 11 | function handleSubmit(newSource) { 12 | if (newSource.length === 0) { 13 | alert(localisedStrings['source-manager-alert-empty-source']); 14 | return; 15 | } 16 | 17 | if (sources.includes(newSource)) { 18 | alert(localisedStrings['source-manager-alert-duplicate']); 19 | return; 20 | } 21 | 22 | fetch(`${serverAddress}/api/validator/source`, { 23 | method: 'POST', 24 | headers: JSON_HEADER, 25 | body: JSON.stringify({ source: newSource }) 26 | }) 27 | .then(loadDataFromJsonResponse) 28 | .then((data) => { 29 | if (data['valid']) { 30 | fetch(`${serverAddress}/api/management/sources`, { 31 | method: 'POST', 32 | headers: JSON_HEADER, 33 | body: JSON.stringify({ source: newSource }) 34 | }) 35 | .then(loadDataFromJsonResponse) 36 | .then((data) => { 37 | setSources(data); 38 | setVisible(false); 39 | }) 40 | .catch((error) => { 41 | alert(localisedStrings['source-manager-alert-failure-adding-source']); 42 | }); 43 | } else { 44 | alert(localisedStrings['source-manager-alert-invalid-source']); 45 | } 46 | }); 47 | } 48 | 49 | return ( 50 | 62 | ); 63 | } -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/rn_edit_text_material.xml: -------------------------------------------------------------------------------- 1 | 2 | 16 | 21 | 22 | 23 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /ios/SilverDictTests/SilverDictTests.m: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | 4 | #import 5 | #import 6 | 7 | #define TIMEOUT_SECONDS 600 8 | #define TEXT_TO_LOOK_FOR @"Welcome to React" 9 | 10 | @interface SilverDictTests : XCTestCase 11 | 12 | @end 13 | 14 | @implementation SilverDictTests 15 | 16 | - (BOOL)findSubviewInView:(UIView *)view matching:(BOOL (^)(UIView *view))test 17 | { 18 | if (test(view)) { 19 | return YES; 20 | } 21 | for (UIView *subview in [view subviews]) { 22 | if ([self findSubviewInView:subview matching:test]) { 23 | return YES; 24 | } 25 | } 26 | return NO; 27 | } 28 | 29 | - (void)testRendersWelcomeScreen 30 | { 31 | UIViewController *vc = [[[RCTSharedApplication() delegate] window] rootViewController]; 32 | NSDate *date = [NSDate dateWithTimeIntervalSinceNow:TIMEOUT_SECONDS]; 33 | BOOL foundElement = NO; 34 | 35 | __block NSString *redboxError = nil; 36 | #ifdef DEBUG 37 | RCTSetLogFunction( 38 | ^(RCTLogLevel level, RCTLogSource source, NSString *fileName, NSNumber *lineNumber, NSString *message) { 39 | if (level >= RCTLogLevelError) { 40 | redboxError = message; 41 | } 42 | }); 43 | #endif 44 | 45 | while ([date timeIntervalSinceNow] > 0 && !foundElement && !redboxError) { 46 | [[NSRunLoop mainRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; 47 | [[NSRunLoop mainRunLoop] runMode:NSRunLoopCommonModes beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; 48 | 49 | foundElement = [self findSubviewInView:vc.view 50 | matching:^BOOL(UIView *view) { 51 | if ([view.accessibilityLabel isEqualToString:TEXT_TO_LOOK_FOR]) { 52 | return YES; 53 | } 54 | return NO; 55 | }]; 56 | } 57 | 58 | #ifdef DEBUG 59 | RCTSetLogFunction(RCTDefaultLogFunction); 60 | #endif 61 | 62 | XCTAssertNil(redboxError, @"RedBox error: %@", redboxError); 63 | XCTAssertTrue(foundElement, @"Couldn't find element with text '%@' in %d seconds", TEXT_TO_LOOK_FOR, TIMEOUT_SECONDS); 64 | } 65 | 66 | @end 67 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | import AsyncStorage from '@react-native-async-storage/async-storage'; 2 | import { interfaceLangIsRTL } from './translations/l10n'; 3 | 4 | export const JSON_CONTENT_TYPE = 'application/json'; 5 | 6 | export const JSON_HEADER = { 7 | 'Content-Type': JSON_CONTENT_TYPE 8 | }; 9 | 10 | export function loadDataFromJsonResponse(response) { 11 | return response.json(); 12 | } 13 | 14 | export function convertDictionarySnakeCaseToCamelCase(dictionary) { 15 | return { 16 | displayName: dictionary.dictionary_display_name, 17 | name: dictionary.dictionary_name, 18 | format: dictionary.dictionary_format, 19 | filename: dictionary.dictionary_filename 20 | }; 21 | } 22 | 23 | export function convertDictionaryCamelCaseToSnakeCase(dictionary) { 24 | return { 25 | dictionary_display_name: dictionary.displayName, 26 | dictionary_name: dictionary.name, 27 | dictionary_format: dictionary.format, 28 | dictionary_filename: dictionary.filename 29 | }; 30 | } 31 | 32 | export async function storePersistentData(key, value) { 33 | try { 34 | if (value && value.length > 0 && value !== '""' && value !== '{}') { // empty string and empty object 35 | await AsyncStorage.setItem(key, value); 36 | } 37 | } catch (error) { 38 | alert(`Failed to store persistent data ${key}. Error: ${error}`); 39 | } 40 | } 41 | 42 | export async function loadPersistentData(key, defaultValue) { 43 | try { 44 | const value = await AsyncStorage.getItem(key); 45 | if (value !== null) { 46 | return value; 47 | } else { 48 | return defaultValue; 49 | } 50 | } catch (error) { 51 | alert(`Failed to load persistent data ${key}. Error: ${error}`); 52 | } 53 | } 54 | 55 | export function getSetFromLangString(lang) { 56 | lang = lang.replace(/\s/g, ''); 57 | lang = lang.toLowerCase(); 58 | const set = new Set(); 59 | for (const l of lang.split(',')) { 60 | set.add(l); 61 | } 62 | return set; 63 | } 64 | 65 | export function isRTL(s) { 66 | if (interfaceLangIsRTL) { 67 | return false; // RTL + RTL = weird RTL 68 | } 69 | 70 | var ltrChars = 71 | 'A-Za-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02B8\u0300-\u0590\u0800-\u1FFF' + 72 | '\u2C00-\uFB1C\uFDFE-\uFE6F\uFEFD-\uFFFF'; 73 | var rtlChars = '\u0591-\u07FF\uFB1D-\uFDFD\uFE70-\uFEFC'; 74 | var rtlDirCheck = new RegExp('^[^' + ltrChars + ']*[' + rtlChars + ']'); 75 | 76 | return rtlDirCheck.test(s); 77 | } -------------------------------------------------------------------------------- /src/components/SettingsScreen/DarkReaderConfigDialogue.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { View } from 'react-native'; 3 | import { Button, Dialog, Portal, Switch, Text } from 'react-native-paper'; 4 | import Slider from '@react-native-community/slider'; 5 | import { useAppContext } from '../../AppContext'; 6 | import { localisedStrings } from '../../translations/l10n'; 7 | 8 | export default function DarkReaderConfigDialogue(props) { 9 | const { visible, setVisible } = props; 10 | const { darkReaderEnabled, setDarkReaderEnabled, darkReaderBrightness, setDarkReaderBrightness, darkReaderContrast, setDarkReaderContrast, darkReaderSepia, setDarkReaderSepia } = useAppContext(); 11 | 12 | return ( 13 | 14 | setVisible(false)}> 17 | {localisedStrings['dark-reader-config-title']} 18 | 19 | {localisedStrings['dark-reader-config-performance-warning']} 20 | 21 | {localisedStrings['dark-reader-config-enabled']} 22 | setDarkReaderEnabled(!darkReaderEnabled)} 26 | /> 27 | 28 | {localisedStrings['dark-reader-config-brightness']} 29 | 36 | {localisedStrings['dark-reader-config-contrast']} 37 | 44 | {localisedStrings['dark-reader-config-sepia']} 45 | 52 | 53 | 54 | 59 | 60 | 61 | 62 | ); 63 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Description 2 | 3 | [![Crowdin](https://badges.crowdin.net/silverdict-mobile/localized.svg)](https://crowdin.com/project/silverdict-mobile) 4 | 5 | [Get it on F-Droid](https://f-droid.org/packages/com.gmail.blandilyte.silverdict) 8 | 9 | [Guide](https://github.com/Crissium/SilverDict/wiki/android) 10 | 11 | This is the mobile (Android ~~& iOS~~) client of [SilverDict](https://github.com/Crissium/SilverDict), considered feature-complete now. 12 | 13 | I cannot develop the iOS version because I don't have a Mac or a 99 USD/year Apple Developer account. If you would like to help, please contact me. 14 | 15 | # Roadmap 16 | 17 | ~~Perhaps before I do anything else, I should switch to Kotlin (which would make so many things easier) since I won't develop for iOS.~~ 18 | 19 | Kotlin's all Greek to me, so I'll stick with JavaScript for now. 20 | 21 | - [x] Basic UI (Search, History/Suggestion, Settings, etc.) 22 | - [x] Go back/forwards in local history 23 | - [x] Zoom in/out and remember the zoom level 24 | - [x] Find in page 25 | - [x] Tap to search 26 | - [x] Dictionary/Group management 27 | - [ ] Two-column view on wider screens 28 | - [x] Localisation 29 | - [x] Launch SilverDict from context menu (that is, when you select a word in another app, you can choose to search for it in SilverDict. Also this would make it integrate better with e-book readers.) 30 | 31 | ## Issues 32 | 33 | - [ ] Dynamic screen width in WebView (if you rotate from portrait to landscape, the WebView will not be resized) 34 | - [ ] The WebView takes too long to load (1 - 300 ms). When the entire Dark Reader library is loaded, the minimum could be as long as 100 ms. 35 | 36 | # Development 37 | 38 | ## Prerequisites 39 | 40 | - React Native (without Expo), Android SDK, Xcode, etc. 41 | - A working SilverDict server for testing 42 | - React Native Paper 43 | - React Navigation 44 | 45 | ## Build 46 | 47 | ```bash 48 | yarn install 49 | yarn start 50 | yarn android 51 | ``` 52 | 53 | ## Note 54 | 55 | The [Dark Reader extension](/src/components/QueryScreen/darkreader.js) is taken from the NPM package, uglified and turned into a string to make it injectable into the WebView. 56 | 57 | There has been a huge update to the web UI recently, which might supersede this project completely, since it doesn't have any Android system integration yet… 58 | -------------------------------------------------------------------------------- /src/components/LibraryScreen/GroupManager/GroupDictionaryEditorDialogue.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { FlatList } from 'react-native'; 3 | import { Button, Dialog, Portal } from 'react-native-paper'; 4 | import GroupDictionaryEditorDialogueItem from './GroupDictionaryEditorDialogueItem'; 5 | import { useAppContext } from '../../../AppContext'; 6 | import { JSON_HEADER, loadDataFromJsonResponse } from '../../../utils'; 7 | import { localisedStrings } from '../../../translations/l10n'; 8 | 9 | export default function GroupDictionaryEditorDialogue(props) { 10 | const { visible, setVisible, groupName } = props; 11 | const { serverAddress, dictionaries, groupings, setGroupings } = useAppContext(); 12 | 13 | function dictionaryIncluded(dictionaryName) { 14 | return groupings[groupName].includes(dictionaryName); 15 | } 16 | 17 | function handleToggle(dictionaryName) { 18 | const included = dictionaryIncluded(dictionaryName); 19 | const requestMethod = included ? 'DELETE' : 'POST'; 20 | 21 | fetch(`${serverAddress}/api/management/dictionary_groupings`, { 22 | method: requestMethod, 23 | headers: JSON_HEADER, 24 | body: JSON.stringify({ dictionary_name: dictionaryName, group_name: groupName }) 25 | }) 26 | .then(loadDataFromJsonResponse) 27 | .then((data) => { 28 | setGroupings(data); 29 | }) 30 | .catch((error) => { 31 | if (included) { 32 | alert(localisedStrings['group-manager-alert-failure-removing-dictionary']); 33 | } else { 34 | alert(localisedStrings['group-manager-alert-failure-adding-dictionary']); 35 | } 36 | }); 37 | } 38 | 39 | return ( 40 | 41 | setVisible(false)} 44 | > 45 | {localisedStrings['group-manager-card-title-dictionaries']} 46 | 47 | } 55 | keyExtractor={(item) => item.name} /> 56 | 57 | 58 | 63 | 64 | 65 | 66 | ); 67 | } 68 | -------------------------------------------------------------------------------- /android/app/src/main/java/com/gmail/blandilyte/silverdict/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.gmail.blandilyte.silverdict 2 | 3 | import android.content.Intent 4 | import android.os.Bundle 5 | import android.util.Log 6 | import com.facebook.react.ReactActivity 7 | import com.facebook.react.ReactActivityDelegate 8 | import com.facebook.react.bridge.Arguments 9 | import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.fabricEnabled 10 | import com.facebook.react.defaults.DefaultReactActivityDelegate 11 | import com.facebook.react.modules.core.DeviceEventManagerModule 12 | 13 | class MainActivity : ReactActivity() { 14 | 15 | override fun getMainComponentName(): String = "SilverDict" 16 | 17 | override fun createReactActivityDelegate(): ReactActivityDelegate = 18 | DefaultReactActivityDelegate(this, mainComponentName, fabricEnabled) 19 | 20 | // Define PROCESS_TEXT intent action and send it to the React Native app 21 | override fun onCreate(savedInstanceState: Bundle?) { 22 | super.onCreate(savedInstanceState) 23 | handleIntent(intent) 24 | } 25 | 26 | override fun onNewIntent(intent: Intent?) { 27 | super.onNewIntent(intent) 28 | handleIntent(intent) 29 | } 30 | 31 | private fun handleIntent(intent: Intent?) { 32 | if (intent?.action == Intent.ACTION_PROCESS_TEXT) { 33 | val selectedText = intent.getCharSequenceExtra(Intent.EXTRA_PROCESS_TEXT) 34 | Log.d("MainActivity", "Selected text: $selectedText") 35 | 36 | if (reactInstanceManager.currentReactContext != null) { 37 | sendTextToReactNative(selectedText.toString()) 38 | } else { 39 | Log.d("MainActivity", "ReactContext is null, polling") 40 | pollReactContextAndSendText(selectedText.toString()) 41 | } 42 | } 43 | } 44 | 45 | private fun pollReactContextAndSendText(selectedText: String) { 46 | Thread { 47 | while (reactInstanceManager.currentReactContext == null) { 48 | Thread.sleep(50) 49 | } 50 | // The processing code is nested very deep inside the React Native part, 51 | // So sleep for a bit more. 52 | Thread.sleep(150) 53 | sendTextToReactNative(selectedText) 54 | Log.d("MainActivity", "ReactContext is no longer null, text sent") 55 | }.start() 56 | } 57 | 58 | private fun sendTextToReactNative(selectedText: String) { 59 | val params = Arguments.createMap() 60 | params.putString("selectedText", selectedText) 61 | 62 | reactInstanceManager.currentReactContext 63 | ?.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java) 64 | ?.emit("onTextSelected", params) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/components/QueryScreen/DictionarySelection.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { FlatList } from 'react-native'; 3 | import { List, Modal, Portal, useTheme } from 'react-native-paper'; 4 | import RNPickerSelect from 'react-native-picker-select'; 5 | import { useAppContext } from '../../AppContext'; 6 | import { useQueryContext } from './QueryContext'; 7 | 8 | function DictionaryItem(props) { 9 | const { name, displayName, setVisible, jumpToDictionaryRef } = props; 10 | 11 | return ( 12 | { 15 | if (jumpToDictionaryRef.current) { 16 | try { 17 | jumpToDictionaryRef.current(name); 18 | } catch (error) { 19 | } 20 | setVisible(false); 21 | } 22 | }} 23 | /> 24 | ); 25 | } 26 | 27 | export default function DictionarySelection(props) { 28 | const theme = useTheme(); 29 | const { dictionaries, groups, groupings } = useAppContext(); 30 | const { nameActiveGroup, setNameActiveGroup, namesActiveDictionaries, jumpToDictionaryRef } = useQueryContext(); 31 | const { visible, setVisible } = props; 32 | 33 | return ( 34 | 35 | setVisible(false)} 38 | style={{ backgroundColor: theme.colors.background }} 39 | > 40 | {groupings && groupings[nameActiveGroup] && namesActiveDictionaries && ( 41 | <> 42 | { 58 | return { 59 | label: group.name, 60 | value: group.name 61 | }; 62 | })} 63 | onValueChange={(value) => { 64 | setNameActiveGroup(value); 65 | setVisible(false); 66 | }} 67 | value={nameActiveGroup} 68 | /> 69 | groupings[nameActiveGroup].includes(dictionary.name) && namesActiveDictionaries.includes(dictionary.name))} 71 | renderItem={({ item }) => 72 | } 77 | keyExtractor={(item) => item.name} 78 | /> 79 | 80 | )} 81 | 82 | 83 | ); 84 | } 85 | -------------------------------------------------------------------------------- /src/config.js: -------------------------------------------------------------------------------- 1 | export const DEFAULT_SERVER_ADDRESS = 'http://localhost:2628'; 2 | export const DEFAULT_FONT_FAMILY = 'serif'; 3 | export const DEFAULT_DARK_TEXT_COLOUR = 'grey'; 4 | export const DEFAULT_TEXT_ZOOM = 100; 5 | export const TEXT_ZOOM_MAX = 200; 6 | export const TEXT_ZOOM_MIN = 40; 7 | export const RTL_LANGS = ['ar', 'dv', 'fa', 'ha', 'he', 'ks', 'ku', 'ps', 'ug', 'ur', 'yi']; 8 | export const ADDITIONAL_GOOGLE_FONTS = { 9 | 'serif': { 10 | 'Arabic': 'Noto Naskh Arabic', 11 | 'Bengali': 'Noto Serif Bengali', 12 | 'Chinese Simplified': 'Noto Serif SC', 13 | 'Chinese Traditional': 'Noto Serif TC', 14 | 'Cyrillic': 'Noto Serif', 15 | 'Devanagari': 'Noto Serif Devanagari', 16 | 'Greek': 'Noto Serif', 17 | 'Gujarati': 'Noto Serif Gujarati', 18 | 'Gurmukhi': 'Noto Serif Gurmukhi', 19 | 'Hebrew': 'Noto Serif Hebrew', 20 | 'Japanese': 'Noto Serif JP', 21 | 'Kannada': 'Noto Serif Kannada', 22 | 'Khmer': 'Noto Serif Khmer', 23 | 'Korean': 'Noto Serif KR', 24 | 'Latin': 'Noto Serif', 25 | 'Malayalam': 'Noto Serif Malayalam', 26 | 'Myanmar': 'Noto Serif Myanmar', 27 | 'Oriya': 'Noto Serif Oriya', 28 | 'Sinhala': 'Noto Serif Sinhala', 29 | 'Tamil': 'Noto Serif Tamil', 30 | 'Telugu': 'Noto Serif Telugu', 31 | 'Thai': 'Noto Serif Thai', 32 | 'Tibetan': 'Noto Serif Tibetan', 33 | 'Vietnamese': 'Noto Serif' 34 | }, 35 | 'sans-serif': { 36 | 'Arabic': 'Noto Sans Arabic', 37 | 'Bengali': 'Noto Sans Bengali', 38 | 'Chinese Simplified': 'Noto Sans SC', 39 | 'Chinese Traditional': 'Noto Sans TC', 40 | 'Cyrillic': 'Noto Sans', 41 | 'Devanagari': 'Noto Sans', 42 | 'Greek': 'Noto Sans', 43 | 'Gujarati': 'Noto Sans Gujarati', 44 | 'Gurmukhi': 'Noto Sans Gurmukhi', 45 | 'Hebrew': 'Noto Sans Hebrew', 46 | 'Japanese': 'Noto Sans JP', 47 | 'Kannada': 'Noto Sans Kannada', 48 | 'Khmer': 'Noto Sans Khmer', 49 | 'Korean': 'Noto Sans KR', 50 | 'Latin': 'Noto Sans', 51 | 'Malayalam': 'Noto Sans Malayalam', 52 | 'Myanmar': 'Noto Sans Myanmar', 53 | 'Oriya': 'Noto Sans Oriya', 54 | 'Sinhala': 'Noto Sans Sinhala', 55 | 'Tamil': 'Noto Sans Tamil', 56 | 'Telugu': 'Noto Sans Telugu', 57 | 'Thai': 'Noto Sans Thai', 58 | 'Tibetan': 'Noto Serif Tibetan', // Tibetan always uses serif font? 59 | 'Vietnamese': 'Noto Sans' 60 | } 61 | }; 62 | export const DEFAULT_ADDITIONAL_GOOGLE_FONTS_ENABLED_STATUS = 63 | Object.keys(ADDITIONAL_GOOGLE_FONTS.serif).reduce((status, script) => { 64 | status[script] = false; 65 | return status; 66 | }, {}); 67 | export const SEPARATOR = '\x10'; // ASCII Data Link Escape, chosen because it is unlikely to appear in dictionary headwords 68 | export const DEFAULT_DARK_READER_ENABLED = false; 69 | export const DEFAULT_DARK_READER_BRIGHTNESS = 70; 70 | export const DEFAULT_DARK_READER_CONTRAST = 100; 71 | export const DEFAULT_DARK_READER_SEPIA = 10; 72 | -------------------------------------------------------------------------------- /src/components/LibraryScreen/LibraryAppbar.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { Appbar } from 'react-native-paper'; 3 | import AddDictionaryDialogue from './DictionaryManager/AddDictionaryDialogue'; 4 | import AddGroupDialogue from './GroupManager/AddGroupDialogue'; 5 | import AddSourceDialogue from './SourceManager/AddSourceDialogue'; 6 | import InProgressDialogue from '../common/InProgressDialogue'; 7 | import { useAppContext } from '../../AppContext'; 8 | import { convertDictionarySnakeCaseToCamelCase, loadDataFromJsonResponse } from '../../utils'; 9 | import { localisedStrings } from '../../translations/l10n'; 10 | 11 | export default function LibraryAppbar(props) { 12 | const { openDrawer, nameActiveTab } = props; 13 | const [addDictionaryDialogueVisible, setAddDictionaryDialogueVisible] = useState(false); 14 | const [addGroupDialogueVisible, setAddGroupDialogueVisible] = useState(false); 15 | const [addSourceDialogueVisible, setAddSourceDialogueVisible] = useState(false); 16 | const [rescaningSources, setRescaningSources] = useState(false); 17 | 18 | const { serverAddress, setDictionaries, setGroupings } = useAppContext(); 19 | function handleRescan() { 20 | setRescaningSources(true); 21 | fetch(`${serverAddress}/api/management/scan`) 22 | .then(loadDataFromJsonResponse) 23 | .then((data) => { 24 | setDictionaries(data['dictionaries'].map(convertDictionarySnakeCaseToCamelCase)); 25 | setGroupings(data['groupings']); 26 | }) 27 | .catch((error) => { 28 | alert(localisedStrings['source-manager-alert-failure-rescaning-sources']); 29 | }) 30 | .finally(() => { 31 | setRescaningSources(false); 32 | }); 33 | } 34 | 35 | const actionsOnPress = { 36 | 'Dictionaries': () => setAddDictionaryDialogueVisible(true), 37 | 'Groups': () => setAddGroupDialogueVisible(true), 38 | 'Sources': () => setAddSourceDialogueVisible(true) 39 | }; 40 | 41 | return ( 42 | 43 | openDrawer()} 46 | /> 47 | 50 | {nameActiveTab === 'Sources' && 51 | handleRescan()} 54 | />} 55 | 59 | 63 | 67 | 71 | 75 | 76 | ); 77 | } -------------------------------------------------------------------------------- /src/components/LibraryScreen/GroupManager/AddGroupDialogue.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { Button, Dialog, Text, TextInput, Portal } from 'react-native-paper'; 3 | import ISO6391 from 'iso-639-1'; 4 | import { useAppContext } from '../../../AppContext'; 5 | import { getSetFromLangString, JSON_HEADER, loadDataFromJsonResponse } from '../../../utils'; 6 | import { localisedStrings } from '../../../translations/l10n'; 7 | 8 | export default function AddGroupDialogue(props) { 9 | const { visible, setVisible } = props; 10 | const { serverAddress, setGroups, groupings, setGroupings } = useAppContext(); 11 | 12 | const [newGroupName, setNewGroupName] = useState(''); 13 | const [newGroupLangsString, setNewGroupLangsString] = useState(''); 14 | 15 | function handleSubmit() { 16 | if (newGroupName.length === 0) { 17 | alert(localisedStrings['group-manager-alert-empty-name']); 18 | return; 19 | } 20 | 21 | if (groupings[newGroupName]) { 22 | alert(localisedStrings['group-manager-alert-name-exists']); 23 | return; 24 | } 25 | 26 | const langs = getSetFromLangString(newGroupLangsString); 27 | for (const lang of langs) { 28 | if (!ISO6391.validate(lang)) { 29 | alert(localisedStrings['group-manager-alert-invalid-language-code']); 30 | return; 31 | } 32 | } 33 | 34 | fetch(`${serverAddress}/api/management/groups`, { 35 | method: 'POST', 36 | headers: JSON_HEADER, 37 | body: JSON.stringify({ 38 | name: newGroupName, 39 | lang: Array.from(langs) 40 | }) 41 | }) 42 | .then(loadDataFromJsonResponse) 43 | .then((data) => { 44 | setGroups(data['groups']); 45 | setGroupings(data['groupings']); 46 | setVisible(false); 47 | }) 48 | .catch((error) => { 49 | alert(localisedStrings['group-manager-alert-failure-adding-group']); 50 | }); 51 | } 52 | 53 | return ( 54 | 55 | setVisible(false)} 58 | > 59 | {localisedStrings['group-manager-dialogue-add-title']} 60 | 61 | {localisedStrings['group-manager-dialogue-add-content']} 62 | setNewGroupName(text)} 66 | mode='outlined' 67 | /> 68 | {localisedStrings['group-manager-dialogue-change-languages-content']} 69 | setNewGroupLangsString(text)} 73 | mode='outlined' 74 | autoCapitalize='none' 75 | autoCorrect={false} 76 | /> 77 | 78 | 79 | 84 | 89 | 90 | 91 | 92 | ); 93 | } -------------------------------------------------------------------------------- /android/gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%"=="" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%"=="" set DIRNAME=. 29 | @rem This is normally unused 30 | set APP_BASE_NAME=%~n0 31 | set APP_HOME=%DIRNAME% 32 | 33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 35 | 36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 38 | 39 | @rem Find java.exe 40 | if defined JAVA_HOME goto findJavaFromJavaHome 41 | 42 | set JAVA_EXE=java.exe 43 | %JAVA_EXE% -version >NUL 2>&1 44 | if %ERRORLEVEL% equ 0 goto execute 45 | 46 | echo. 1>&2 47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 48 | echo. 1>&2 49 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 50 | echo location of your Java installation. 1>&2 51 | 52 | goto fail 53 | 54 | :findJavaFromJavaHome 55 | set JAVA_HOME=%JAVA_HOME:"=% 56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 57 | 58 | if exist "%JAVA_EXE%" goto execute 59 | 60 | echo. 1>&2 61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 62 | echo. 1>&2 63 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 64 | echo location of your Java installation. 1>&2 65 | 66 | goto fail 67 | 68 | :execute 69 | @rem Setup the command line 70 | 71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 72 | 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if %ERRORLEVEL% equ 0 goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | set EXIT_CODE=%ERRORLEVEL% 85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 87 | exit /b %EXIT_CODE% 88 | 89 | :mainEnd 90 | if "%OS%"=="Windows_NT" endlocal 91 | 92 | :omega 93 | -------------------------------------------------------------------------------- /src/components/LibraryScreen/GroupManager/GroupCard.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { FlatList } from 'react-native'; 3 | import { Button, Card, List } from 'react-native-paper'; 4 | import ISO6391 from 'iso-639-1'; 5 | import ConfirmDeleteDialogue from './ConfirmDeleteDialogue'; 6 | import GroupNameEditorDialogue from './GroupNameEditorDialogue'; 7 | import GroupLanguageEditorDialogue from './GroupLanguageEditorDialogue'; 8 | import GroupDictionaryEditorDialogue from './GroupDictionaryEditorDialogue'; 9 | import { useAppContext } from '../../../AppContext'; 10 | import { localisedStrings } from '../../../translations/l10n'; 11 | 12 | export default function GroupCard(props) { 13 | const { name, lang, onLongPress } = props; 14 | const { dictionaries, groupings } = useAppContext(); 15 | const isDefaultGroup = name === 'Default Group'; 16 | const [confirmDeleteDialogueVisible, setConfirmDeleteDialogueVisible] = useState(false); 17 | const [groupNameEditorDialogueVisible, setGroupNameEditorDialogueVisible] = useState(false); 18 | const [groupLanguageEditorDialogueVisible, setGroupLanguageEditorDialogueVisible] = useState(false); 19 | const [groupDictionaryEditorDialogueVisible, setGroupDictionaryEditorDialogueVisible] = useState(false); 20 | 21 | return ( 22 | 23 | 25 | {!isDefaultGroup && 26 | 29 | 34 | 39 | } 40 | 41 | setGroupLanguageEditorDialogueVisible(true)} 44 | > 45 | } 49 | keyExtractor={(item) => item} /> 50 | 51 | setGroupDictionaryEditorDialogueVisible(true)} 54 | > 55 | {groupings[name] && groupings[name].includes(dictionary.name))} 57 | renderItem={({ item }) => } 58 | keyExtractor={(item) => item.name} />} 59 | 60 | 61 | 65 | 69 | 74 | 78 | 79 | ); 80 | } 81 | -------------------------------------------------------------------------------- /src/components/LibraryScreen/DictionaryManager.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import DraggableFlatList from 'react-native-draggable-flatlist'; 3 | import { List } from 'react-native-paper'; 4 | import EditMenu from './DictionaryManager/EditMenu'; 5 | import DictionaryNameEditorDialogue from './DictionaryManager/DictionaryNameEditorDialogue'; 6 | import ConfirmDeleteDialogue from './DictionaryManager/ConfirmDeleteDialogue'; 7 | import { useAppContext } from '../../AppContext'; 8 | import { convertDictionaryCamelCaseToSnakeCase, convertDictionarySnakeCaseToCamelCase, JSON_HEADER, loadDataFromJsonResponse } from '../../utils'; 9 | import { localisedStrings } from '../../translations/l10n'; 10 | 11 | export default function DictionaryManager() { 12 | const { serverAddress, dictionaries, setDictionaries } = useAppContext(); 13 | const [editedDictionaryIndex, setEditedDictionaryIndex] = useState(-1); 14 | const [menuVisible, setMenuVisible] = useState(false); 15 | const [menuAnchor, setMenuAnchor] = useState({ x: 0, y: 0 }); 16 | const [nameEditorVisible, setNameEditorVisible] = useState(false); 17 | const [confirmDeleteVisible, setConfirmDeleteVisible] = useState(false); 18 | 19 | function handleEdit(index) { 20 | setEditedDictionaryIndex(index); 21 | setNameEditorVisible(true); 22 | } 23 | 24 | function handleDelete(index) { 25 | setEditedDictionaryIndex(index); 26 | setConfirmDeleteVisible(true); 27 | } 28 | 29 | function handlePress(event, dictionaryName) { 30 | setMenuAnchor({ x: event.nativeEvent.pageX, y: event.nativeEvent.pageY }); 31 | setMenuVisible(true); 32 | setEditedDictionaryIndex(dictionaries.findIndex((dictionary) => dictionary.name === dictionaryName)); 33 | } 34 | 35 | function handleReorder(newDictionaries) { 36 | fetch(`${serverAddress}/api/management/dictionaries`, { 37 | method: 'PUT', 38 | headers: JSON_HEADER, 39 | body: JSON.stringify(newDictionaries.map(convertDictionaryCamelCaseToSnakeCase)) 40 | }) 41 | .then(loadDataFromJsonResponse) 42 | .then((data) => { 43 | setDictionaries(data.map(convertDictionarySnakeCaseToCamelCase)); 44 | }) 45 | .catch((error) => { 46 | alert(localisedStrings['dictionary-manager-alert-failure-reordering']); 47 | }); 48 | } 49 | 50 | return ( 51 | <> 52 | handleReorder(data)} 55 | renderItem={({ item, drag }) => 56 | handlePress(event, item.name)} 59 | onLongPress={drag} 60 | /> 61 | } 62 | keyExtractor={(item) => item.name} 63 | /> 64 | handleEdit(editedDictionaryIndex)} 69 | onDelete={() => handleDelete(editedDictionaryIndex)} 70 | /> 71 | {editedDictionaryIndex >= 0 && } 77 | {editedDictionaryIndex >= 0 && } 83 | 84 | ); 85 | } 86 | -------------------------------------------------------------------------------- /ios/SilverDict.xcodeproj/xcshareddata/xcschemes/SilverDict.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 42 | 43 | 53 | 55 | 61 | 62 | 63 | 64 | 70 | 72 | 78 | 79 | 80 | 81 | 83 | 84 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /src/components/QueryScreen/ArticleBottomBar.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { View } from 'react-native'; 3 | import { Appbar, TextInput, useTheme } from 'react-native-paper'; 4 | import { TEXT_ZOOM_MAX, TEXT_ZOOM_MIN } from '../../config'; 5 | import { useQueryContext } from './QueryContext'; 6 | import { localisedStrings } from '../../translations/l10n'; 7 | 8 | const TEXT_ZOOM_STEP = 10; 9 | 10 | export default function ArticleBottomBar() { 11 | const onSurfaceColour = useTheme().colors.onSurface; 12 | const { ableToGoBackInHistory, ableToGoForwardInHistory, searchInLocalHistory, textZoom, setTextZoom, findInPageRef } = useQueryContext(); 13 | 14 | const [findBarActive, setFindBarActive] = useState(false); 15 | const [wordToFind, setWordToFind] = useState(''); 16 | 17 | function handleTextZoomDecrease() { 18 | setTextZoom(Math.max(textZoom - TEXT_ZOOM_STEP, TEXT_ZOOM_MIN)); 19 | } 20 | 21 | function handleTextZoomIncrease() { 22 | setTextZoom(Math.min(textZoom + TEXT_ZOOM_STEP, TEXT_ZOOM_MAX)); 23 | } 24 | 25 | if (findBarActive) 26 | return ( 27 | 28 | 29 | setWordToFind(text)} 43 | onSubmitEditing={(e) => { 44 | findInPageRef.current(wordToFind, false); 45 | }} 46 | /> 47 | { 51 | findInPageRef.current(wordToFind, true); 52 | }} /> 53 | { 57 | findInPageRef.current(wordToFind, false); 58 | }} /> 59 | { 63 | setFindBarActive(false); 64 | setWordToFind(''); 65 | }} /> 66 | 67 | 68 | ); 69 | else 70 | return ( 71 | 72 | 73 | { 74 | ableToGoBackInHistory ? 75 | searchInLocalHistory(-1)} /> : 79 | 82 | } 83 | { 84 | ableToGoForwardInHistory ? 85 | searchInLocalHistory(+1)} /> : 89 | 92 | } 93 | handleTextZoomDecrease()} /> 97 | handleTextZoomIncrease()} /> 101 | setFindBarActive(true)} /> 105 | 106 | 107 | ); 108 | } 109 | -------------------------------------------------------------------------------- /ios/SilverDict/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 24 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /android/app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: "com.android.application" 2 | apply plugin: "org.jetbrains.kotlin.android" 3 | apply plugin: "com.facebook.react" 4 | 5 | /** 6 | * This is the configuration block to customize your React Native Android app. 7 | * By default you don't need to apply any configuration, just uncomment the lines you need. 8 | */ 9 | react { 10 | /* Folders */ 11 | // The root of your project, i.e. where "package.json" lives. Default is '..' 12 | // root = file("../") 13 | // The folder where the react-native NPM package is. Default is ../node_modules/react-native 14 | // reactNativeDir = file("../node_modules/react-native") 15 | // The folder where the react-native Codegen package is. Default is ../node_modules/@react-native/codegen 16 | // codegenDir = file("../node_modules/@react-native/codegen") 17 | // The cli.js file which is the React Native CLI entrypoint. Default is ../node_modules/react-native/cli.js 18 | // cliFile = file("../node_modules/react-native/cli.js") 19 | 20 | /* Variants */ 21 | // The list of variants to that are debuggable. For those we're going to 22 | // skip the bundling of the JS bundle and the assets. By default is just 'debug'. 23 | // If you add flavors like lite, prod, etc. you'll have to list your debuggableVariants. 24 | // debuggableVariants = ["liteDebug", "prodDebug"] 25 | 26 | /* Bundling */ 27 | // A list containing the node command and its flags. Default is just 'node'. 28 | // nodeExecutableAndArgs = ["node"] 29 | // 30 | // The command to run when bundling. By default is 'bundle' 31 | // bundleCommand = "ram-bundle" 32 | // 33 | // The path to the CLI configuration file. Default is empty. 34 | // bundleConfig = file(../rn-cli.config.js) 35 | // 36 | // The name of the generated asset file containing your JS bundle 37 | // bundleAssetName = "MyApplication.android.bundle" 38 | // 39 | // The entry file for bundle generation. Default is 'index.android.js' or 'index.js' 40 | // entryFile = file("../js/MyApplication.android.js") 41 | // 42 | // A list of extra flags to pass to the 'bundle' commands. 43 | // See https://github.com/react-native-community/cli/blob/main/docs/commands.md#bundle 44 | // extraPackagerArgs = [] 45 | 46 | /* Hermes Commands */ 47 | // The hermes compiler command to run. By default it is 'hermesc' 48 | // hermesCommand = "$rootDir/my-custom-hermesc/bin/hermesc" 49 | // 50 | // The list of flags to pass to the Hermes compiler. By default is "-O", "-output-source-map" 51 | // hermesFlags = ["-O", "-output-source-map"] 52 | } 53 | 54 | /** 55 | * Set this to true to Run Proguard on Release builds to minify the Java bytecode. 56 | */ 57 | def enableProguardInReleaseBuilds = true 58 | 59 | /** 60 | * The preferred build flavor of JavaScriptCore (JSC) 61 | * 62 | * For example, to use the international variant, you can use: 63 | * `def jscFlavor = 'org.webkit:android-jsc-intl:+'` 64 | * 65 | * The international variant includes ICU i18n library and necessary data 66 | * allowing to use e.g. `Date.toLocaleString` and `String.localeCompare` that 67 | * give correct results when using with locales other than en-US. Note that 68 | * this variant is about 6MiB larger per architecture than default. 69 | */ 70 | def jscFlavor = 'org.webkit:android-jsc:+' 71 | 72 | android { 73 | ndkVersion rootProject.ext.ndkVersion 74 | buildToolsVersion rootProject.ext.buildToolsVersion 75 | compileSdk rootProject.ext.compileSdkVersion 76 | 77 | namespace "com.gmail.blandilyte.silverdict" 78 | defaultConfig { 79 | applicationId "com.gmail.blandilyte.silverdict" 80 | minSdkVersion rootProject.ext.minSdkVersion 81 | targetSdkVersion rootProject.ext.targetSdkVersion 82 | versionCode 13 83 | versionName "1.5.0" 84 | } 85 | signingConfigs { 86 | debug { 87 | storeFile file('debug.keystore') 88 | storePassword 'android' 89 | keyAlias 'androiddebugkey' 90 | keyPassword 'android' 91 | } 92 | 93 | release { 94 | storeFile file('release_key.keystore') 95 | storePassword '!!REPLACE_ME!!' 96 | keyAlias 'silverdict' 97 | keyPassword '!!REPLACE_ME!!' 98 | } 99 | } 100 | buildTypes { 101 | debug { 102 | signingConfig signingConfigs.debug 103 | } 104 | release { 105 | // Caution! In production, you need to generate your own keystore file. 106 | // see https://reactnative.dev/docs/signed-apk-android. 107 | signingConfig signingConfigs.debug 108 | minifyEnabled enableProguardInReleaseBuilds 109 | shrinkResources enableProguardInReleaseBuilds 110 | proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro" 111 | } 112 | } 113 | 114 | splits { 115 | abi { 116 | reset() 117 | enable true 118 | include "armeabi-v7a", "x86", "arm64-v8a", "x86_64" 119 | universalApk false 120 | } 121 | } 122 | 123 | project.ext.versionCodes = ["armeabi-v7a":1, "x86":2, "arm64-v8a":3, "x86_64":4] 124 | 125 | android.applicationVariants.all { variant -> 126 | variant.outputs.each { output -> 127 | output.versionCodeOverride = 128 | project.ext.versionCodes.get(output.getFilter( 129 | com.android.build.OutputFile.ABI), 0) + android.defaultConfig.versionCode * 10 130 | } 131 | } 132 | } 133 | 134 | dependencies { 135 | // The version of react-native is set by the React Native Gradle Plugin 136 | implementation("com.facebook.react:react-android") 137 | 138 | if (hermesEnabled.toBoolean()) { 139 | implementation("com.facebook.react:hermes-android") 140 | } else { 141 | implementation jscFlavor 142 | } 143 | } 144 | 145 | apply from: file("../../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesAppBuildGradle(project) 146 | 147 | project.ext.vectoricons = [ 148 | iconFontNames: [ 'MaterialCommunityIcons.ttf' ] // Name of the font files you want to copy 149 | ] 150 | 151 | apply from: file("../../node_modules/react-native-vector-icons/fonts.gradle"); 152 | -------------------------------------------------------------------------------- /src/components/LibraryScreen/DictionaryManager/AddDictionaryDialogue.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { Button, Dialog, Text, TextInput, Portal, useTheme } from 'react-native-paper'; 3 | import RNPickerSelect from 'react-native-picker-select'; 4 | import { useAppContext } from '../../../AppContext'; 5 | import { JSON_HEADER, loadDataFromJsonResponse, convertDictionarySnakeCaseToCamelCase } from '../../../utils'; 6 | import { localisedStrings } from '../../../translations/l10n'; 7 | 8 | export default function AddDictionaryDialogue(props) { 9 | const theme = useTheme(); 10 | const { visible, setVisible } = props; 11 | const { serverAddress, formats, dictionaries, setDictionaries, groups, setGroupings } = useAppContext(); 12 | const [newDictionaryDisplayName, setNewDictionaryDisplayName] = useState(''); 13 | const [newDictionaryFilename, setNewDictionaryFilename] = useState(''); 14 | const [newDictionaryFormat, setNewDictionaryFormat] = useState(formats[0]); 15 | const [newDictionaryGroup, setNewDictionaryGroup] = useState('Default Group'); 16 | 17 | function handleSubmit() { 18 | if (newDictionaryDisplayName.length === 0) { 19 | alert(localisedStrings['add-dictionary-dialogue-alert-empty-name']); 20 | return; 21 | } 22 | 23 | if (newDictionaryFilename.length === 0) { 24 | alert(localisedStrings['add-dictionary-dialogue-alert-empty-filename']); 25 | return; 26 | } 27 | 28 | const pathSep = newDictionaryFilename.includes('/') ? '/' : '\\'; 29 | const newDictionaryName = newDictionaryFilename.split(pathSep).pop().split('.')[0]; 30 | if (dictionaries.map((dictionary) => dictionary.name).includes(newDictionaryName)) { 31 | alert(localisedStrings['add-dictionary-dialogue-alert-duplicate']); 32 | return; 33 | } 34 | 35 | const newDictionaryInfo = { 36 | dictionary_display_name: newDictionaryDisplayName, 37 | dictionary_name: newDictionaryName, 38 | dictionary_format: newDictionaryFormat, 39 | dictionary_filename: newDictionaryFilename 40 | }; 41 | 42 | fetch(`${serverAddress}/api/validator/dictionary_info`, { 43 | method: 'POST', 44 | headers: JSON_HEADER, 45 | body: JSON.stringify(newDictionaryInfo) 46 | }) 47 | .then(loadDataFromJsonResponse) 48 | .then((data) => { 49 | if (data['valid']) { 50 | newDictionaryInfo['group_name'] = newDictionaryGroup; 51 | fetch(`${serverAddress}/api/management/dictionaries`, { 52 | method: 'POST', 53 | headers: JSON_HEADER, 54 | body: JSON.stringify(newDictionaryInfo) 55 | }) 56 | .then(loadDataFromJsonResponse) 57 | .then((data) => { 58 | setDictionaries(data['dictionaries'].map(convertDictionarySnakeCaseToCamelCase)); 59 | setGroupings(data['groupings']); 60 | setVisible(false); 61 | }) 62 | .catch((error) => { 63 | alert(localisedStrings['add-dictionary-dialogue-alert-failure-adding']); 64 | }); 65 | } else { 66 | alert(localisedStrings['add-dictionary-dialogue-alert-invalid']); 67 | } 68 | }) 69 | .catch((error) => { 70 | alert(localisedStrings['add-dictionary-dialogue-alert-failure-validating']); 71 | }); 72 | } 73 | 74 | return ( 75 | 76 | setVisible(false)} 79 | > 80 | {localisedStrings['add-dictionary-dialogue-title']} 81 | 82 | {localisedStrings['add-dictionary-dialogue-content-name']} 83 | setNewDictionaryDisplayName(text)} 88 | /> 89 | {localisedStrings['add-dictionary-dialogue-content-filename']} 90 | setNewDictionaryFilename(text)} 95 | /> 96 | {localisedStrings['add-dictionary-dialogue-content-format']} 97 | { 113 | return { 114 | label: format, 115 | value: format 116 | }; 117 | })} 118 | onValueChange={(value) => setNewDictionaryFormat(value)} 119 | value={newDictionaryFormat} 120 | /> 121 | {localisedStrings['add-dictionary-dialogue-content-group']} 122 | { 138 | return { 139 | label: group.name, 140 | value: group.name 141 | }; 142 | })} 143 | onValueChange={(value) => setNewDictionaryGroup(value)} 144 | value={newDictionaryGroup} 145 | /> 146 | 147 | 148 | 153 | 158 | 159 | 160 | 161 | ); 162 | } -------------------------------------------------------------------------------- /src/translations/zh-CN.json: -------------------------------------------------------------------------------- 1 | { 2 | "additional-fonts-dialogue-content": "从 Google Fonts 下载附加字体。\n有些字体文件很大,请仅当看到小方框或问号时使用。", 3 | "additional-fonts-dialogue-title": "附加字体", 4 | "add-dictionary-dialogue-alert-duplicate": "已存在同名词典。", 5 | "add-dictionary-dialogue-alert-empty-filename": "文件名不能为空。", 6 | "add-dictionary-dialogue-alert-empty-name": "词典名称不能为空。", 7 | "add-dictionary-dialogue-alert-failure-adding": "添加词典失败。", 8 | "add-dictionary-dialogue-alert-failure-validating": "检查词典有效性失败。", 9 | "add-dictionary-dialogue-alert-invalid": "无效的词典信息。", 10 | "add-dictionary-dialogue-title": "添加词典", 11 | "add-dictionary-dialogue-content-group": "分组", 12 | "add-dictionary-dialogue-content-filename": "文件名", 13 | "add-dictionary-dialogue-content-format": "格式", 14 | "add-dictionary-dialogue-content-name": "词典名", 15 | "app-context-message-failure-fetching-data": "从服务器获取数据失败。请检查网络连接,或更改服务器地址并重试。", 16 | "article-view-tap-to-search-dialogue-message": "搜索 “{0}”?", 17 | "change-address-dialogue-message-unable-to-connect": "无法连接到服务器。", 18 | "change-address-dialogue-title": "变更服务器地址", 19 | "change-colour-dialogue-content": "推荐填写“white”(白色)或“grey”(灰色)。\n该设置仅在 Dark Reader 扩展被禁用时生效。", 20 | "change-colour-dialogue-title": "变更暗色模式下字体颜色", 21 | "change-font-dialogue-font-sans-serif": "无衬线字体(黑体)", 22 | "change-font-dialogue-font-serif": "衬线字体(宋体)", 23 | "change-font-dialogue-title": "变更字体", 24 | "change-size-history-dialogue-message-failure": "更新历史记录大小失败。", 25 | "change-size-history-dialogue-placeholder": "非负整数", 26 | "change-size-history-dialogue-title": "变更历史记录大小", 27 | "change-size-suggestion-dialogue-message-failure": "更新候选词数量失败。", 28 | "change-size-suggestion-dialogue-placeholder": "正整数", 29 | "change-size-suggestion-dialogue-title": "变更候选词数量", 30 | "clear-history-dialogue-content": "确认清空历史记录?", 31 | "clear-history-dialogue-message-failure": "清空历史记录失败。", 32 | "clear-history-dialogue-title": "清空历史记录", 33 | "confirm-recreate-ngram-dialogue-content": "确认重建 ngram 索引?", 34 | "confirm-recreate-ngram-dialogue-message-failure": "重建 ngram 索引失败。", 35 | "confirm-recreate-ngram-dialogue-message-in-progress": "正在重建 ngram 索引…", 36 | "confirm-recreate-ngram-dialogue-message-success": "成功重建 ngram 索引。", 37 | "confirm-recreate-ngram-dialogue-title": "重建 ngram 索引", 38 | "dark-reader-config-brightness": "亮度", 39 | "dark-reader-config-contrast": "对比度", 40 | "dark-reader-config-sepia": "棕褐色", 41 | "dark-reader-config-enabled": "启用", 42 | "dark-reader-config-performance-warning": "注意,该扩展可能导致文章加载速度变慢。\n", 43 | "dark-reader-config-title": "Dark Reader 扩展设置", 44 | "dictionary-manager-alert-edit-name-failure": "更新词典名称失败。", 45 | "dictionary-manager-alert-delete-dictionary-failure": "删除词典失败。", 46 | "dictionary-manager-alert-failure-reordering": "重新排序词典失败。", 47 | "dictionary-manager-dialogue-edit-name-title": "编辑词典名", 48 | "dictionary-manager-dialogue-delete-dictionary-content": "确定从数据库中删除《{0}》?词典文件不会被删除。", 49 | "dictionary-manager-dialogue-delete-dictionary-title": "删除词典", 50 | "dictionary-manager-dialogue-name-placeholder": "新词典名称", 51 | "drawer-library-label": "库", 52 | "drawer-query-label": "查询", 53 | "drawer-reconnect-label": "重新连接", 54 | "drawer-settings-label": "设置", 55 | "generic-alert-invalid-number": "无效数字。", 56 | "generic-cancel": "取消", 57 | "generic-delete": "删除", 58 | "generic-edit": "编辑", 59 | "generic-no": "否", 60 | "generic-ok": "好", 61 | "generic-rename": "重命名", 62 | "generic-yes": "是", 63 | "group-manager-alert-empty-name": "组名不能为空。", 64 | "group-manager-alert-failure-adding-dictionary": "将词典添加到分组失败。", 65 | "group-manager-alert-failure-adding-group": "添加分组失败。", 66 | "group-manager-alert-failure-changing-languages": "更改分组语言失败。", 67 | "group-manager-alert-failure-removing-dictionary": "从分组中删除词典失败。", 68 | "group-manager-alert-failure-removing-group": "删除分组失败。", 69 | "group-manager-alert-failure-renaming-group": "重命名分组失败。", 70 | "group-manager-alert-failure-reordering-groups": "重新排序分组失败。", 71 | "group-manager-alert-invalid-language-code": "发现无效的语言代码。", 72 | "group-manager-alert-name-exists": "已存在同名分组。", 73 | "group-manager-card-title-dictionaries": "词典", 74 | "group-manager-card-title-languages": "语言", 75 | "group-manager-dialogue-add-title": "添加分组", 76 | "group-manager-dialogue-add-content": "组名", 77 | "group-manager-dialogue-change-languages-content": "ISO-639-1 两字母语言代码列表,逗号隔开,不能为空。", 78 | "group-manager-dialogue-change-languages-title": "更改分组语言", 79 | "group-manager-dialogue-delete-content": "确定删除分组“{0}”?", 80 | "group-manager-dialogue-delete-title": "删除分组", 81 | "group-manager-dialogue-rename-title": "重命名分组", 82 | "library-screen-groups": "分组", 83 | "library-screen-sources": "来源", 84 | "placeholder-search": "搜索……", 85 | "query-screen-failure-fetch-articles": "获取文章失败。", 86 | "query-screen-failure-fetch-suggestions": "获取候选词失败。", 87 | "script-name-arabic": "阿拉伯字母", 88 | "script-name-bengali": "孟加拉文", 89 | "script-name-chinese-simplified": "简体中文", 90 | "script-name-chinese-traditional": "繁体中文", 91 | "script-name-cyrillic": "西里尔字母", 92 | "script-name-devanagari": "天城书", 93 | "script-name-greek": "希腊字母", 94 | "script-name-gujarati": "古吉拉特文", 95 | "script-name-gurmukhi": "果鲁穆奇字母", 96 | "script-name-hebrew": "希伯来文", 97 | "script-name-japanese": "日文", 98 | "script-name-kannada": "坎纳达文", 99 | "script-name-khmer": "高棉文", 100 | "script-name-korean": "韩文", 101 | "script-name-latin": "拉丁字母", 102 | "script-name-malayalam": "马拉雅拉姆文", 103 | "script-name-myanmar": "缅甸文", 104 | "script-name-oriya": "奥里雅文", 105 | "script-name-sinhala": "僧伽罗文", 106 | "script-name-tamil": "泰米尔文", 107 | "script-name-telugu": "泰卢固文", 108 | "script-name-thai": "泰文", 109 | "script-name-tibetan": "藏文", 110 | "script-name-vietnamese": "越南文", 111 | "settings-screen-dark-text-colour-title": "暗色模式下字体颜色", 112 | "settings-screen-font-family-title": "字体", 113 | "settings-screen-recreate-ngram-description": "可能会很慢", 114 | "settings-screen-server-address-title": "服务器地址", 115 | "settings-screen-size-history-title": "历史记录大小", 116 | "settings-screen-size-suggestions-title": "候选词数量", 117 | "source-manager-alert-duplicate": "来源已存在。", 118 | "source-manager-alert-empty-source": "来源不能为空。", 119 | "source-manager-alert-failure-adding-source": "添加来源失败。", 120 | "source-manager-alert-failure-deleting-source": "删除来源失败。", 121 | "source-manager-alert-failure-fetching-sources": "获取来源失败。", 122 | "source-manager-alert-failure-rescaning-sources": "重新扫描失败。", 123 | "source-manager-alert-invalid-source": "无效的来源。", 124 | "source-manager-dialogue-add-source-title": "添加来源", 125 | "source-manager-dialogue-rescanning-sources-title": "正在重新扫描", 126 | "source-manager-confirm-delete-title": "删除来源", 127 | "source-manager-confirm-delete-content": "确定删除来源 {0} ?" 128 | } -------------------------------------------------------------------------------- /src/components/SettingsScreen.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { FlatList, View } from 'react-native'; 3 | import { Appbar, List } from 'react-native-paper'; 4 | import ChangeAddressDialogue from './SettingsScreen/ChangeAddressDialogue'; 5 | import ChangeFontDialogue from './SettingsScreen/ChangeFontDialogue'; 6 | import AdditionalFontsDialogue from './SettingsScreen/AdditionalFontsDialogue'; 7 | import ChangeColourDialogue from './SettingsScreen/ChangeColourDialogue'; 8 | import DarkReaderConfigDialogue from './SettingsScreen/DarkReaderConfigDialogue'; 9 | import ChangeSizeSuggestionDialogue from './SettingsScreen/ChangeSizeSuggestionDialogue'; 10 | import ChangeSizeHistoryDialogue from './SettingsScreen/ChangeSizeHistoryDialogue'; 11 | import ConfirmClearHistoryDialogue from './SettingsScreen/ConfirmClearHistoryDialogue'; 12 | import ConfirmRecreateNgramDialogue from './SettingsScreen/ConfirmRecreateNgramDialogue'; 13 | import { useAppContext } from '../AppContext'; 14 | import { localisedStrings } from '../translations/l10n'; 15 | 16 | export default function SettingsScreen({ navigation }) { 17 | const { serverAddress, setServerAddress, fontFamily, setFontFamily, scriptsWithAdditionalFonts, setScriptsWithAdditionalFonts, darkTextColour, setDarkTextColour, sizeSuggestion, setSizeSuggestion, sizeHistory, setSizeHistory, setHistory } = useAppContext(); 18 | 19 | const [addressDialogueVisible, setAddressDialogueVisible] = useState(false); 20 | const [fontDialogueVisible, setFontDialogueVisible] = useState(false); 21 | const [additionalFontsDialogueVisible, setAdditionalFontsDialogueVisible] = useState(false); 22 | const [colourDialogueVisible, setColourDialogueVisible] = useState(false); 23 | const [darkReaderConfigDialogueVisible, setDarkReaderConfigDialogueVisible] = useState(false); 24 | const [sizeSuggestionDialogueVisible, setSizeSuggestionDialogueVisible] = useState(false); 25 | const [sizeHistoryDialogueVisible, setSizeHistoryDialogueVisible] = useState(false); 26 | const [clearHistoryDialogueVisible, setClearHistoryDialogueVisible] = useState(false); 27 | const [recreateNgramDialogueVisible, setRecreateNgramDialogueVisible] = useState(false); 28 | 29 | return ( 30 | 31 | 32 | navigation.openDrawer()} /> 33 | 34 | 35 | { 41 | setAddressDialogueVisible(true); 42 | } 43 | }, 44 | { 45 | title: localisedStrings['settings-screen-font-family-title'], 46 | description: localisedStrings[`change-font-dialogue-font-${fontFamily}`], 47 | onPress: () => { 48 | setFontDialogueVisible(true); 49 | } 50 | }, 51 | { 52 | title: localisedStrings['additional-fonts-dialogue-title'], 53 | description: '', 54 | onPress: () => { 55 | setAdditionalFontsDialogueVisible(true); 56 | } 57 | }, 58 | { 59 | title: localisedStrings['settings-screen-dark-text-colour-title'], 60 | description: darkTextColour, 61 | onPress: () => { 62 | setColourDialogueVisible(true); 63 | } 64 | }, 65 | { 66 | title: localisedStrings['dark-reader-config-title'], 67 | description: '', 68 | onPress: () => { 69 | setDarkReaderConfigDialogueVisible(true); 70 | } 71 | }, 72 | { 73 | title: localisedStrings['settings-screen-size-suggestions-title'], 74 | description: sizeSuggestion.toString(), 75 | onPress: () => { 76 | setSizeSuggestionDialogueVisible(true); 77 | } 78 | }, 79 | { 80 | title: localisedStrings['settings-screen-size-history-title'], 81 | description: sizeHistory.toString(), 82 | onPress: () => { 83 | setSizeHistoryDialogueVisible(true); 84 | } 85 | }, 86 | { 87 | title: localisedStrings['clear-history-dialogue-title'], 88 | description: '', 89 | onPress: () => { 90 | setClearHistoryDialogueVisible(true); 91 | } 92 | }, 93 | { 94 | title: localisedStrings['confirm-recreate-ngram-dialogue-title'], 95 | description: localisedStrings['settings-screen-recreate-ngram-description'], 96 | onPress: () => { 97 | setRecreateNgramDialogueVisible(true); 98 | } 99 | } 100 | ]} 101 | renderItem={({ item }) => ( 102 | 106 | )} 107 | keyExtractor={(item, index) => index.toString()} /> 108 | 112 | 117 | 122 | 127 | 130 | 136 | 143 | 148 | 152 | 153 | ); 154 | } -------------------------------------------------------------------------------- /src/AppContext.js: -------------------------------------------------------------------------------- 1 | import React, { createContext, useContext, useEffect, useState } from 'react'; 2 | import { loadPersistentData, storePersistentData } from './utils'; 3 | import { DEFAULT_SERVER_ADDRESS, DEFAULT_FONT_FAMILY, DEFAULT_DARK_TEXT_COLOUR, DEFAULT_ADDITIONAL_GOOGLE_FONTS_ENABLED_STATUS, DEFAULT_DARK_READER_ENABLED, DEFAULT_DARK_READER_BRIGHTNESS, DEFAULT_DARK_READER_CONTRAST, DEFAULT_DARK_READER_SEPIA } from './config'; 4 | import { loadDataFromJsonResponse, convertDictionarySnakeCaseToCamelCase } from './utils'; 5 | import { localisedStrings } from './translations/l10n'; 6 | 7 | const AppContext = createContext(); 8 | 9 | export function AppProvider({ children }) { 10 | const [serverAddress, setServerAddress] = useState(''); 11 | 12 | const [dictionaries, setDictionaries] = useState([]); 13 | const [groups, setGroups] = useState([]); 14 | const [groupings, setGroupings] = useState({}); 15 | 16 | const [history, setHistory] = useState([]); 17 | 18 | const [fontFamily, setFontFamily] = useState(''); 19 | const [darkTextColour, setDarkTextColour] = useState(''); 20 | const [scriptsWithAdditionalFonts, setScriptsWithAdditionalFonts] = useState({}); 21 | 22 | const [darkReaderEnabled, setDarkReaderEnabled] = useState(''); 23 | const [darkReaderBrightness, setDarkReaderBrightness] = useState(''); 24 | const [darkReaderContrast, setDarkReaderContrast] = useState(''); 25 | const [darkReaderSepia, setDarkReaderSepia] = useState(''); 26 | 27 | const [sizeSuggestion, setSizeSuggestion] = useState(10); 28 | const [sizeHistory, setSizeHistory] = useState(100); 29 | 30 | const [formats, setFormats] = useState([]); 31 | const [sources, setSources] = useState([]); 32 | 33 | async function loadStoredData() { 34 | const storedServerAddress = await loadPersistentData('serverAddress', DEFAULT_SERVER_ADDRESS); 35 | setServerAddress(storedServerAddress); 36 | const storedFontFamily = await loadPersistentData('fontFamily', DEFAULT_FONT_FAMILY); 37 | setFontFamily(storedFontFamily); 38 | const storedDarkTextColour = await loadPersistentData('darkTextColour', DEFAULT_DARK_TEXT_COLOUR); 39 | setDarkTextColour(storedDarkTextColour); 40 | const storedScriptsWithAdditionalFonts = await loadPersistentData('scriptsWithAdditionalFonts', JSON.stringify(DEFAULT_ADDITIONAL_GOOGLE_FONTS_ENABLED_STATUS)); 41 | setScriptsWithAdditionalFonts(JSON.parse(storedScriptsWithAdditionalFonts)); 42 | const storedDarkReaderEnabled = await loadPersistentData('darkReaderEnabled', JSON.stringify(DEFAULT_DARK_READER_ENABLED)); 43 | setDarkReaderEnabled(JSON.parse(storedDarkReaderEnabled)); 44 | const storedDarkReaderBrightness = await loadPersistentData('darkReaderBrightness', DEFAULT_DARK_READER_BRIGHTNESS); 45 | setDarkReaderBrightness(parseInt(storedDarkReaderBrightness)); 46 | const storedDarkReaderContrast = await loadPersistentData('darkReaderContrast', DEFAULT_DARK_READER_CONTRAST); 47 | setDarkReaderContrast(parseInt(storedDarkReaderContrast)); 48 | const storedDarkReaderSepia = await loadPersistentData('darkReaderSepia', DEFAULT_DARK_READER_SEPIA); 49 | setDarkReaderSepia(parseInt(storedDarkReaderSepia)); 50 | } 51 | 52 | async function storeServerAddress() { 53 | await storePersistentData('serverAddress', serverAddress.toString()); 54 | } 55 | 56 | async function storeFontFamily() { 57 | await storePersistentData('fontFamily', fontFamily.toString()); 58 | } 59 | 60 | async function storeDarkTextColour() { 61 | await storePersistentData('darkTextColour', darkTextColour.toString()); 62 | } 63 | 64 | async function storeScriptsWithAdditionalFonts() { 65 | await storePersistentData('scriptsWithAdditionalFonts', JSON.stringify(scriptsWithAdditionalFonts)); 66 | } 67 | 68 | async function storeDarkReaderEnabled() { 69 | await storePersistentData('darkReaderEnabled', JSON.stringify(darkReaderEnabled)); 70 | } 71 | 72 | async function storeDarkReaderBrightness() { 73 | await storePersistentData('darkReaderBrightness', darkReaderBrightness.toString()); 74 | } 75 | 76 | async function storeDarkReaderContrast() { 77 | await storePersistentData('darkReaderContrast', darkReaderContrast.toString()); 78 | } 79 | 80 | async function storeDarkReaderSepia() { 81 | await storePersistentData('darkReaderSepia', darkReaderSepia.toString()); 82 | } 83 | 84 | useEffect(function () { loadStoredData(); }, []); 85 | useEffect(function () { storeServerAddress(); }, [serverAddress]); 86 | useEffect(function () { storeFontFamily(); }, [fontFamily]); 87 | useEffect(function () { storeDarkTextColour(); }, [darkTextColour]); 88 | useEffect(function () { storeScriptsWithAdditionalFonts(); }, [scriptsWithAdditionalFonts]); 89 | useEffect(function () { storeDarkReaderEnabled(); }, [darkReaderEnabled]); 90 | useEffect(function () { storeDarkReaderBrightness(); }, [darkReaderBrightness]); 91 | useEffect(function () { storeDarkReaderContrast(); }, [darkReaderContrast]); 92 | useEffect(function () { storeDarkReaderSepia(); }, [darkReaderSepia]); 93 | 94 | async function fetchInitialData( 95 | apiPrefix, 96 | setDictionaries, 97 | setGroups, 98 | setGroupings, 99 | setHistory, 100 | setSizeHistory, 101 | setSizeSuggestion 102 | ) { 103 | try { 104 | const [ 105 | dictionariesData, 106 | groupsData, 107 | groupingsData, 108 | historyData, 109 | sizeHistoryData, 110 | sizeSuggestionData, 111 | formatsData, 112 | sourcesData 113 | ] = await Promise.all([ 114 | fetch(`${apiPrefix}/management/dictionaries`).then(loadDataFromJsonResponse), 115 | fetch(`${apiPrefix}/management/groups`).then(loadDataFromJsonResponse), 116 | fetch(`${apiPrefix}/management/dictionary_groupings`).then(loadDataFromJsonResponse), 117 | fetch(`${apiPrefix}/management/history`).then(loadDataFromJsonResponse), 118 | fetch(`${apiPrefix}/management/history_size`).then(loadDataFromJsonResponse), 119 | fetch(`${apiPrefix}/management/num_suggestions`).then(loadDataFromJsonResponse), 120 | fetch(`${apiPrefix}/management/formats`).then(loadDataFromJsonResponse), 121 | fetch(`${apiPrefix}/management/sources`).then(loadDataFromJsonResponse) 122 | ]); 123 | 124 | setDictionaries(dictionariesData.map(convertDictionarySnakeCaseToCamelCase)); 125 | setGroups(groupsData); 126 | setGroupings(groupingsData); 127 | setHistory(historyData); 128 | setSizeHistory(sizeHistoryData['size']); 129 | setSizeSuggestion(sizeSuggestionData['size']); 130 | setFormats(formatsData); 131 | setSources(sourcesData); 132 | } catch (error) { 133 | alert(localisedStrings['app-context-message-failure-fetching-data']); 134 | } 135 | } 136 | 137 | useEffect(function () { 138 | if (serverAddress.length > 0) 139 | fetchInitialData( 140 | `${serverAddress}/api`, 141 | setDictionaries, 142 | setGroups, 143 | setGroupings, 144 | setHistory, 145 | setSizeHistory, 146 | setSizeSuggestion 147 | ); 148 | }, [serverAddress]); 149 | 150 | return ( 151 | 187 | {children} 188 | 189 | ); 190 | } 191 | 192 | export function useAppContext() { 193 | return useContext(AppContext); 194 | } 195 | -------------------------------------------------------------------------------- /src/translations/zh-TW.json: -------------------------------------------------------------------------------- 1 | { 2 | "additional-fonts-dialogue-content": "Download additional fonts from Google Fonts.\nSome fonts are rather large, so please only use when you see little boxes or question marks in the text.", 3 | "additional-fonts-dialogue-title": "附加字型", 4 | "add-dictionary-dialogue-alert-duplicate": "A dictionary with the same name already exists.", 5 | "add-dictionary-dialogue-alert-empty-filename": "Filename cannot be empty.", 6 | "add-dictionary-dialogue-alert-empty-name": "Dictionary name cannot be empty.", 7 | "add-dictionary-dialogue-alert-failure-adding": "Failed to add dictionary.", 8 | "add-dictionary-dialogue-alert-failure-validating": "Failed to validate dictionary.", 9 | "add-dictionary-dialogue-alert-invalid": "Invalid dictionary info.", 10 | "add-dictionary-dialogue-title": "Add dictionary", 11 | "add-dictionary-dialogue-content-group": "Group", 12 | "add-dictionary-dialogue-content-filename": "Filename", 13 | "add-dictionary-dialogue-content-format": "Format", 14 | "add-dictionary-dialogue-content-name": "Dictionary name", 15 | "app-context-message-failure-fetching-data": "從伺服器獲取資料失敗。請檢查網路連線,或更改伺服器地址並重試。", 16 | "article-view-tap-to-search-dialogue-message": "搜尋 “{0}”?", 17 | "change-address-dialogue-message-unable-to-connect": "無法連線到伺服器。", 18 | "change-address-dialogue-title": "變更伺服器地址", 19 | "change-colour-dialogue-content": "A value such as ‘white’ or ‘grey’ is recommended.\nThis setting only takes effect when the Dark Reader extension is disabled.", 20 | "change-colour-dialogue-title": "變更暗色模式下字型顏色", 21 | "change-font-dialogue-font-sans-serif": "sans-serif", 22 | "change-font-dialogue-font-serif": "serif", 23 | "change-font-dialogue-title": "變更字型", 24 | "change-size-history-dialogue-message-failure": "Failed to update history size.", 25 | "change-size-history-dialogue-placeholder": "a non-negative integer", 26 | "change-size-history-dialogue-title": "Change the size of history", 27 | "change-size-suggestion-dialogue-message-failure": "Failed to update the number of suggestions.", 28 | "change-size-suggestion-dialogue-placeholder": "a positive integer", 29 | "change-size-suggestion-dialogue-title": "Change the number of suggestions", 30 | "clear-history-dialogue-content": "Are you sure you want to clear history?", 31 | "clear-history-dialogue-message-failure": "Failed to clear history.", 32 | "clear-history-dialogue-title": "Clear history", 33 | "confirm-recreate-ngram-dialogue-content": "Are you sure you want to recreate the ngram index?", 34 | "confirm-recreate-ngram-dialogue-message-failure": "Failed to recreate ngram index.", 35 | "confirm-recreate-ngram-dialogue-message-in-progress": "Recreating ngram index…", 36 | "confirm-recreate-ngram-dialogue-message-success": "Recreated ngram index.", 37 | "confirm-recreate-ngram-dialogue-title": "Recreate ngram index", 38 | "dark-reader-config-brightness": "Brightness", 39 | "dark-reader-config-contrast": "Contrast", 40 | "dark-reader-config-sepia": "Sepia", 41 | "dark-reader-config-enabled": "Enabled", 42 | "dark-reader-config-performance-warning": "Please note, this extension may cause articles to load a bit slower than usual.\n", 43 | "dark-reader-config-title": "Dark Reader extension preferences", 44 | "dictionary-manager-alert-edit-name-failure": "Failed to update dictionary name.", 45 | "dictionary-manager-alert-delete-dictionary-failure": "Failed to delete dictionary.", 46 | "dictionary-manager-alert-failure-reordering": "Failed to reorder dictionaries.", 47 | "dictionary-manager-dialogue-edit-name-title": "Edit dictionary name", 48 | "dictionary-manager-dialogue-delete-dictionary-content": "Are you sure you want to delete ‘{0}’ from the database? The dictionary files will not be deleted.", 49 | "dictionary-manager-dialogue-delete-dictionary-title": "Delete dictionary", 50 | "dictionary-manager-dialogue-name-placeholder": "new dictionary name", 51 | "drawer-library-label": "Library", 52 | "drawer-query-label": "查詢", 53 | "drawer-reconnect-label": "Reconnect", 54 | "drawer-settings-label": "設定", 55 | "generic-alert-invalid-number": "\"無效數字。", 56 | "generic-cancel": "取消", 57 | "generic-delete": "Delete", 58 | "generic-edit": "編輯", 59 | "generic-no": "否", 60 | "generic-ok": "好", 61 | "generic-rename": "Rename", 62 | "generic-yes": "是", 63 | "group-manager-alert-empty-name": "Group name cannot be empty.", 64 | "group-manager-alert-failure-adding-dictionary": "Failed to add dictionary to group.", 65 | "group-manager-alert-failure-adding-group": "Failed to add group.", 66 | "group-manager-alert-failure-changing-languages": "Failed to change the group's languages.", 67 | "group-manager-alert-failure-removing-dictionary": "Failed to remove dictionary from group.", 68 | "group-manager-alert-failure-removing-group": "Failed to remove group.", 69 | "group-manager-alert-failure-renaming-group": "Failed to rename group.", 70 | "group-manager-alert-failure-reordering-groups": "Failed to reorder groups.", 71 | "group-manager-alert-invalid-language-code": "Invalid language code(s) found.", 72 | "group-manager-alert-name-exists": "A group with the same name already exists.", 73 | "group-manager-card-title-dictionaries": "Dictionaries", 74 | "group-manager-card-title-languages": "Languages", 75 | "group-manager-dialogue-add-title": "Add group", 76 | "group-manager-dialogue-add-content": "Group name", 77 | "group-manager-dialogue-change-languages-content": "A comma separated list of two-letter ISO-639-1 language codes. It cannot be empty.", 78 | "group-manager-dialogue-change-languages-title": "Change the group's languages", 79 | "group-manager-dialogue-delete-content": "Are you sure you want to delete the group ‘{0}’?", 80 | "group-manager-dialogue-delete-title": "Delete group", 81 | "group-manager-dialogue-rename-title": "Rename group", 82 | "library-screen-groups": "Groups", 83 | "library-screen-sources": "Sources", 84 | "placeholder-search": "搜尋……", 85 | "query-screen-failure-fetch-articles": "Failed to fetch articles.", 86 | "query-screen-failure-fetch-suggestions": "Failed to fetch suggestions.", 87 | "script-name-arabic": "阿拉伯字母", 88 | "script-name-bengali": "孟加拉文", 89 | "script-name-chinese-simplified": "簡體中文", 90 | "script-name-chinese-traditional": "正體中文", 91 | "script-name-cyrillic": "西里爾字母", 92 | "script-name-devanagari": "Devanagari", 93 | "script-name-greek": "Greek", 94 | "script-name-gujarati": "Gujarati", 95 | "script-name-gurmukhi": "Gurmukhi", 96 | "script-name-hebrew": "Hebrew", 97 | "script-name-japanese": "Japanese", 98 | "script-name-kannada": "Kannada", 99 | "script-name-khmer": "Khmer", 100 | "script-name-korean": "Korean", 101 | "script-name-latin": "Latin", 102 | "script-name-malayalam": "Malayalam", 103 | "script-name-myanmar": "Burmese", 104 | "script-name-oriya": "Oriya", 105 | "script-name-sinhala": "Sinhalese", 106 | "script-name-tamil": "Tamil", 107 | "script-name-telugu": "Telugu", 108 | "script-name-thai": "Thai", 109 | "script-name-tibetan": "Tibetan", 110 | "script-name-vietnamese": "Vietnamese", 111 | "settings-screen-dark-text-colour-title": "Colour of the text in dark mode", 112 | "settings-screen-font-family-title": "Font", 113 | "settings-screen-recreate-ngram-description": "可能會很慢", 114 | "settings-screen-server-address-title": "Server address", 115 | "settings-screen-size-history-title": "History size", 116 | "settings-screen-size-suggestions-title": "Suggestion size", 117 | "source-manager-alert-duplicate": "The source already exists.", 118 | "source-manager-alert-empty-source": "Source cannot be empty.", 119 | "source-manager-alert-failure-adding-source": "Failed to add source.", 120 | "source-manager-alert-failure-deleting-source": "Failed to delete source.", 121 | "source-manager-alert-failure-fetching-sources": "Failed to fetch sources.", 122 | "source-manager-alert-failure-rescaning-sources": "Failed to rescan sources.", 123 | "source-manager-alert-invalid-source": "Invalid source.", 124 | "source-manager-dialogue-add-source-title": "Add source", 125 | "source-manager-dialogue-rescanning-sources-title": "Rescanning sources", 126 | "source-manager-confirm-delete-title": "Delete source", 127 | "source-manager-confirm-delete-content": "Are you sure you want to delete the source {0} ?" 128 | } -------------------------------------------------------------------------------- /src/translations/ar-SA.json: -------------------------------------------------------------------------------- 1 | { 2 | "additional-fonts-dialogue-content": "Download additional fonts from Google Fonts.\nSome fonts are rather large, so please only use when you see little boxes or question marks in the text.", 3 | "additional-fonts-dialogue-title": "Additional fonts", 4 | "add-dictionary-dialogue-alert-duplicate": "يوجد بالفعل قاموس بنفس الاسم.", 5 | "add-dictionary-dialogue-alert-empty-filename": "Filename cannot be empty.", 6 | "add-dictionary-dialogue-alert-empty-name": "Dictionary name cannot be empty.", 7 | "add-dictionary-dialogue-alert-failure-adding": "Failed to add dictionary.", 8 | "add-dictionary-dialogue-alert-failure-validating": "Failed to validate dictionary.", 9 | "add-dictionary-dialogue-alert-invalid": "Invalid dictionary info.", 10 | "add-dictionary-dialogue-title": "Add dictionary", 11 | "add-dictionary-dialogue-content-group": "Group", 12 | "add-dictionary-dialogue-content-filename": "Filename", 13 | "add-dictionary-dialogue-content-format": "Format", 14 | "add-dictionary-dialogue-content-name": "Dictionary name", 15 | "app-context-message-failure-fetching-data": "Failed to fetch data from the server. Please check your connection or change the server address and try again.", 16 | "article-view-tap-to-search-dialogue-message": "Search for ‘{0}’?", 17 | "change-address-dialogue-message-unable-to-connect": "Unable to connect to server.", 18 | "change-address-dialogue-title": "Change server address", 19 | "change-colour-dialogue-content": "A value such as ‘white’ or ‘grey’ is recommended.\nThis setting only takes effect when the Dark Reader extension is disabled.", 20 | "change-colour-dialogue-title": "Change the colour of the text in the dark mode", 21 | "change-font-dialogue-font-sans-serif": "sans-serif", 22 | "change-font-dialogue-font-serif": "serif", 23 | "change-font-dialogue-title": "Change font", 24 | "change-size-history-dialogue-message-failure": "Failed to update history size.", 25 | "change-size-history-dialogue-placeholder": "a non-negative integer", 26 | "change-size-history-dialogue-title": "Change the size of history", 27 | "change-size-suggestion-dialogue-message-failure": "Failed to update the number of suggestions.", 28 | "change-size-suggestion-dialogue-placeholder": "a positive integer", 29 | "change-size-suggestion-dialogue-title": "Change the number of suggestions", 30 | "clear-history-dialogue-content": "Are you sure you want to clear history?", 31 | "clear-history-dialogue-message-failure": "Failed to clear history.", 32 | "clear-history-dialogue-title": "Clear history", 33 | "confirm-recreate-ngram-dialogue-content": "Are you sure you want to recreate the ngram index?", 34 | "confirm-recreate-ngram-dialogue-message-failure": "Failed to recreate ngram index.", 35 | "confirm-recreate-ngram-dialogue-message-in-progress": "Recreating ngram index…", 36 | "confirm-recreate-ngram-dialogue-message-success": "Recreated ngram index.", 37 | "confirm-recreate-ngram-dialogue-title": "Recreate ngram index", 38 | "dark-reader-config-brightness": "Brightness", 39 | "dark-reader-config-contrast": "Contrast", 40 | "dark-reader-config-sepia": "Sepia", 41 | "dark-reader-config-enabled": "Enabled", 42 | "dark-reader-config-performance-warning": "Please note, this extension may cause articles to load a bit slower than usual.\n", 43 | "dark-reader-config-title": "Dark Reader extension preferences", 44 | "dictionary-manager-alert-edit-name-failure": "Failed to update dictionary name.", 45 | "dictionary-manager-alert-delete-dictionary-failure": "Failed to delete dictionary.", 46 | "dictionary-manager-alert-failure-reordering": "Failed to reorder dictionaries.", 47 | "dictionary-manager-dialogue-edit-name-title": "Edit dictionary name", 48 | "dictionary-manager-dialogue-delete-dictionary-content": "Are you sure you want to delete ‘{0}’ from the database? The dictionary files will not be deleted.", 49 | "dictionary-manager-dialogue-delete-dictionary-title": "Delete dictionary", 50 | "dictionary-manager-dialogue-name-placeholder": "new dictionary name", 51 | "drawer-library-label": "Library", 52 | "drawer-query-label": "Query", 53 | "drawer-reconnect-label": "Reconnect", 54 | "drawer-settings-label": "Settings", 55 | "generic-alert-invalid-number": "Invalid number.", 56 | "generic-cancel": "Cancel", 57 | "generic-delete": "Delete", 58 | "generic-edit": "Edit", 59 | "generic-no": "No", 60 | "generic-ok": "OK", 61 | "generic-rename": "Rename", 62 | "generic-yes": "Yes", 63 | "group-manager-alert-empty-name": "Group name cannot be empty.", 64 | "group-manager-alert-failure-adding-dictionary": "Failed to add dictionary to group.", 65 | "group-manager-alert-failure-adding-group": "Failed to add group.", 66 | "group-manager-alert-failure-changing-languages": "Failed to change the group's languages.", 67 | "group-manager-alert-failure-removing-dictionary": "Failed to remove dictionary from group.", 68 | "group-manager-alert-failure-removing-group": "Failed to remove group.", 69 | "group-manager-alert-failure-renaming-group": "Failed to rename group.", 70 | "group-manager-alert-failure-reordering-groups": "Failed to reorder groups.", 71 | "group-manager-alert-invalid-language-code": "Invalid language code(s) found.", 72 | "group-manager-alert-name-exists": "A group with the same name already exists.", 73 | "group-manager-card-title-dictionaries": "Dictionaries", 74 | "group-manager-card-title-languages": "Languages", 75 | "group-manager-dialogue-add-title": "Add group", 76 | "group-manager-dialogue-add-content": "Group name", 77 | "group-manager-dialogue-change-languages-content": "A comma separated list of two-letter ISO-639-1 language codes. It cannot be empty.", 78 | "group-manager-dialogue-change-languages-title": "Change the group's languages", 79 | "group-manager-dialogue-delete-content": "Are you sure you want to delete the group ‘{0}’?", 80 | "group-manager-dialogue-delete-title": "Delete group", 81 | "group-manager-dialogue-rename-title": "Rename group", 82 | "library-screen-groups": "Groups", 83 | "library-screen-sources": "Sources", 84 | "placeholder-search": "Search…", 85 | "query-screen-failure-fetch-articles": "Failed to fetch articles.", 86 | "query-screen-failure-fetch-suggestions": "Failed to fetch suggestions.", 87 | "script-name-arabic": "Arabic", 88 | "script-name-bengali": "Bengali", 89 | "script-name-chinese-simplified": "Chinese, Simplified", 90 | "script-name-chinese-traditional": "Chinese, Traditional", 91 | "script-name-cyrillic": "Cyrillic", 92 | "script-name-devanagari": "Devanagari", 93 | "script-name-greek": "Greek", 94 | "script-name-gujarati": "Gujarati", 95 | "script-name-gurmukhi": "Gurmukhi", 96 | "script-name-hebrew": "Hebrew", 97 | "script-name-japanese": "Japanese", 98 | "script-name-kannada": "Kannada", 99 | "script-name-khmer": "Khmer", 100 | "script-name-korean": "Korean", 101 | "script-name-latin": "Latin", 102 | "script-name-malayalam": "Malayalam", 103 | "script-name-myanmar": "Burmese", 104 | "script-name-oriya": "Oriya", 105 | "script-name-sinhala": "Sinhalese", 106 | "script-name-tamil": "Tamil", 107 | "script-name-telugu": "Telugu", 108 | "script-name-thai": "Thai", 109 | "script-name-tibetan": "Tibetan", 110 | "script-name-vietnamese": "Vietnamese", 111 | "settings-screen-dark-text-colour-title": "Colour of the text in dark mode", 112 | "settings-screen-font-family-title": "Font", 113 | "settings-screen-recreate-ngram-description": "It could be very slow.", 114 | "settings-screen-server-address-title": "Server address", 115 | "settings-screen-size-history-title": "History size", 116 | "settings-screen-size-suggestions-title": "Suggestion size", 117 | "source-manager-alert-duplicate": "The source already exists.", 118 | "source-manager-alert-empty-source": "Source cannot be empty.", 119 | "source-manager-alert-failure-adding-source": "Failed to add source.", 120 | "source-manager-alert-failure-deleting-source": "Failed to delete source.", 121 | "source-manager-alert-failure-fetching-sources": "Failed to fetch sources.", 122 | "source-manager-alert-failure-rescaning-sources": "Failed to rescan sources.", 123 | "source-manager-alert-invalid-source": "Invalid source.", 124 | "source-manager-dialogue-add-source-title": "Add source", 125 | "source-manager-dialogue-rescanning-sources-title": "Rescanning sources", 126 | "source-manager-confirm-delete-title": "Delete source", 127 | "source-manager-confirm-delete-content": "Are you sure you want to delete the source {0} ?" 128 | } -------------------------------------------------------------------------------- /src/translations/en-US.json: -------------------------------------------------------------------------------- 1 | { 2 | "additional-fonts-dialogue-content": "Download additional fonts from Google Fonts.\nSome fonts are rather large, so please only use when you see little boxes or question marks in the text.", 3 | "additional-fonts-dialogue-title": "Additional fonts", 4 | "add-dictionary-dialogue-alert-duplicate": "A dictionary with the same name already exists.", 5 | "add-dictionary-dialogue-alert-empty-filename": "Filename cannot be empty.", 6 | "add-dictionary-dialogue-alert-empty-name": "Dictionary name cannot be empty.", 7 | "add-dictionary-dialogue-alert-failure-adding": "Failed to add dictionary.", 8 | "add-dictionary-dialogue-alert-failure-validating": "Failed to validate dictionary.", 9 | "add-dictionary-dialogue-alert-invalid": "Invalid dictionary info.", 10 | "add-dictionary-dialogue-title": "Add dictionary", 11 | "add-dictionary-dialogue-content-group": "Group", 12 | "add-dictionary-dialogue-content-filename": "Filename", 13 | "add-dictionary-dialogue-content-format": "Format", 14 | "add-dictionary-dialogue-content-name": "Dictionary name", 15 | "app-context-message-failure-fetching-data": "Failed to fetch data from the server. Please check your connection or change the server address and try again.", 16 | "article-view-tap-to-search-dialogue-message": "Search for “{0}”?", 17 | "change-address-dialogue-message-unable-to-connect": "Unable to connect to server.", 18 | "change-address-dialogue-title": "Change server address", 19 | "change-colour-dialogue-content": "A value such as “white” or “gray” is recommended.\nThis setting only takes effect when the Dark Reader extension is disabled.", 20 | "change-colour-dialogue-title": "Change the color of the text in the dark mode", 21 | "change-font-dialogue-font-sans-serif": "sans-serif", 22 | "change-font-dialogue-font-serif": "serif", 23 | "change-font-dialogue-title": "Change font", 24 | "change-size-history-dialogue-message-failure": "Failed to update history size.", 25 | "change-size-history-dialogue-placeholder": "a non-negative integer", 26 | "change-size-history-dialogue-title": "Change the size of history", 27 | "change-size-suggestion-dialogue-message-failure": "Failed to update the number of suggestions.", 28 | "change-size-suggestion-dialogue-placeholder": "a positive integer", 29 | "change-size-suggestion-dialogue-title": "Change the number of suggestions", 30 | "clear-history-dialogue-content": "Are you sure you want to clear history?", 31 | "clear-history-dialogue-message-failure": "Failed to clear history.", 32 | "clear-history-dialogue-title": "Clear history", 33 | "confirm-recreate-ngram-dialogue-content": "Are you sure you want to recreate the ngram index?", 34 | "confirm-recreate-ngram-dialogue-message-failure": "Failed to recreate ngram index.", 35 | "confirm-recreate-ngram-dialogue-message-in-progress": "Recreating ngram index…", 36 | "confirm-recreate-ngram-dialogue-message-success": "Recreated ngram index.", 37 | "confirm-recreate-ngram-dialogue-title": "Recreate ngram index", 38 | "dark-reader-config-brightness": "Brightness", 39 | "dark-reader-config-contrast": "Contrast", 40 | "dark-reader-config-sepia": "Sepia", 41 | "dark-reader-config-enabled": "Enabled", 42 | "dark-reader-config-performance-warning": "Please note, this extension may cause articles to load a bit slower than usual.\n", 43 | "dark-reader-config-title": "Dark Reader extension preferences", 44 | "dictionary-manager-alert-edit-name-failure": "Failed to update dictionary name.", 45 | "dictionary-manager-alert-delete-dictionary-failure": "Failed to delete dictionary.", 46 | "dictionary-manager-alert-failure-reordering": "Failed to reorder dictionaries.", 47 | "dictionary-manager-dialogue-edit-name-title": "Edit dictionary name", 48 | "dictionary-manager-dialogue-delete-dictionary-content": "Are you sure you want to delete “{0}” from the database? The dictionary files will not be deleted.", 49 | "dictionary-manager-dialogue-delete-dictionary-title": "Delete dictionary", 50 | "dictionary-manager-dialogue-name-placeholder": "new dictionary name", 51 | "drawer-library-label": "Library", 52 | "drawer-query-label": "Query", 53 | "drawer-reconnect-label": "Reconnect", 54 | "drawer-settings-label": "Settings", 55 | "generic-alert-invalid-number": "Invalid number.", 56 | "generic-cancel": "Cancel", 57 | "generic-delete": "Delete", 58 | "generic-edit": "Edit", 59 | "generic-no": "No", 60 | "generic-ok": "OK", 61 | "generic-rename": "Rename", 62 | "generic-yes": "Yes", 63 | "group-manager-alert-empty-name": "Group name cannot be empty.", 64 | "group-manager-alert-failure-adding-dictionary": "Failed to add dictionary to group.", 65 | "group-manager-alert-failure-adding-group": "Failed to add group.", 66 | "group-manager-alert-failure-changing-languages": "Failed to change the group's languages.", 67 | "group-manager-alert-failure-removing-dictionary": "Failed to remove dictionary from group.", 68 | "group-manager-alert-failure-removing-group": "Failed to remove group.", 69 | "group-manager-alert-failure-renaming-group": "Failed to rename group.", 70 | "group-manager-alert-failure-reordering-groups": "Failed to reorder groups.", 71 | "group-manager-alert-invalid-language-code": "Invalid language code(s) found.", 72 | "group-manager-alert-name-exists": "A group with the same name already exists.", 73 | "group-manager-card-title-dictionaries": "Dictionaries", 74 | "group-manager-card-title-languages": "Languages", 75 | "group-manager-dialogue-add-title": "Add group", 76 | "group-manager-dialogue-add-content": "Group name", 77 | "group-manager-dialogue-change-languages-content": "A comma separated list of two-letter ISO-639-1 language codes. It cannot be empty.", 78 | "group-manager-dialogue-change-languages-title": "Change the group's languages", 79 | "group-manager-dialogue-delete-content": "Are you sure you want to delete the group “{0}“?", 80 | "group-manager-dialogue-delete-title": "Delete group", 81 | "group-manager-dialogue-rename-title": "Rename group", 82 | "library-screen-groups": "Groups", 83 | "library-screen-sources": "Sources", 84 | "placeholder-search": "Search…", 85 | "query-screen-failure-fetch-articles": "Failed to fetch articles.", 86 | "query-screen-failure-fetch-suggestions": "Failed to fetch suggestions.", 87 | "script-name-arabic": "Arabic", 88 | "script-name-bengali": "Bengali", 89 | "script-name-chinese-simplified": "Chinese, Simplified", 90 | "script-name-chinese-traditional": "Chinese, Traditional", 91 | "script-name-cyrillic": "Cyrillic", 92 | "script-name-devanagari": "Devanagari", 93 | "script-name-greek": "Greek", 94 | "script-name-gujarati": "Gujarati", 95 | "script-name-gurmukhi": "Gurmukhi", 96 | "script-name-hebrew": "Hebrew", 97 | "script-name-japanese": "Japanese", 98 | "script-name-kannada": "Kannada", 99 | "script-name-khmer": "Khmer", 100 | "script-name-korean": "Korean", 101 | "script-name-latin": "Latin", 102 | "script-name-malayalam": "Malayalam", 103 | "script-name-myanmar": "Burmese", 104 | "script-name-oriya": "Oriya", 105 | "script-name-sinhala": "Sinhalese", 106 | "script-name-tamil": "Tamil", 107 | "script-name-telugu": "Telugu", 108 | "script-name-thai": "Thai", 109 | "script-name-tibetan": "Tibetan", 110 | "script-name-vietnamese": "Vietnamese", 111 | "settings-screen-dark-text-colour-title": "Color of the text in dark mode", 112 | "settings-screen-font-family-title": "Font", 113 | "settings-screen-recreate-ngram-description": "It could be very slow.", 114 | "settings-screen-server-address-title": "Server address", 115 | "settings-screen-size-history-title": "History size", 116 | "settings-screen-size-suggestions-title": "Suggestion size", 117 | "source-manager-alert-duplicate": "The source already exists.", 118 | "source-manager-alert-empty-source": "Source cannot be empty.", 119 | "source-manager-alert-failure-adding-source": "Failed to add source.", 120 | "source-manager-alert-failure-deleting-source": "Failed to delete source.", 121 | "source-manager-alert-failure-fetching-sources": "Failed to fetch sources.", 122 | "source-manager-alert-failure-rescaning-sources": "Failed to rescan sources.", 123 | "source-manager-alert-invalid-source": "Invalid source.", 124 | "source-manager-dialogue-add-source-title": "Add source", 125 | "source-manager-dialogue-rescanning-sources-title": "Rescanning sources", 126 | "source-manager-confirm-delete-title": "Delete source", 127 | "source-manager-confirm-delete-content": "Are you sure you want to delete the source {0} ?" 128 | } -------------------------------------------------------------------------------- /src/translations/es-ES.json: -------------------------------------------------------------------------------- 1 | { 2 | "additional-fonts-dialogue-content": "Download additional fonts from Google Fonts.\nSome fonts are rather large, so please only use when you see little boxes or question marks in the text.", 3 | "additional-fonts-dialogue-title": "Additional fonts", 4 | "add-dictionary-dialogue-alert-duplicate": "A dictionary with the same name already exists.", 5 | "add-dictionary-dialogue-alert-empty-filename": "Filename cannot be empty.", 6 | "add-dictionary-dialogue-alert-empty-name": "Dictionary name cannot be empty.", 7 | "add-dictionary-dialogue-alert-failure-adding": "Failed to add dictionary.", 8 | "add-dictionary-dialogue-alert-failure-validating": "Failed to validate dictionary.", 9 | "add-dictionary-dialogue-alert-invalid": "Invalid dictionary info.", 10 | "add-dictionary-dialogue-title": "Add dictionary", 11 | "add-dictionary-dialogue-content-group": "Grupo", 12 | "add-dictionary-dialogue-content-filename": "Filename", 13 | "add-dictionary-dialogue-content-format": "Format", 14 | "add-dictionary-dialogue-content-name": "Dictionary name", 15 | "app-context-message-failure-fetching-data": "Failed to fetch data from the server. Please check your connection or change the server address and try again.", 16 | "article-view-tap-to-search-dialogue-message": "¿Buscar ‘{0}’?", 17 | "change-address-dialogue-message-unable-to-connect": "Unable to connect to server.", 18 | "change-address-dialogue-title": "Change server address", 19 | "change-colour-dialogue-content": "A value such as ‘white’ or ‘grey’ is recommended.\nThis setting only takes effect when the Dark Reader extension is disabled.", 20 | "change-colour-dialogue-title": "Change the colour of the text in the dark mode", 21 | "change-font-dialogue-font-sans-serif": "sans-serif", 22 | "change-font-dialogue-font-serif": "serif", 23 | "change-font-dialogue-title": "Change font", 24 | "change-size-history-dialogue-message-failure": "Failed to update history size.", 25 | "change-size-history-dialogue-placeholder": "a non-negative integer", 26 | "change-size-history-dialogue-title": "Change the size of history", 27 | "change-size-suggestion-dialogue-message-failure": "Failed to update the number of suggestions.", 28 | "change-size-suggestion-dialogue-placeholder": "a positive integer", 29 | "change-size-suggestion-dialogue-title": "Change the number of suggestions", 30 | "clear-history-dialogue-content": "Are you sure you want to clear history?", 31 | "clear-history-dialogue-message-failure": "Failed to clear history.", 32 | "clear-history-dialogue-title": "Clear history", 33 | "confirm-recreate-ngram-dialogue-content": "Are you sure you want to recreate the ngram index?", 34 | "confirm-recreate-ngram-dialogue-message-failure": "Failed to recreate ngram index.", 35 | "confirm-recreate-ngram-dialogue-message-in-progress": "Recreating ngram index…", 36 | "confirm-recreate-ngram-dialogue-message-success": "Recreated ngram index.", 37 | "confirm-recreate-ngram-dialogue-title": "Recreate ngram index", 38 | "dark-reader-config-brightness": "Brightness", 39 | "dark-reader-config-contrast": "Contrast", 40 | "dark-reader-config-sepia": "Sepia", 41 | "dark-reader-config-enabled": "Enabled", 42 | "dark-reader-config-performance-warning": "Please note, this extension may cause articles to load a bit slower than usual.\n", 43 | "dark-reader-config-title": "Dark Reader extension preferences", 44 | "dictionary-manager-alert-edit-name-failure": "Failed to update dictionary name.", 45 | "dictionary-manager-alert-delete-dictionary-failure": "Failed to delete dictionary.", 46 | "dictionary-manager-alert-failure-reordering": "Failed to reorder dictionaries.", 47 | "dictionary-manager-dialogue-edit-name-title": "Edit dictionary name", 48 | "dictionary-manager-dialogue-delete-dictionary-content": "Are you sure you want to delete ‘{0}’ from the database? The dictionary files will not be deleted.", 49 | "dictionary-manager-dialogue-delete-dictionary-title": "Delete dictionary", 50 | "dictionary-manager-dialogue-name-placeholder": "new dictionary name", 51 | "drawer-library-label": "Library", 52 | "drawer-query-label": "Query", 53 | "drawer-reconnect-label": "Reconnect", 54 | "drawer-settings-label": "Settings", 55 | "generic-alert-invalid-number": "Invalid number.", 56 | "generic-cancel": "Cancel", 57 | "generic-delete": "Delete", 58 | "generic-edit": "Edit", 59 | "generic-no": "No", 60 | "generic-ok": "OK", 61 | "generic-rename": "Rename", 62 | "generic-yes": "Sí", 63 | "group-manager-alert-empty-name": "Group name cannot be empty.", 64 | "group-manager-alert-failure-adding-dictionary": "Failed to add dictionary to group.", 65 | "group-manager-alert-failure-adding-group": "Failed to add group.", 66 | "group-manager-alert-failure-changing-languages": "Failed to change the group's languages.", 67 | "group-manager-alert-failure-removing-dictionary": "Failed to remove dictionary from group.", 68 | "group-manager-alert-failure-removing-group": "Failed to remove group.", 69 | "group-manager-alert-failure-renaming-group": "Failed to rename group.", 70 | "group-manager-alert-failure-reordering-groups": "Failed to reorder groups.", 71 | "group-manager-alert-invalid-language-code": "Invalid language code(s) found.", 72 | "group-manager-alert-name-exists": "A group with the same name already exists.", 73 | "group-manager-card-title-dictionaries": "Dictionaries", 74 | "group-manager-card-title-languages": "Languages", 75 | "group-manager-dialogue-add-title": "Add group", 76 | "group-manager-dialogue-add-content": "Group name", 77 | "group-manager-dialogue-change-languages-content": "A comma separated list of two-letter ISO-639-1 language codes. It cannot be empty.", 78 | "group-manager-dialogue-change-languages-title": "Change the group's languages", 79 | "group-manager-dialogue-delete-content": "Are you sure you want to delete the group ‘{0}’?", 80 | "group-manager-dialogue-delete-title": "Delete group", 81 | "group-manager-dialogue-rename-title": "Rename group", 82 | "library-screen-groups": "Groups", 83 | "library-screen-sources": "Sources", 84 | "placeholder-search": "Búsqueda…", 85 | "query-screen-failure-fetch-articles": "Failed to fetch articles.", 86 | "query-screen-failure-fetch-suggestions": "Failed to fetch suggestions.", 87 | "script-name-arabic": "Arabic", 88 | "script-name-bengali": "Bengali", 89 | "script-name-chinese-simplified": "Chinese, Simplified", 90 | "script-name-chinese-traditional": "Chinese, Traditional", 91 | "script-name-cyrillic": "Cyrillic", 92 | "script-name-devanagari": "Devanagari", 93 | "script-name-greek": "Greek", 94 | "script-name-gujarati": "Gujarati", 95 | "script-name-gurmukhi": "Gurmukhi", 96 | "script-name-hebrew": "Hebrew", 97 | "script-name-japanese": "Japanese", 98 | "script-name-kannada": "Kannada", 99 | "script-name-khmer": "Khmer", 100 | "script-name-korean": "Korean", 101 | "script-name-latin": "Latin", 102 | "script-name-malayalam": "Malayalam", 103 | "script-name-myanmar": "Burmese", 104 | "script-name-oriya": "Oriya", 105 | "script-name-sinhala": "Sinhalese", 106 | "script-name-tamil": "Tamil", 107 | "script-name-telugu": "Telugu", 108 | "script-name-thai": "Thai", 109 | "script-name-tibetan": "Tibetan", 110 | "script-name-vietnamese": "Vietnamese", 111 | "settings-screen-dark-text-colour-title": "Colour of the text in dark mode", 112 | "settings-screen-font-family-title": "Font", 113 | "settings-screen-recreate-ngram-description": "It could be very slow.", 114 | "settings-screen-server-address-title": "Server address", 115 | "settings-screen-size-history-title": "History size", 116 | "settings-screen-size-suggestions-title": "Suggestion size", 117 | "source-manager-alert-duplicate": "The source already exists.", 118 | "source-manager-alert-empty-source": "Source cannot be empty.", 119 | "source-manager-alert-failure-adding-source": "Failed to add source.", 120 | "source-manager-alert-failure-deleting-source": "Failed to delete source.", 121 | "source-manager-alert-failure-fetching-sources": "Failed to fetch sources.", 122 | "source-manager-alert-failure-rescaning-sources": "Failed to rescan sources.", 123 | "source-manager-alert-invalid-source": "Invalid source.", 124 | "source-manager-dialogue-add-source-title": "Add source", 125 | "source-manager-dialogue-rescanning-sources-title": "Rescanning sources", 126 | "source-manager-confirm-delete-title": "Delete source", 127 | "source-manager-confirm-delete-content": "Are you sure you want to delete the source {0} ?" 128 | } -------------------------------------------------------------------------------- /src/translations/de-DE.json: -------------------------------------------------------------------------------- 1 | { 2 | "additional-fonts-dialogue-content": "Download additional fonts from Google Fonts.\nSome fonts are rather large, so please only use when you see little boxes or question marks in the text.", 3 | "additional-fonts-dialogue-title": "Additional fonts", 4 | "add-dictionary-dialogue-alert-duplicate": "A dictionary with the same name already exists.", 5 | "add-dictionary-dialogue-alert-empty-filename": "Filename cannot be empty.", 6 | "add-dictionary-dialogue-alert-empty-name": "Dictionary name cannot be empty.", 7 | "add-dictionary-dialogue-alert-failure-adding": "Failed to add dictionary.", 8 | "add-dictionary-dialogue-alert-failure-validating": "Failed to validate dictionary.", 9 | "add-dictionary-dialogue-alert-invalid": "Invalid dictionary info.", 10 | "add-dictionary-dialogue-title": "Add dictionary", 11 | "add-dictionary-dialogue-content-group": "Group", 12 | "add-dictionary-dialogue-content-filename": "Filename", 13 | "add-dictionary-dialogue-content-format": "Format", 14 | "add-dictionary-dialogue-content-name": "Dictionary name", 15 | "app-context-message-failure-fetching-data": "Failed to fetch data from the server. Please check your connection or change the server address and try again.", 16 | "article-view-tap-to-search-dialogue-message": "Search for ‘{0}’?", 17 | "change-address-dialogue-message-unable-to-connect": "Unable to connect to server.", 18 | "change-address-dialogue-title": "Change server address", 19 | "change-colour-dialogue-content": "A value such as ‘white’ or ‘grey’ is recommended.\nThis setting only takes effect when the Dark Reader extension is disabled.", 20 | "change-colour-dialogue-title": "Change the colour of the text in the dark mode", 21 | "change-font-dialogue-font-sans-serif": "sans-serif", 22 | "change-font-dialogue-font-serif": "serif", 23 | "change-font-dialogue-title": "Change font", 24 | "change-size-history-dialogue-message-failure": "Failed to update history size.", 25 | "change-size-history-dialogue-placeholder": "a non-negative integer", 26 | "change-size-history-dialogue-title": "Change the size of history", 27 | "change-size-suggestion-dialogue-message-failure": "Failed to update the number of suggestions.", 28 | "change-size-suggestion-dialogue-placeholder": "a positive integer", 29 | "change-size-suggestion-dialogue-title": "Change the number of suggestions", 30 | "clear-history-dialogue-content": "Are you sure you want to clear history?", 31 | "clear-history-dialogue-message-failure": "Failed to clear history.", 32 | "clear-history-dialogue-title": "Clear history", 33 | "confirm-recreate-ngram-dialogue-content": "Are you sure you want to recreate the ngram index?", 34 | "confirm-recreate-ngram-dialogue-message-failure": "Failed to recreate ngram index.", 35 | "confirm-recreate-ngram-dialogue-message-in-progress": "Recreating ngram index…", 36 | "confirm-recreate-ngram-dialogue-message-success": "Recreated ngram index.", 37 | "confirm-recreate-ngram-dialogue-title": "Recreate ngram index", 38 | "dark-reader-config-brightness": "Brightness", 39 | "dark-reader-config-contrast": "Contrast", 40 | "dark-reader-config-sepia": "Sepia", 41 | "dark-reader-config-enabled": "Enabled", 42 | "dark-reader-config-performance-warning": "Please note, this extension may cause articles to load a bit slower than usual.\n", 43 | "dark-reader-config-title": "Dark Reader extension preferences", 44 | "dictionary-manager-alert-edit-name-failure": "Failed to update dictionary name.", 45 | "dictionary-manager-alert-delete-dictionary-failure": "Failed to delete dictionary.", 46 | "dictionary-manager-alert-failure-reordering": "Failed to reorder dictionaries.", 47 | "dictionary-manager-dialogue-edit-name-title": "Edit dictionary name", 48 | "dictionary-manager-dialogue-delete-dictionary-content": "Are you sure you want to delete ‘{0}’ from the database? The dictionary files will not be deleted.", 49 | "dictionary-manager-dialogue-delete-dictionary-title": "Delete dictionary", 50 | "dictionary-manager-dialogue-name-placeholder": "new dictionary name", 51 | "drawer-library-label": "Library", 52 | "drawer-query-label": "Query", 53 | "drawer-reconnect-label": "Reconnect", 54 | "drawer-settings-label": "Settings", 55 | "generic-alert-invalid-number": "Invalid number.", 56 | "generic-cancel": "Cancel", 57 | "generic-delete": "Delete", 58 | "generic-edit": "Edit", 59 | "generic-no": "No", 60 | "generic-ok": "OK", 61 | "generic-rename": "Rename", 62 | "generic-yes": "Yes", 63 | "group-manager-alert-empty-name": "Group name cannot be empty.", 64 | "group-manager-alert-failure-adding-dictionary": "Failed to add dictionary to group.", 65 | "group-manager-alert-failure-adding-group": "Failed to add group.", 66 | "group-manager-alert-failure-changing-languages": "Failed to change the group's languages.", 67 | "group-manager-alert-failure-removing-dictionary": "Failed to remove dictionary from group.", 68 | "group-manager-alert-failure-removing-group": "Failed to remove group.", 69 | "group-manager-alert-failure-renaming-group": "Failed to rename group.", 70 | "group-manager-alert-failure-reordering-groups": "Failed to reorder groups.", 71 | "group-manager-alert-invalid-language-code": "Invalid language code(s) found.", 72 | "group-manager-alert-name-exists": "A group with the same name already exists.", 73 | "group-manager-card-title-dictionaries": "Dictionaries", 74 | "group-manager-card-title-languages": "Languages", 75 | "group-manager-dialogue-add-title": "Add group", 76 | "group-manager-dialogue-add-content": "Group name", 77 | "group-manager-dialogue-change-languages-content": "A comma separated list of two-letter ISO-639-1 language codes. It cannot be empty.", 78 | "group-manager-dialogue-change-languages-title": "Change the group's languages", 79 | "group-manager-dialogue-delete-content": "Are you sure you want to delete the group ‘{0}’?", 80 | "group-manager-dialogue-delete-title": "Delete group", 81 | "group-manager-dialogue-rename-title": "Rename group", 82 | "library-screen-groups": "Groups", 83 | "library-screen-sources": "Sources", 84 | "placeholder-search": "Search…", 85 | "query-screen-failure-fetch-articles": "Failed to fetch articles.", 86 | "query-screen-failure-fetch-suggestions": "Failed to fetch suggestions.", 87 | "script-name-arabic": "Arabic", 88 | "script-name-bengali": "Bengali", 89 | "script-name-chinese-simplified": "Chinese, Simplified", 90 | "script-name-chinese-traditional": "Chinese, Traditional", 91 | "script-name-cyrillic": "Cyrillic", 92 | "script-name-devanagari": "Devanagari", 93 | "script-name-greek": "Greek", 94 | "script-name-gujarati": "Gujarati", 95 | "script-name-gurmukhi": "Gurmukhi", 96 | "script-name-hebrew": "Hebrew", 97 | "script-name-japanese": "Japanese", 98 | "script-name-kannada": "Kannada", 99 | "script-name-khmer": "Khmer", 100 | "script-name-korean": "Korean", 101 | "script-name-latin": "Latin", 102 | "script-name-malayalam": "Malayalam", 103 | "script-name-myanmar": "Burmese", 104 | "script-name-oriya": "Oriya", 105 | "script-name-sinhala": "Sinhalese", 106 | "script-name-tamil": "Tamil", 107 | "script-name-telugu": "Telugu", 108 | "script-name-thai": "Thai", 109 | "script-name-tibetan": "Tibetan", 110 | "script-name-vietnamese": "Vietnamese", 111 | "settings-screen-dark-text-colour-title": "Colour of the text in dark mode", 112 | "settings-screen-font-family-title": "Font", 113 | "settings-screen-recreate-ngram-description": "It could be very slow.", 114 | "settings-screen-server-address-title": "Server address", 115 | "settings-screen-size-history-title": "History size", 116 | "settings-screen-size-suggestions-title": "Suggestion size", 117 | "source-manager-alert-duplicate": "The source already exists.", 118 | "source-manager-alert-empty-source": "Source cannot be empty.", 119 | "source-manager-alert-failure-adding-source": "Failed to add source.", 120 | "source-manager-alert-failure-deleting-source": "Failed to delete source.", 121 | "source-manager-alert-failure-fetching-sources": "Failed to fetch sources.", 122 | "source-manager-alert-failure-rescaning-sources": "Failed to rescan sources.", 123 | "source-manager-alert-invalid-source": "Invalid source.", 124 | "source-manager-dialogue-add-source-title": "Add source", 125 | "source-manager-dialogue-rescanning-sources-title": "Rescanning sources", 126 | "source-manager-confirm-delete-title": "Delete source", 127 | "source-manager-confirm-delete-content": "Are you sure you want to delete the source {0} ?" 128 | } -------------------------------------------------------------------------------- /src/translations/el-GR.json: -------------------------------------------------------------------------------- 1 | { 2 | "additional-fonts-dialogue-content": "Download additional fonts from Google Fonts.\nSome fonts are rather large, so please only use when you see little boxes or question marks in the text.", 3 | "additional-fonts-dialogue-title": "Additional fonts", 4 | "add-dictionary-dialogue-alert-duplicate": "A dictionary with the same name already exists.", 5 | "add-dictionary-dialogue-alert-empty-filename": "Filename cannot be empty.", 6 | "add-dictionary-dialogue-alert-empty-name": "Dictionary name cannot be empty.", 7 | "add-dictionary-dialogue-alert-failure-adding": "Failed to add dictionary.", 8 | "add-dictionary-dialogue-alert-failure-validating": "Failed to validate dictionary.", 9 | "add-dictionary-dialogue-alert-invalid": "Invalid dictionary info.", 10 | "add-dictionary-dialogue-title": "Add dictionary", 11 | "add-dictionary-dialogue-content-group": "Group", 12 | "add-dictionary-dialogue-content-filename": "Filename", 13 | "add-dictionary-dialogue-content-format": "Format", 14 | "add-dictionary-dialogue-content-name": "Dictionary name", 15 | "app-context-message-failure-fetching-data": "Failed to fetch data from the server. Please check your connection or change the server address and try again.", 16 | "article-view-tap-to-search-dialogue-message": "Search for ‘{0}’?", 17 | "change-address-dialogue-message-unable-to-connect": "Unable to connect to server.", 18 | "change-address-dialogue-title": "Change server address", 19 | "change-colour-dialogue-content": "A value such as ‘white’ or ‘grey’ is recommended.\nThis setting only takes effect when the Dark Reader extension is disabled.", 20 | "change-colour-dialogue-title": "Change the colour of the text in the dark mode", 21 | "change-font-dialogue-font-sans-serif": "sans-serif", 22 | "change-font-dialogue-font-serif": "serif", 23 | "change-font-dialogue-title": "Change font", 24 | "change-size-history-dialogue-message-failure": "Failed to update history size.", 25 | "change-size-history-dialogue-placeholder": "a non-negative integer", 26 | "change-size-history-dialogue-title": "Change the size of history", 27 | "change-size-suggestion-dialogue-message-failure": "Failed to update the number of suggestions.", 28 | "change-size-suggestion-dialogue-placeholder": "a positive integer", 29 | "change-size-suggestion-dialogue-title": "Change the number of suggestions", 30 | "clear-history-dialogue-content": "Are you sure you want to clear history?", 31 | "clear-history-dialogue-message-failure": "Failed to clear history.", 32 | "clear-history-dialogue-title": "Clear history", 33 | "confirm-recreate-ngram-dialogue-content": "Are you sure you want to recreate the ngram index?", 34 | "confirm-recreate-ngram-dialogue-message-failure": "Failed to recreate ngram index.", 35 | "confirm-recreate-ngram-dialogue-message-in-progress": "Recreating ngram index…", 36 | "confirm-recreate-ngram-dialogue-message-success": "Recreated ngram index.", 37 | "confirm-recreate-ngram-dialogue-title": "Recreate ngram index", 38 | "dark-reader-config-brightness": "Brightness", 39 | "dark-reader-config-contrast": "Contrast", 40 | "dark-reader-config-sepia": "Sepia", 41 | "dark-reader-config-enabled": "Enabled", 42 | "dark-reader-config-performance-warning": "Please note, this extension may cause articles to load a bit slower than usual.\n", 43 | "dark-reader-config-title": "Dark Reader extension preferences", 44 | "dictionary-manager-alert-edit-name-failure": "Failed to update dictionary name.", 45 | "dictionary-manager-alert-delete-dictionary-failure": "Failed to delete dictionary.", 46 | "dictionary-manager-alert-failure-reordering": "Failed to reorder dictionaries.", 47 | "dictionary-manager-dialogue-edit-name-title": "Edit dictionary name", 48 | "dictionary-manager-dialogue-delete-dictionary-content": "Are you sure you want to delete ‘{0}’ from the database? The dictionary files will not be deleted.", 49 | "dictionary-manager-dialogue-delete-dictionary-title": "Delete dictionary", 50 | "dictionary-manager-dialogue-name-placeholder": "new dictionary name", 51 | "drawer-library-label": "Library", 52 | "drawer-query-label": "Query", 53 | "drawer-reconnect-label": "Reconnect", 54 | "drawer-settings-label": "Settings", 55 | "generic-alert-invalid-number": "Invalid number.", 56 | "generic-cancel": "Cancel", 57 | "generic-delete": "Delete", 58 | "generic-edit": "Edit", 59 | "generic-no": "No", 60 | "generic-ok": "OK", 61 | "generic-rename": "Rename", 62 | "generic-yes": "Yes", 63 | "group-manager-alert-empty-name": "Group name cannot be empty.", 64 | "group-manager-alert-failure-adding-dictionary": "Failed to add dictionary to group.", 65 | "group-manager-alert-failure-adding-group": "Failed to add group.", 66 | "group-manager-alert-failure-changing-languages": "Failed to change the group's languages.", 67 | "group-manager-alert-failure-removing-dictionary": "Failed to remove dictionary from group.", 68 | "group-manager-alert-failure-removing-group": "Failed to remove group.", 69 | "group-manager-alert-failure-renaming-group": "Failed to rename group.", 70 | "group-manager-alert-failure-reordering-groups": "Failed to reorder groups.", 71 | "group-manager-alert-invalid-language-code": "Invalid language code(s) found.", 72 | "group-manager-alert-name-exists": "A group with the same name already exists.", 73 | "group-manager-card-title-dictionaries": "Dictionaries", 74 | "group-manager-card-title-languages": "Languages", 75 | "group-manager-dialogue-add-title": "Add group", 76 | "group-manager-dialogue-add-content": "Group name", 77 | "group-manager-dialogue-change-languages-content": "A comma separated list of two-letter ISO-639-1 language codes. It cannot be empty.", 78 | "group-manager-dialogue-change-languages-title": "Change the group's languages", 79 | "group-manager-dialogue-delete-content": "Are you sure you want to delete the group ‘{0}’?", 80 | "group-manager-dialogue-delete-title": "Delete group", 81 | "group-manager-dialogue-rename-title": "Rename group", 82 | "library-screen-groups": "Groups", 83 | "library-screen-sources": "Sources", 84 | "placeholder-search": "Search…", 85 | "query-screen-failure-fetch-articles": "Failed to fetch articles.", 86 | "query-screen-failure-fetch-suggestions": "Failed to fetch suggestions.", 87 | "script-name-arabic": "Arabic", 88 | "script-name-bengali": "Bengali", 89 | "script-name-chinese-simplified": "Chinese, Simplified", 90 | "script-name-chinese-traditional": "Chinese, Traditional", 91 | "script-name-cyrillic": "Cyrillic", 92 | "script-name-devanagari": "Devanagari", 93 | "script-name-greek": "Greek", 94 | "script-name-gujarati": "Gujarati", 95 | "script-name-gurmukhi": "Gurmukhi", 96 | "script-name-hebrew": "Hebrew", 97 | "script-name-japanese": "Japanese", 98 | "script-name-kannada": "Kannada", 99 | "script-name-khmer": "Khmer", 100 | "script-name-korean": "Korean", 101 | "script-name-latin": "Latin", 102 | "script-name-malayalam": "Malayalam", 103 | "script-name-myanmar": "Burmese", 104 | "script-name-oriya": "Oriya", 105 | "script-name-sinhala": "Sinhalese", 106 | "script-name-tamil": "Tamil", 107 | "script-name-telugu": "Telugu", 108 | "script-name-thai": "Thai", 109 | "script-name-tibetan": "Tibetan", 110 | "script-name-vietnamese": "Vietnamese", 111 | "settings-screen-dark-text-colour-title": "Colour of the text in dark mode", 112 | "settings-screen-font-family-title": "Font", 113 | "settings-screen-recreate-ngram-description": "It could be very slow.", 114 | "settings-screen-server-address-title": "Server address", 115 | "settings-screen-size-history-title": "History size", 116 | "settings-screen-size-suggestions-title": "Suggestion size", 117 | "source-manager-alert-duplicate": "The source already exists.", 118 | "source-manager-alert-empty-source": "Source cannot be empty.", 119 | "source-manager-alert-failure-adding-source": "Failed to add source.", 120 | "source-manager-alert-failure-deleting-source": "Failed to delete source.", 121 | "source-manager-alert-failure-fetching-sources": "Failed to fetch sources.", 122 | "source-manager-alert-failure-rescaning-sources": "Failed to rescan sources.", 123 | "source-manager-alert-invalid-source": "Invalid source.", 124 | "source-manager-dialogue-add-source-title": "Add source", 125 | "source-manager-dialogue-rescanning-sources-title": "Rescanning sources", 126 | "source-manager-confirm-delete-title": "Delete source", 127 | "source-manager-confirm-delete-content": "Are you sure you want to delete the source {0} ?" 128 | } -------------------------------------------------------------------------------- /src/translations/en-GB.json: -------------------------------------------------------------------------------- 1 | { 2 | "additional-fonts-dialogue-content": "Download additional fonts from Google Fonts.\nSome fonts are rather large, so please only use when you see little boxes or question marks in the text.", 3 | "additional-fonts-dialogue-title": "Additional fonts", 4 | "add-dictionary-dialogue-alert-duplicate": "A dictionary with the same name already exists.", 5 | "add-dictionary-dialogue-alert-empty-filename": "Filename cannot be empty.", 6 | "add-dictionary-dialogue-alert-empty-name": "Dictionary name cannot be empty.", 7 | "add-dictionary-dialogue-alert-failure-adding": "Failed to add dictionary.", 8 | "add-dictionary-dialogue-alert-failure-validating": "Failed to validate dictionary.", 9 | "add-dictionary-dialogue-alert-invalid": "Invalid dictionary info.", 10 | "add-dictionary-dialogue-title": "Add dictionary", 11 | "add-dictionary-dialogue-content-group": "Group", 12 | "add-dictionary-dialogue-content-filename": "Filename", 13 | "add-dictionary-dialogue-content-format": "Format", 14 | "add-dictionary-dialogue-content-name": "Dictionary name", 15 | "app-context-message-failure-fetching-data": "Failed to fetch data from the server. Please check your connection or change the server address and try again.", 16 | "article-view-tap-to-search-dialogue-message": "Search for ‘{0}’?", 17 | "change-address-dialogue-message-unable-to-connect": "Unable to connect to server.", 18 | "change-address-dialogue-title": "Change server address", 19 | "change-colour-dialogue-content": "A value such as ‘white’ or ‘grey’ is recommended.\nThis setting only takes effect when the Dark Reader extension is disabled.", 20 | "change-colour-dialogue-title": "Change the colour of the text in the dark mode", 21 | "change-font-dialogue-font-sans-serif": "sans-serif", 22 | "change-font-dialogue-font-serif": "serif", 23 | "change-font-dialogue-title": "Change font", 24 | "change-size-history-dialogue-message-failure": "Failed to update history size.", 25 | "change-size-history-dialogue-placeholder": "a non-negative integer", 26 | "change-size-history-dialogue-title": "Change the size of history", 27 | "change-size-suggestion-dialogue-message-failure": "Failed to update the number of suggestions.", 28 | "change-size-suggestion-dialogue-placeholder": "a positive integer", 29 | "change-size-suggestion-dialogue-title": "Change the number of suggestions", 30 | "clear-history-dialogue-content": "Are you sure you want to clear history?", 31 | "clear-history-dialogue-message-failure": "Failed to clear history.", 32 | "clear-history-dialogue-title": "Clear history", 33 | "confirm-recreate-ngram-dialogue-content": "Are you sure you want to recreate the ngram index?", 34 | "confirm-recreate-ngram-dialogue-message-failure": "Failed to recreate ngram index.", 35 | "confirm-recreate-ngram-dialogue-message-in-progress": "Recreating ngram index…", 36 | "confirm-recreate-ngram-dialogue-message-success": "Recreated ngram index.", 37 | "confirm-recreate-ngram-dialogue-title": "Recreate ngram index", 38 | "dark-reader-config-brightness": "Brightness", 39 | "dark-reader-config-contrast": "Contrast", 40 | "dark-reader-config-sepia": "Sepia", 41 | "dark-reader-config-enabled": "Enabled", 42 | "dark-reader-config-performance-warning": "Please note, this extension may cause articles to load a bit slower than usual.\n", 43 | "dark-reader-config-title": "Dark Reader extension preferences", 44 | "dictionary-manager-alert-edit-name-failure": "Failed to update dictionary name.", 45 | "dictionary-manager-alert-delete-dictionary-failure": "Failed to delete dictionary.", 46 | "dictionary-manager-alert-failure-reordering": "Failed to reorder dictionaries.", 47 | "dictionary-manager-dialogue-edit-name-title": "Edit dictionary name", 48 | "dictionary-manager-dialogue-delete-dictionary-content": "Are you sure you want to delete ‘{0}’ from the database? The dictionary files will not be deleted.", 49 | "dictionary-manager-dialogue-delete-dictionary-title": "Delete dictionary", 50 | "dictionary-manager-dialogue-name-placeholder": "new dictionary name", 51 | "drawer-library-label": "Library", 52 | "drawer-query-label": "Query", 53 | "drawer-reconnect-label": "Reconnect", 54 | "drawer-settings-label": "Settings", 55 | "generic-alert-invalid-number": "Invalid number.", 56 | "generic-cancel": "Cancel", 57 | "generic-delete": "Delete", 58 | "generic-edit": "Edit", 59 | "generic-no": "No", 60 | "generic-ok": "OK", 61 | "generic-rename": "Rename", 62 | "generic-yes": "Yes", 63 | "group-manager-alert-empty-name": "Group name cannot be empty.", 64 | "group-manager-alert-failure-adding-dictionary": "Failed to add dictionary to group.", 65 | "group-manager-alert-failure-adding-group": "Failed to add group.", 66 | "group-manager-alert-failure-changing-languages": "Failed to change the group's languages.", 67 | "group-manager-alert-failure-removing-dictionary": "Failed to remove dictionary from group.", 68 | "group-manager-alert-failure-removing-group": "Failed to remove group.", 69 | "group-manager-alert-failure-renaming-group": "Failed to rename group.", 70 | "group-manager-alert-failure-reordering-groups": "Failed to reorder groups.", 71 | "group-manager-alert-invalid-language-code": "Invalid language code(s) found.", 72 | "group-manager-alert-name-exists": "A group with the same name already exists.", 73 | "group-manager-card-title-dictionaries": "Dictionaries", 74 | "group-manager-card-title-languages": "Languages", 75 | "group-manager-dialogue-add-title": "Add group", 76 | "group-manager-dialogue-add-content": "Group name", 77 | "group-manager-dialogue-change-languages-content": "A comma separated list of two-letter ISO-639-1 language codes. It cannot be empty.", 78 | "group-manager-dialogue-change-languages-title": "Change the group's languages", 79 | "group-manager-dialogue-delete-content": "Are you sure you want to delete the group ‘{0}’?", 80 | "group-manager-dialogue-delete-title": "Delete group", 81 | "group-manager-dialogue-rename-title": "Rename group", 82 | "library-screen-groups": "Groups", 83 | "library-screen-sources": "Sources", 84 | "placeholder-search": "Search…", 85 | "query-screen-failure-fetch-articles": "Failed to fetch articles.", 86 | "query-screen-failure-fetch-suggestions": "Failed to fetch suggestions.", 87 | "script-name-arabic": "Arabic", 88 | "script-name-bengali": "Bengali", 89 | "script-name-chinese-simplified": "Chinese, Simplified", 90 | "script-name-chinese-traditional": "Chinese, Traditional", 91 | "script-name-cyrillic": "Cyrillic", 92 | "script-name-devanagari": "Devanagari", 93 | "script-name-greek": "Greek", 94 | "script-name-gujarati": "Gujarati", 95 | "script-name-gurmukhi": "Gurmukhi", 96 | "script-name-hebrew": "Hebrew", 97 | "script-name-japanese": "Japanese", 98 | "script-name-kannada": "Kannada", 99 | "script-name-khmer": "Khmer", 100 | "script-name-korean": "Korean", 101 | "script-name-latin": "Latin", 102 | "script-name-malayalam": "Malayalam", 103 | "script-name-myanmar": "Burmese", 104 | "script-name-oriya": "Oriya", 105 | "script-name-sinhala": "Sinhalese", 106 | "script-name-tamil": "Tamil", 107 | "script-name-telugu": "Telugu", 108 | "script-name-thai": "Thai", 109 | "script-name-tibetan": "Tibetan", 110 | "script-name-vietnamese": "Vietnamese", 111 | "settings-screen-dark-text-colour-title": "Colour of the text in dark mode", 112 | "settings-screen-font-family-title": "Font", 113 | "settings-screen-recreate-ngram-description": "It could be very slow.", 114 | "settings-screen-server-address-title": "Server address", 115 | "settings-screen-size-history-title": "History size", 116 | "settings-screen-size-suggestions-title": "Suggestion size", 117 | "source-manager-alert-duplicate": "The source already exists.", 118 | "source-manager-alert-empty-source": "Source cannot be empty.", 119 | "source-manager-alert-failure-adding-source": "Failed to add source.", 120 | "source-manager-alert-failure-deleting-source": "Failed to delete source.", 121 | "source-manager-alert-failure-fetching-sources": "Failed to fetch sources.", 122 | "source-manager-alert-failure-rescaning-sources": "Failed to rescan sources.", 123 | "source-manager-alert-invalid-source": "Invalid source.", 124 | "source-manager-dialogue-add-source-title": "Add source", 125 | "source-manager-dialogue-rescanning-sources-title": "Rescanning sources", 126 | "source-manager-confirm-delete-title": "Delete source", 127 | "source-manager-confirm-delete-content": "Are you sure you want to delete the source {0} ?" 128 | } --------------------------------------------------------------------------------